瀏覽代碼

New rule: oversized images (#276)

New rule: oversized images
Gaël Métais 6 年之前
父節點
當前提交
8ffab2ae70

+ 2 - 2
front/src/css/rule.css

@@ -261,8 +261,8 @@
 }
 .smallPreview {
   display: block;
-  max-height: 4em;
-  max-width: 8em;
+  max-height: 6em;
+  max-width: 16em;
   border: 1px solid #000;
   margin: 1em auto 0.2em;
 }

+ 2 - 2
front/src/less/rule.less

@@ -290,8 +290,8 @@
 
 .smallPreview {
     display: block;
-    max-height: 4em;
-    max-width: 8em;
+    max-height: 6em;
+    max-width: 16em;
     border: 1px solid #000;
     margin: 1em auto 0.2em;
 }

+ 6 - 0
front/src/views/rule.html

@@ -137,6 +137,12 @@
                         <url-link url="offender" max-length="100"></url-link>
                     </div>
 
+                    <div ng-if="policyName === 'imagesTooLarge'">
+                        <img ng-src="{{offender.url | https}}" class="smallPreview checker"></img>
+                        <div>{{offender.width}}x{{offender.height}}</div>
+                        <url-link url="offender.url" max-length="100"></url-link>
+                    </div>
+
                     <div ng-if="policyName === 'notFound' || policyName === 'emptyRequests' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
                         <url-link url="offender" max-length="100"></url-link>
                     </div>

+ 9 - 0
lib/metadata/policies.js

@@ -914,6 +914,15 @@ var policies = {
         "hasOffenders": true,
         "unit": 'bytes'
     },
+    "imagesTooLarge": {
+        "tool": "redownload",
+        "label": "Oversized images",
+        "message": "<p>This is the number of images with a width >800px on mobile or >1500px on desktop. Try </p><p>Please ignore if the file is used as a sprite.</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect detection.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 5,
+        "isAbnormalThreshold": 10,
+        "hasOffenders": true
+    },
     "gzipCompression": {
         "tool": "redownload",
         "label": "Gzip compression",

+ 1 - 0
lib/metadata/scoreProfileGeneric.json

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

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

@@ -0,0 +1,51 @@
+var debug               = require('debug')('ylt:imageDimensions');
+var Q                   = require('q');
+var sizeOf              = require('image-size');
+
+var ImageDimensions = function() {
+
+    function getDimensions(entry) {
+        var deferred = Q.defer();
+
+        if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
+            // No valid file available
+            deferred.resolve(entry);
+            return deferred.promise;
+        }
+
+        var fileSize = entry.weightCheck.uncompressedSize;
+
+        if (isJPEG(entry) || isPNG(entry)) {
+            try {
+                var dimensions = sizeOf(entry.weightCheck.bodyBuffer);
+                debug('Image dimensions of %s: %sx%s', entry.url, dimensions.width, dimensions.height);
+
+                entry.imageDimensions = {
+                    width: dimensions.width,
+                    height: dimensions.height
+                };
+            } catch(err) {
+                debug('Error while checking image dimensions:');
+                debug(err);
+            }
+        }
+        
+        deferred.resolve(entry);
+
+        return deferred.promise;
+    }
+
+    function isJPEG(entry) {
+        return entry.isImage && entry.contentType === 'image/jpeg';
+    }
+
+    function isPNG(entry) {
+        return entry.isImage && entry.contentType === 'image/png';
+    }
+
+    return {
+        getDimensions: getDimensions
+    };
+};
+
+module.exports = new ImageDimensions();

+ 30 - 2
lib/tools/redownload/redownload.js

@@ -19,6 +19,7 @@ var fileMinifier        = require('./fileMinifier');
 var gzipCompressor      = require('./gzipCompressor');
 var contentTypeChecker  = require('./contentTypeChecker');
 var fontAnalyzer        = require('./fontAnalyzer');
