فهرست منبع

Merge pull request #387 from YellowLabTools/webp-avif

Introduce a new "Old Image Formats" rule
Gaël Métais 1 سال پیش
والد
کامیت
599d02ea0e

+ 3 - 1
lib/index.js

@@ -33,10 +33,11 @@ var yellowLabTools = function(url, options) {
 
 
             // If a screenshot saveFunction was provided in the options
             // If a screenshot saveFunction was provided in the options
             if (options && typeof options.saveScreenshotFn === 'function') {
             if (options && typeof options.saveScreenshotFn === 'function') {
+                const screenshotTmpPath = data.params.options.screenshot;
                 debug('Now optimizing screenshot...');
                 debug('Now optimizing screenshot...');
 
 
                 // TODO: temporarily set all screenshot sizes to 600px, until we find a solution
                 // TODO: temporarily set all screenshot sizes to 600px, until we find a solution
-                ScreenshotHandler.findAndOptimizeScreenshot(data.params.options.screenshot, 600)
+                ScreenshotHandler.findAndOptimizeScreenshot(screenshotTmpPath, 600)
 
 
                 .then(function(screenshotBuffer) {
                 .then(function(screenshotBuffer) {
                     debug('Screenshot optimized, now saving...');
                     debug('Screenshot optimized, now saving...');
@@ -50,6 +51,7 @@ var yellowLabTools = function(url, options) {
 
 
                     // Remove uneeded temp screenshot path
                     // Remove uneeded temp screenshot path
                     delete data.params.options.screenshot;
                     delete data.params.options.screenshot;
+                    return ScreenshotHandler.deleteTmpFile(screenshotTmpPath);
                 })
                 })
 
 
                 .catch(function(err) {
                 .catch(function(err) {

+ 11 - 1
lib/metadata/policies.js

@@ -729,7 +729,7 @@ var policies = {
     "totalWeight": {
     "totalWeight": {
         "tool": "redownload",
         "tool": "redownload",
         "label": "Total weight",
         "label": "Total weight",
-        "message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.</p>",
+        "message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1.5MB.</p>",
         "isOkThreshold": 1572864,
         "isOkThreshold": 1572864,
         "isBadThreshold": 3145728,
         "isBadThreshold": 3145728,
         "isAbnormalThreshold": 5242880,
         "isAbnormalThreshold": 5242880,
@@ -746,6 +746,16 @@ var policies = {
         "hasOffenders": true,
         "hasOffenders": true,
         "unit": 'bytes'
         "unit": 'bytes'
     },
     },
+    "oldImageFormats": {
+        "tool": "redownload",
+        "label": "Old image formats",
+        "message": "<p>Measures the number of bytes that could be saved by converting images to newer and more efficient formats. The best image format is generally AVIF and the second best is WebP.</p><p>Be careful, you need to provide fallback images for old browsers and search engine bots.</p>",
+        "isOkThreshold": 30720,
+        "isBadThreshold": 307200,
+        "isAbnormalThreshold": 512000,
+        "hasOffenders": true,
+        "unit": 'bytes'
+    },
     "imagesTooLarge": {
     "imagesTooLarge": {
         "tool": "redownload",
         "tool": "redownload",
         "label": "Oversized images",
         "label": "Oversized images",

+ 1 - 0
lib/metadata/scoreProfileGeneric.json

@@ -5,6 +5,7 @@
             "policies": {
             "policies": {
                 "totalWeight": 5,
                 "totalWeight": 5,
                 "imageOptimization": 2,
                 "imageOptimization": 2,
+                "oldImageFormats": 2,
                 "imagesTooLarge": 1,
                 "imagesTooLarge": 1,
                 "compression": 2,
                 "compression": 2,
                 "fileMinification": 2
                 "fileMinification": 2

+ 9 - 80
lib/screenshotHandler.js

@@ -1,90 +1,19 @@
-var debug       = require('debug')('ylt:screenshotHandlerAgent');
-var Jimp        = require('jimp');
+var debug       = require('debug')('ylt:screenshotHandler');
+var sharp       = require('sharp');
 var Q           = require('q');
 var Q           = require('q');
 var fs          = require('fs');
 var fs          = require('fs');
 var path        = require('path');
 var path        = require('path');
 
 
+// Disable sharp cache to reduce the "disk is full" error on Amazon Lambda
+sharp.cache(false);
 
 
 var screenshotHandler = function() {
 var screenshotHandler = function() {
 
 
-    this.findAndOptimizeScreenshot = function(tmpScreenshotPath, width) {
-        var that = this;
-
-        debug('Starting screenshot transformation');
-
-        return this.openImage(tmpScreenshotPath)
-
-            .then(function(image) {
-                that.deleteTmpFile(tmpScreenshotPath);
-                return that.resizeImage(image, width);
-            })
-
-            .then(this.toBuffer);
-    };
-
-
-    this.openImage = function(imagePath) {
-        var deferred = Q.defer();
-
-        Jimp.read(imagePath, function(err, image){
-            if (err) {
-                debug('Could not open imagePath %s', imagePath);
-                debug(err);
-
-                deferred.reject(err);
-            } else {
-                debug('Image correctly open');
-                deferred.resolve(image);
-            }
-        });
-
-        return deferred.promise;
-    };
-
-
-    this.resizeImage = function(image, newWidth) {
-        var deferred = Q.defer();
-
-        var currentWidth = image.bitmap.width;
-
-        if (currentWidth > 0) {
-            var ratio = newWidth / currentWidth;
-
-            image.scale(ratio, function(err, image){
-                if (err) {
-                    debug('Could not resize image');
-                    debug(err);
-
-                    deferred.reject(err);
-                } else {
-                    debug('Image correctly resized');
-                    deferred.resolve(image);
-                }
-            });
-        } else {
-            deferred.reject('Could not resize an empty image');
-        }
-
-        return deferred.promise;        
-    };
-
-
-    this.toBuffer = function(image) {
-        var deferred = Q.defer();
-
-        image.quality(85).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
-            if (err) {
-                debug('Could not save image to buffer');
-                debug(err);
-
-                deferred.reject(err);
-            } else {
-                debug('Image correctly transformed to buffer');
-                deferred.resolve(buffer);
-            }
-        });
-
-        return deferred.promise;
+    this.findAndOptimizeScreenshot = async function(tmpScreenshotPath, width) {
+        return sharp(tmpScreenshotPath)
+            .resize({width: 600})
+            .jpeg({quality: 85})
+            .toBuffer();
     };
     };
 
 
 
 

+ 23 - 54
lib/tools/redownload/contentTypeChecker.js

@@ -1,20 +1,12 @@
-var debug   = require('debug')('ylt:contentTypeChecker');
-var Q       = require('q');
-var isJpg   = require('is-jpg');
-var isPng   = require('is-png');
-var isSvg   = require('is-svg');
-var isGif   = require('is-gif');
-var isWebp  = require('is-webp');
-var isWoff  = require('is-woff');
-var isWoff2 = require('is-woff2');
-var isOtf   = require('is-otf');
-var isTtf   = require('is-ttf');
-var isEot   = require('is-eot');
-var isJson  = require('is-json');
+var debug       = require('debug')('ylt:contentTypeChecker');
+var Q           = require('q');
+var FileType    = require('file-type');
+var isSvg       = require('is-svg');
+var isJson      = require('is-json');
 
 
 var ContentTypeChecker = function() {
 var ContentTypeChecker = function() {
 
 
-    function checkContentType(entry) {
+    async function checkContentType(entry) {
         var deferred = Q.defer();
         var deferred = Q.defer();
         
         
         // Setting isSomething values:
         // Setting isSomething values:
@@ -55,12 +47,12 @@ var ContentTypeChecker = function() {
             var foundType;
             var foundType;
 
 
             try {
             try {
-                foundType = findContentType(entry.weightCheck.bodyBuffer);
+                foundType = await findContentType(entry.weightCheck.bodyBuffer);
 
 
                 // If it's an image or a font, then rewrite.
                 // If it's an image or a font, then rewrite.
                 if (foundType !== null && (foundType.type === 'image' || foundType.type === 'webfont' || foundType.type === 'json')) {
                 if (foundType !== null && (foundType.type === 'image' || foundType.type === 'webfont' || foundType.type === 'json')) {
                     if (foundType.type !== entry.type) {
                     if (foundType.type !== entry.type) {
-                        debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type);
+                        debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.url, foundType.type);
                     }
                     }
                     rewriteContentType(entry, foundType);
                     rewriteContentType(entry, foundType);
                 }
                 }
@@ -76,54 +68,23 @@ var ContentTypeChecker = function() {
         return deferred.promise;
         return deferred.promise;
     }
     }
 
 
-    function findContentType(bodyBuffer) {
+    async function findContentType(bodyBuffer) {
         var bodyStr = bodyBuffer.toString();
         var bodyStr = bodyBuffer.toString();
 
 
-        if (isJpg(bodyBuffer)) {
-            return contentTypes.jpeg;
-        }
-
-        if (isPng(bodyBuffer)) {
-            return contentTypes.png;
-        }
-
         // https://github.com/sindresorhus/is-svg/issues/7
         // https://github.com/sindresorhus/is-svg/issues/7
         if (/<svg/.test(bodyStr) && isSvg(bodyStr)) {
         if (/<svg/.test(bodyStr) && isSvg(bodyStr)) {
             return contentTypes.svg;
             return contentTypes.svg;
         }
         }
 
 
-        if (isGif(bodyBuffer)) {
-            return contentTypes.gif;
-        }
-
-        if (isWebp(bodyBuffer)) {
-            return contentTypes.webp;
-        }
-
-        if (isWoff(bodyBuffer)) {
-            return contentTypes.woff;
-        }
-
-        if (isWoff2(bodyBuffer)) {
-            return contentTypes.woff2;
-        }
-
-        if (isOtf(bodyBuffer)) {
-            return contentTypes.otf;
-        }
-
-        if (isTtf(bodyBuffer)) {
-            return contentTypes.ttf;
-        }
-
-        if (isEot(bodyBuffer)) {
-            return contentTypes.eot;
-        }
-
         if (isJson(bodyStr)) {
         if (isJson(bodyStr)) {
             return contentTypes.json;
             return contentTypes.json;
         }
         }
 
 
+        const type = await FileType.fromBuffer(bodyBuffer);
+        if (type && type.ext && contentTypes[type.ext]) {
+            return contentTypes[type.ext];
+        }
+
         return null;
         return null;
     }
     }
 
 
@@ -146,7 +107,7 @@ var ContentTypeChecker = function() {
     }
     }
 
 
     var contentTypes = {
     var contentTypes = {
-        jpeg: {
+        jpg: {
             type: 'image',
             type: 'image',
             mimes: ['image/jpeg'],
             mimes: ['image/jpeg'],
             updateFn: function(entry) {
             updateFn: function(entry) {
@@ -187,6 +148,14 @@ var ContentTypeChecker = function() {
                 entry.isImage = true;
                 entry.isImage = true;
             }
             }
         },
         },
+        avif: {
+            type: 'image',
+            mimes: ['image/avif'],
+            updateFn: function(entry) {
+                entry.type = 'image';
+                entry.isImage = true;
+            }
+        },
         woff: {
         woff: {
             type: 'webfont',
             type: 'webfont',
             mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'],
             mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'],

+ 4 - 0
lib/tools/redownload/imageDimensions.js

@@ -43,6 +43,10 @@ var ImageDimensions = function() {
         return entry.isImage && entry.contentType === 'image/png';
         return entry.isImage && entry.contentType === 'image/png';
     }
     }
 
 
+    function isWebP(entry) {
+        return entry.isImage && entry.contentType === 'image/webp';
+    }
+
     return {
     return {
         getDimensions: getDimensions
         getDimensions: getDimensions
     };
     };

+ 138 - 0
lib/tools/redownload/imageReformater.js

@@ -0,0 +1,138 @@
+var debug       = require('debug')('ylt:imageReformater');
+var sharp       = require('sharp');
+
+// Disable sharp cache to reduce the "disk is full" error on Amazon Lambda
+sharp.cache(false);
+
+var ImageOptimizer = function() {
+
+    // https://www.industrialempathy.com/posts/avif-webp-quality-settings
+    const WEBP_QUALITY = 82;
+    const AVIF_QUALITY = 64;
+
+    async function reformatImage(entry) {
+        if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
+            // No valid file available
+            return entry;
+        }
+
+        var fileSize = entry.weightCheck.uncompressedSize;
+        debug('Let\'s try to convert %s to other image formats', entry.url);
+        debug('Current file size is %d', fileSize);
+
+        var animated = await isAnimated(entry);
+        debug('Check if the file is animated: %s', animated);
+
+
+        if (isJPEG(entry) || isPNG(entry)) {
+            debug('File is %s, let\'s try to convert it to WebP', entry.contentType);
+
+            try {
+
+                const webpFile = await convertToWebp(entry.weightCheck.bodyBuffer, animated);
+
+                if (webpFile) {
+                    var webpFileSize = webpFile.length;
+
+                    debug('WebP transformation complete for %s', entry.url);
+                    debug('WebP size is %d bytes', webpFileSize);
+
+                    if (webpFile.length > 0 && gainIsEnough(fileSize, webpFileSize)) {
+                        entry.weightCheck.webpSize = webpFileSize;
+                        debug('WebP size is %d bytes smaller (-%d%)', fileSize - webpFileSize, Math.round((fileSize - webpFileSize) * 100 / fileSize));
+                    }
+
+                } else {
+                    debug('Convertion to WebP didn\'t work');
+                }
+
+            } catch(err) {
+                debug('Error while converting to WebP, ignoring');
+            }
+        }
+
+        if (!animated && (isJPEG(entry) || isPNG(entry) || isWebP(entry))) {
+            debug('File is %s and is not animated, let\'s try to convert it to AVIF', entry.contentType);
+
+            try {
+
+                const avifFile = await convertToAvif(entry.weightCheck.bodyBuffer);
+
+                if (avifFile) {
+                    var avifFileSize = avifFile.length;
+
+                    debug('AVIF transformation complete for %s', entry.url);
+                    debug('AVIF size is %d bytes', avifFileSize);
+
+                    if (avifFile.length > 0 && gainIsEnough(fileSize, avifFileSize)) {
+                        entry.weightCheck.avifSize = avifFileSize;
+                        debug('AVIF size is %d bytes smaller (-%d%)', fileSize - avifFileSize, Math.round((fileSize - avifFileSize) * 100 / fileSize));
+                    }
+
+                } else {
+                    debug('Convertion to AVIF didn\'t work');
+                }
+
+            } catch(err) {
+                debug('Error while converting to AVIF, ignoring');
+            }
+        }
+
+        return entry;
+    }
+
+    async function convertToWebp(bodyBuffer, isAnimated) {
+        return sharp(bodyBuffer, {animated: isAnimated})
+            .webp({quality: WEBP_QUALITY, alphaQuality: WEBP_QUALITY})
+            .toBuffer();
+    }
+
+    async function convertToAvif(bodyBuffer) {
+        return sharp(bodyBuffer)
+            .webp({quality: AVIF_QUALITY})
+            .toBuffer();
+    }
+
+    // The gain is estimated of enough value if it's over 2KB or over 20%,
+    // but it's ignored if is below 100 bytes
+    function gainIsEnough(oldWeight, newWeight) {
+        var gain = oldWeight - newWeight;
+        var ratio = gain / oldWeight;
+        return (gain > 2048 || (ratio > 0.2 && gain > 100));
+    }
+
+    function isJPEG(entry) {
+        return entry.isImage && entry.contentType === 'image/jpeg';
+    }
+
+    function isPNG(entry) {
+        return entry.isImage && entry.contentType === 'image/png';
+    }
+
+    function isWebP(entry) {
+        return entry.isImage && entry.contentType === 'image/webp';
+    }
+
+    function entryTypeCanBeReformated(entry) {
+        return isJPEG(entry) || isPNG(entry) || isWebP(entry);
+    }
+
+    async function isAnimated(entry) {
+        if (isWebP(entry)) {
+            const metadata = await sharp(entry.weightCheck.bodyBuffer).metadata();
+            return metadata.pages > 1;
+        }
+        return false;
+    }
+
+    return {
+        reformatImage: reformatImage,
+        convertToWebp: convertToWebp,
+        convertToAvif: convertToAvif,
+        gainIsEnough: gainIsEnough,
+        entryTypeCanBeReformated: entryTypeCanBeReformated,
+        isAnimated: isAnimated
+    };
+};
+
+module.exports = new ImageOptimizer();

+ 73 - 3
lib/tools/redownload/redownload.js

@@ -15,6 +15,7 @@ var request             = require('request');
 var md5                 = require('md5');
 var md5                 = require('md5');
 
 
 var imageOptimizer      = require('./imageOptimizer');
 var imageOptimizer      = require('./imageOptimizer');
+var imageReformater     = require('./imageReformater');
 var fileMinifier        = require('./fileMinifier');
 var fileMinifier        = require('./fileMinifier');
 var gzipCompressor      = require('./gzipCompressor');
 var gzipCompressor      = require('./gzipCompressor');
 var brotliCompressor    = require('./brotliCompressor');
 var brotliCompressor    = require('./brotliCompressor');
@@ -78,6 +79,10 @@ var Redownload = function() {
 
 
                 .then(imageOptimizer.optimizeImage)
                 .then(imageOptimizer.optimizeImage)
 
 
+                .then(function(entry) {
+                    return Q(imageReformater.reformatImage(entry));
+                })
+
                 .then(imageDimensions.getDimensions)
                 .then(imageDimensions.getDimensions)
 
 
                 .then(fileMinifier.minifyFile)
                 .then(fileMinifier.minifyFile)
@@ -91,7 +96,7 @@ var Redownload = function() {
                 })
                 })
 
 
                 .then(function(newEntry) {
                 .then(function(newEntry) {
-                    debug('File %s - Redownloaded, optimized, minified, compressed, analyzed: done', entry.url);
+                    debug('File %s - Redownloaded, optimized, reformated, minified, compressed, analyzed: done', entry.url);
 
 
                     // For the progress bar
                     // For the progress bar
                     doneCount ++;
                     doneCount ++;
@@ -154,6 +159,10 @@ var Redownload = function() {
                 offenders.imageOptimization = listImagesNotOptimized(results);
                 offenders.imageOptimization = listImagesNotOptimized(results);
                 metrics.imageOptimization = offenders.imageOptimization.totalGain;
                 metrics.imageOptimization = offenders.imageOptimization.totalGain;
 
 
+                // Old image formats
+                offenders.oldImageFormats = listImagesWithOldFormats(results);
+                metrics.oldImageFormats = offenders.oldImageFormats.totalGain;
+
                 // Image width
                 // Image width
                 offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device);
                 offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device);
                 metrics.imagesTooLarge = offenders.imagesTooLarge.length;
                 metrics.imagesTooLarge = offenders.imagesTooLarge.length;
@@ -398,6 +407,67 @@ var Redownload = function() {
         return results;
         return results;
     }
     }
 
 
+    function listImagesWithOldFormats(requests) {
+        var results = {
+            totalGain: 0,
+            images: []
+        };
+
+        requests.forEach(function(req) {
+
+            if (req.weightCheck.bodySize > 0 &&
+                imageReformater.entryTypeCanBeReformated(req) &&
+                (req.weightCheck.webpSize > 0 || req.weightCheck.avifSize > 0)) {
+
+                var image = {
+                    url: req.url,
+                    originalWeigth: req.weightCheck.bodySize,
+                };
+
+                switch (req.contentType) {
+                    case 'image/jpeg':
+                        image.originalFormat = 'JPEG';
+                        break;
+                    case 'image/png':
+                        image.originalFormat = 'PNG';
+                        break;
+                    case 'image/gif':
+                        image.originalFormat = 'GIF';
+                        break;
+                    case 'image/webp':
+                        image.originalFormat = 'WebP';
+                        break;
+                    case 'image/avif':
+                        image.originalFormat = 'AVIF';
+                        break;
+                }
+
+                if (req.weightCheck.webpSize) {
+                    image.webpSize = req.weightCheck.webpSize;
+                    image.webpGain = req.weightCheck.bodySize - req.weightCheck.webpSize;
+                    
+                    image.bestFormat = 'WebP';
+                    image.maxGain = image.webpGain;
+                }
+
+                if (req.weightCheck.avifSize) {
+                    image.avifSize = req.weightCheck.avifSize;
+                    image.avifGain = req.weightCheck.bodySize - req.weightCheck.avifSize;
+
+                    if (!req.weightCheck.webpSize || req.weightCheck.webpSize > req.weightCheck.avifSize) {
+                        image.bestFormat = 'AVIF';
+                        image.maxGain = image.avifGain;
+                    }
+                }
+
+                results.totalGain += image.maxGain;
+                results.images.push(image);
+            }
+        });
+
+        return results;
+    }
+
     function listImagesTooLarge(requests, device) {
     function listImagesTooLarge(requests, device) {
         var results = [];
         var results = [];
 
 
@@ -819,9 +889,9 @@ var Redownload = function() {
 
 
         debug('Downloading %s', entry.url);
         debug('Downloading %s', entry.url);
 
 
-        // Always add compression and webp headers before sending, in case the server listens to them
+        // Always add compression and webp/avif headers before sending, in case the server listens to them
         var reqHeaders = [];
         var reqHeaders = [];
-        reqHeaders['Accept'] = '*/*,image/webp';
+        reqHeaders['Accept'] = '*/*,image/webp,image/avif';
         reqHeaders['Accept-Encoding'] = 'gzip, deflate, br';
         reqHeaders['Accept-Encoding'] = 'gzip, deflate, br';
         reqHeaders['Connection'] = 'keep-alive';
         reqHeaders['Connection'] = 'keep-alive';
         reqHeaders['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36';
         reqHeaders['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36';

+ 2 - 10
package.json

@@ -26,6 +26,7 @@
     "css-mq-parser": "0.0.3",
     "css-mq-parser": "0.0.3",
     "debug": "4.3.4",
     "debug": "4.3.4",
     "easyxml": "2.0.1",
     "easyxml": "2.0.1",
+    "file-type": "16.5.3",
     "fontkit": "2.0.2",
     "fontkit": "2.0.2",
     "html-minifier": "4.0.0",
     "html-minifier": "4.0.0",
     "image-size": "1.0.2",
     "image-size": "1.0.2",
@@ -34,24 +35,15 @@
     "imagemin-jpegtran": "7.0.0",
     "imagemin-jpegtran": "7.0.0",
     "imagemin-optipng": "8.0.0",
     "imagemin-optipng": "8.0.0",
     "imagemin-svgo": "9.0.0",
     "imagemin-svgo": "9.0.0",
-    "is-eot": "1.0.0",
-    "is-gif": "3.0.0",
-    "is-jpg": "2.0.0",
     "is-json": "2.0.1",
     "is-json": "2.0.1",
-    "is-otf": "0.1.2",
-    "is-png": "1.1.0",
     "is-svg": "3.0.0",
     "is-svg": "3.0.0",
-    "is-ttf": "0.2.2",
-    "is-webp": "1.0.1",
-    "is-woff": "1.0.3",
-    "is-woff2": "1.0.0",
-    "jimp": "0.22.8",
     "md5": "2.3.0",
     "md5": "2.3.0",
     "meow": "5.0.0",
     "meow": "5.0.0",
     "parse-color": "1.0.0",
     "parse-color": "1.0.0",
     "phantomas": "2.8.0",
     "phantomas": "2.8.0",
     "q": "1.5.1",
     "q": "1.5.1",
     "request": "2.88.2",
     "request": "2.88.2",
+    "sharp": "0.32.3",
     "ttf2woff2": "5.0.0",
     "ttf2woff2": "5.0.0",
     "uglify-js": "3.17.4",
     "uglify-js": "3.17.4",
     "woff-tools": "0.1.0"
     "woff-tools": "0.1.0"

+ 5 - 5
test/core/contentTypeCheckerTest.js

@@ -10,11 +10,11 @@ describe('contentTypeChecker', function() {
     var svgImageContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
     var svgImageContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
     var cssFileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
     var cssFileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
     
     
-    it('detect the right content type', function() {
-        contentTypeChecker.findContentType(jpgImageContent).mimes.should.deep.equal(['image/jpeg']);
-        contentTypeChecker.findContentType(pngImageContent).mimes.should.deep.equal(['image/png']);
-        contentTypeChecker.findContentType(svgImageContent).mimes.should.deep.equal(['image/svg+xml']);
-        should.equal(contentTypeChecker.findContentType(cssFileContent), null);
+    it('detect the right content type', async function() {
+        (await contentTypeChecker.findContentType(jpgImageContent)).mimes.should.deep.equal(['image/jpeg']);
+        (await contentTypeChecker.findContentType(pngImageContent)).mimes.should.deep.equal(['image/png']);
+        (await contentTypeChecker.findContentType(svgImageContent)).mimes.should.deep.equal(['image/svg+xml']);
+        should.equal(await contentTypeChecker.findContentType(cssFileContent), null);
     });
     });
 
 
 });
 });

+ 122 - 0
test/core/imageReformaterTest.js

@@ -0,0 +1,122 @@
+var should = require('chai').should();
+var imageReformater = require('../../lib/tools/redownload/imageReformater');
+var fs = require('fs');
+var path = require('path');
+
+describe('imageReformater', function() {
+
+    it('should convert a JPEG image to WebP and AVIF', async function() {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
+        let entry = {
+            isImage: true,
+            type: 'image',
+            contentType: 'image/jpeg',
+            weightCheck: {
+                bodyBuffer: fileContent,
+                uncompressedSize: fileContent.length
+            }
+        };
+
+        var newEntry = await imageReformater.reformatImage(entry);
+
+        newEntry.weightCheck.should.have.a.property('webpSize');
+        newEntry.weightCheck.webpSize.should.be.below(fileContent.length);
+
+        newEntry.weightCheck.should.have.a.property('avifSize');
+        newEntry.weightCheck.avifSize.should.be.below(fileContent.length);
+    });
+
+    it('should convert a PNG image to WebP and AVIF', async function() {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
+        let entry = {
+            isImage: true,
+            type: 'image',
+            contentType: 'image/png',
+            weightCheck: {
+                bodyBuffer: fileContent,
+                uncompressedSize: fileContent.length
+            }
+        };
+
+        var newEntry = await imageReformater.reformatImage(entry);
+
+        newEntry.weightCheck.should.have.a.property('webpSize');
+        newEntry.weightCheck.webpSize.should.be.below(fileContent.length);
+
+        newEntry.weightCheck.should.have.a.property('avifSize');
+        newEntry.weightCheck.avifSize.should.be.below(fileContent.length);
+    });
+
+    it('should convert a WebP image to AVIF', async function() {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
+        let entry = {
+            isImage: true,
+            type: 'image',
+            contentType: 'image/webp',
+            weightCheck: {
+                bodyBuffer: fileContent,
+                uncompressedSize: fileContent.length
+            }
+        };
+
+        var newEntry = await imageReformater.reformatImage(entry);
+
+        newEntry.weightCheck.should.not.have.a.property('webpSize');
+
+        newEntry.weightCheck.should.have.a.property('avifSize');
+        newEntry.weightCheck.avifSize.should.be.below(fileContent.length);
+    });
+
+    it('should recognize an animated WebP', async function() {
+        // Test on an animated image
+        let fileContent = fs.readFileSync(path.resolve(__dirname, '../www/animated.webp'));
+        let entry = {
+            isImage: true,
+            type: 'image',
+            contentType: 'image/webp',
+            weightCheck: {
+                bodyBuffer: fileContent,
+                uncompressedSize: fileContent.length
+            }
+        };
+
+        (await imageReformater.isAnimated(entry)).should.equal(true);
+
+        // Test on a not animated image
+        fileContent = fs.readFileSync(path.resolve(__dirname, '../www/monster.webp'));
+        entry.weightCheck.bodyBuffer = fileContent;
+        (await imageReformater.isAnimated(entry)).should.equal(false);
+    });
+
+    it('should not convert an animated WebP', async function() {
+        // Test on an animated image
+        let fileContent = fs.readFileSync(path.resolve(__dirname, '../www/animated.webp'));
+        let entry = {
+            isImage: true,
+            type: 'image',
+            contentType: 'image/webp',
+            weightCheck: {
+                bodyBuffer: fileContent,
+                uncompressedSize: fileContent.length
+            }
+        };
+
+        var newEntry = await imageReformater.reformatImage(entry);
+
+        // Test on a not animated image
+        newEntry.weightCheck.should.not.have.a.property('avifSize');
+    });
+
+    it('should determine if gain is enough', function() {
+        imageReformater.gainIsEnough(20000, 10000).should.equal(true);
+        imageReformater.gainIsEnough(2000, 1000).should.equal(true);
+        imageReformater.gainIsEnough(20000, 21000).should.equal(false);
+        imageReformater.gainIsEnough(20000, 40000).should.equal(false);
+        imageReformater.gainIsEnough(20000, 19500).should.equal(false);
+        imageReformater.gainIsEnough(250, 120).should.equal(true);
+        imageReformater.gainIsEnough(200, 120).should.equal(false);
+        imageReformater.gainIsEnough(2000, 1900).should.equal(false);
+        imageReformater.gainIsEnough(200000, 197000).should.equal(true);
+    });
+
+});

+ 5 - 10
test/core/redownloadTest.js

@@ -86,6 +86,10 @@ describe('redownload', function() {
             data.toolsResults.redownload.offenders.imageOptimization.totalGain.should.be.above(0);
             data.toolsResults.redownload.offenders.imageOptimization.totalGain.should.be.above(0);
             data.toolsResults.redownload.offenders.imageOptimization.images.length.should.equal(2);
             data.toolsResults.redownload.offenders.imageOptimization.images.length.should.equal(2);
 
 
+            data.toolsResults.redownload.offenders.should.have.a.property('oldImageFormats');
+            data.toolsResults.redownload.offenders.oldImageFormats.totalGain.should.be.above(0);
+            data.toolsResults.redownload.offenders.oldImageFormats.images.length.should.equal(1);
+
             data.toolsResults.redownload.offenders.should.have.a.property('imagesTooLarge');
             data.toolsResults.redownload.offenders.should.have.a.property('imagesTooLarge');
             data.toolsResults.redownload.offenders.imagesTooLarge.length.should.equal(0);
             data.toolsResults.redownload.offenders.imagesTooLarge.length.should.equal(0);
 
 
@@ -167,18 +171,9 @@ describe('redownload', function() {
         redownload.redownloadEntry(entry)
         redownload.redownloadEntry(entry)
 
 
         .then(function(newEntry) {
         .then(function(newEntry) {
-
             newEntry.weightCheck.bodySize.should.equal(4193);
             newEntry.weightCheck.bodySize.should.equal(4193);
             newEntry.weightCheck.bodyBuffer.should.deep.equal(fileContent);
             newEntry.weightCheck.bodyBuffer.should.deep.equal(fileContent);
-
-            // Opening the image in jimp to check if the format is good
-            var Jimp = require('jimp');
-            Jimp.read(newEntry.weightCheck.bodyBuffer, function(err, image) {
-                image.bitmap.width.should.equal(620);
-                image.bitmap.height.should.equal(104);
-                done(err);
-            });
-
+            done();
         })
         })
 
 
         .fail(function(err) {
         .fail(function(err) {

BIN
test/www/animated.webp


BIN
test/www/monster.webp