diff --git a/lib/tools/weightChecker/imageOptimizer.js b/lib/tools/weightChecker/imageOptimizer.js index cf968df..542ee87 100644 --- a/lib/tools/weightChecker/imageOptimizer.js +++ b/lib/tools/weightChecker/imageOptimizer.js @@ -54,6 +54,27 @@ var ImageOptimizer = function() { return entry; }); + } else if (isPNG(entry)) { + + debug('File is a PNG'); + + // Starting softly with a lossless compression + return compressPngLosslessly(entry.weightCheck.body) + + .then(function(newFile) { + var newFileSize = newFile.contents.length; + + debug('PNG lossless compression complete for %s', entry.url); + + if (newFileSize < fileSize) { + entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize; + entry.weightCheck.isOptimized = false; + debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize)); + } + + return entry; + }); + } else { debug('File type is not an optimizable image'); deferred.resolve(entry); @@ -92,7 +113,7 @@ var ImageOptimizer = function() { var deferred = Q.defer(); var startTime = Date.now(); - debug('Starting JPEG lossly compression'); + debug('Starting JPEG lossy compression'); new Imagemin() .src(imageBody) @@ -110,11 +131,38 @@ var ImageOptimizer = function() { return deferred.promise; } + function isPNG(entry) { + return entry.isImage && entry.contentType === 'image/png'; + } + + function compressPngLosslessly(imageBody) { + var deferred = Q.defer(); + var startTime = Date.now(); + + debug('Starting PNG losslessly compression'); + + new Imagemin() + .src(imageBody) + .use(Imagemin.optipng({optimizationLevel: 2})) + .run(function (err, files) { + if (err) { + deferred.reject(err); + } else { + deferred.resolve(files[0]); + var endTime = Date.now(); + debug('compressPngLosslessly took %d ms', endTime - startTime); + } + }); + + return deferred.promise; + } + return { //recompressIfImage: recompressIfImage, optimizeImage: optimizeImage, compressJpegLosslessly: compressJpegLosslessly, - compressJpegLossly: compressJpegLossly + compressJpegLossly: compressJpegLossly, + compressPngLosslessly: compressPngLosslessly }; }; diff --git a/test/core/imageOptimizerTest.js b/test/core/imageOptimizerTest.js index d170334..ca05d40 100644 --- a/test/core/imageOptimizerTest.js +++ b/test/core/imageOptimizerTest.js @@ -76,4 +76,154 @@ describe('imageOptimizer', function() { }); }); + it('should optimize a PNG image losslessly', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../fixtures/png-image.png')); + + var fileSize = fileContent.length; + + imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) { + var newFileSize = newFile.contents.length; + newFileSize.should.be.below(fileSize); + done(); + }).fail(function(err) { + done(err); + }); + }); + + it('should optimize a png', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../fixtures/png-image.png')); + var fileSize = fileContent.length; + + 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: { + body: fileContent, + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize + } + }; + + imageOptimizer.optimizeImage(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false); + newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + + + + + + + + + it('shouldn\'t fail optimizing a corrupted jpeg', function(done) { + + // In this test, we try to optimize a PNG but with a falsy "image/jpeg" content type + + var fileContent = fs.readFileSync(path.resolve(__dirname, '../fixtures/png-image.png')); + var fileSize = fileContent.length; + + 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/jpeg', + contentLength: 999, + weightCheck: { + body: fileContent, + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize + } + }; + + imageOptimizer.optimizeImage(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.not.have.a.property('isOptimized'); + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('shouldn\'t fail optimizing a corrupted png', function(done) { + + // In this test, we try to optimize a JPEG but with a falsy "image/png" content type + + var fileContent = fs.readFileSync(path.resolve(__dirname, '../fixtures/jpeg-image.jpg')); + var fileSize = fileContent.length; + + 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/png', + contentLength: 999, + weightCheck: { + body: fileContent, + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize + } + }; + + imageOptimizer.optimizeImage(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.not.have.a.property('isOptimized'); + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + }); diff --git a/test/fixtures/png-image.png b/test/fixtures/png-image.png new file mode 100644 index 0000000..e1d92f7 Binary files /dev/null and b/test/fixtures/png-image.png differ