+var imageDimensions     = require('./imageDimensions');
 
 
 var Redownload = function() {
@@ -69,6 +70,8 @@ var Redownload = function() {
 
                 .then(imageOptimizer.optimizeImage)
 
+                .then(imageDimensions.getDimensions)
+
                 .then(fileMinifier.minifyFile)
 
                 .then(gzipCompressor.compressFile)
@@ -142,9 +145,14 @@ var Redownload = function() {
 
 
                 // Image compression
-                offenders.imageOptimization = listImageNotOptimized(results);
+                offenders.imageOptimization = listImagesNotOptimized(results);
                 metrics.imageOptimization = offenders.imageOptimization.totalGain;
 
+                // Image width
+                var isMobile = data.params.options.device === 'phone';
+                offenders.imagesTooLarge = listImagesTooLarge(results, isMobile);
+                metrics.imagesTooLarge = offenders.imagesTooLarge.length;
+
                 // File minification
                 offenders.fileMinification = listFilesNotMinified(results);
                 metrics.fileMinification = offenders.fileMinification.totalGain;
@@ -273,7 +281,7 @@ var Redownload = function() {
     }
 
 
-    function listImageNotOptimized(requests) {
+    function listImagesNotOptimized(requests) {
         var results = {
             totalGain: 0,
             images: []
@@ -305,6 +313,26 @@ var Redownload = function() {
         return results;
     }
 
+    function listImagesTooLarge(requests, isMobile) {
+        var results = [];
+
+        requests.forEach(function(req) {
+            if (req.weightCheck.bodySize > 0 && 
+                req.imageDimensions &&
+                ((isMobile && req.imageDimensions.width > 800) || req.imageDimensions.width > 1500)) {
+
+                results.push({
+                    url: req.url,
+                    weight: req.weightCheck.bodySize,
+                    width: req.imageDimensions.width,
+                    height: req.imageDimensions.height
+                });
+            }
+        });
+
+        return results;
+    }
+
 
     function listFilesNotMinified(requests) {
         var results = {

+ 1 - 0
package.json

@@ -40,6 +40,7 @@
     "ejs": "2.5.7",
     "express": "4.16.2",
     "fontkit": "1.7.7",
+    "image-size": "0.7.1",
     "imagemin": "5.3.1",
     "imagemin-jpegoptim": "5.2.0",
     "imagemin-jpegtran": "5.0.2",

+ 90 - 0
test/core/imageDimensionsTest.js

@@ -0,0 +1,90 @@
+var should = require('chai').should();
+var imageDimensions = require('../../lib/tools/redownload/imageDimensions');
+var fs = require('fs');
+var path = require('path');
+
+describe('imageDimensions', function() {
+    
+    it('should detect png image dimensions', function(done) {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/an-image.png',
+            requestHeaders: {
+                'User-Agent': 'something',
+                Referer: 'http://www.google.fr/',
+                Accept: '*/*',
+                'Accept-Encoding': 'gzip, deflate'
+            },
+            status: 200,
+            isImage: true,
+            type: 'image',
+            contentType: 'image/png',
+            contentLength: 999,
+            weightCheck: {
+                bodyBuffer: fileContent,
+                totalWeight: 999,
+                headersSize: 200,
+                bodySize: 999,
+                isCompressed: false,
+                uncompressedSize: 999
+            }
+        };
+
+        imageDimensions.getDimensions(entry)
+
+        .then(function(newEntry) {
+            newEntry.should.have.a.property('imageDimensions');
+            newEntry.imageDimensions.should.have.a.property('width').that.equals(664);
+            newEntry.imageDimensions.should.have.a.property('height').that.equals(314);
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should detect a jpg image dimensions', function(done) {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/an-image.jpg',
+            requestHeaders: {
+                'User-Agent': 'something',
+                Referer: 'http://www.google.fr/',
+                Accept: '*/*',
+                'Accept-Encoding': 'gzip, deflate'
+            },
+            status: 200,
+            isImage: true,
+            type: 'image',
+            contentType: 'image/jpeg',
+            contentLength: 999,
+            weightCheck: {
+                bodyBuffer: fileContent,
+                totalWeight: 999,
+                headersSize: 200,
+                bodySize: 999,
+                isCompressed: false,
+                uncompressedSize: 999
+            }
+        };
+
+        imageDimensions.getDimensions(entry)
+
+        .then(function(newEntry) {
+            newEntry.should.have.a.property('imageDimensions');
+            newEntry.imageDimensions.should.have.a.property('width').that.equals(285);
+            newEntry.imageDimensions.should.have.a.property('height').that.equals(427);
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+});

+ 8 - 0
test/core/redownloadTest.js

@@ -109,6 +109,11 @@ describe('redownload', function() {
         ];
 
         var data = {
+            params: {
+                options: {
+                    device: 'phone'
+                }
+            },
             toolsResults: {
                 phantomas: {
                     metrics: {
@@ -140,6 +145,9 @@ describe('redownload', function() {
             data.toolsResults.redownload.offenders.imageOptimization.totalGain.should.be.above(0);
             data.toolsResults.redownload.offenders.imageOptimization.images.length.should.equal(2);
 
+            data.toolsResults.redownload.offenders.should.have.a.property('imagesTooLarge');
+            data.toolsResults.redownload.offenders.imagesTooLarge.length.should.equal(0);
+
             data.toolsResults.redownload.offenders.should.have.a.property('gzipCompression');
             data.toolsResults.redownload.offenders.gzipCompression.totalGain.should.be.above(0);
             data.toolsResults.redownload.offenders.gzipCompression.files.length.should.equal(5);