imageOptimizer.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. var debug = require('debug')('ylt:imageOptimizer');
  2. var Q = require('q');
  3. var imagemin = require('imagemin');
  4. var imageminJpegtran = require('imagemin-jpegtran');
  5. var imageminJpegoptim = require('imagemin-jpegoptim');
  6. var imageminOptipng = require('imagemin-optipng');
  7. var imageminSvgo = require('imagemin-svgo');
  8. var ImageOptimizer = function() {
  9. var MAX_JPEG_QUALITY = 85;
  10. var OPTIPNG_COMPRESSION_LEVEL = 1;
  11. function optimizeImage(entry) {
  12. var deferred = Q.defer();
  13. if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
  14. // No valid file available
  15. deferred.resolve(entry);
  16. return deferred.promise;
  17. }
  18. var fileSize = entry.weightCheck.uncompressedSize;
  19. debug('Let\'s try to optimize %s', entry.url);
  20. debug('Current file size is %d', fileSize);
  21. if (isJPEG(entry)) {
  22. debug('File is a JPEG');
  23. // Starting softly with a lossless compression
  24. return compressJpegLosslessly(entry.weightCheck.bodyBuffer)
  25. .then(function(newFile) {
  26. if (!newFile) {
  27. debug('Optimization didn\'t work');
  28. return entry;
  29. }
  30. var newFileSize = newFile.length;
  31. debug('JPEG lossless compression complete for %s', entry.url);
  32. if (gainIsEnough(fileSize, newFileSize)) {
  33. entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
  34. entry.weightCheck.isOptimized = false;
  35. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  36. }
  37. // Now let's compress lossy to MAX_JPEG_QUALITY
  38. return compressJpegLossly(entry.weightCheck.bodyBuffer);
  39. })
  40. .then(function(newFile) {
  41. if (!newFile) {
  42. debug('Optimization didn\'t work');
  43. return entry;
  44. }
  45. var newFileSize = newFile.length;
  46. debug('JPEG lossy compression complete for %s', entry.url);
  47. if (entry.weightCheck.lossless && entry.weightCheck.lossless < newFileSize) {
  48. debug('Lossy compression is not as good as lossless compression. Skipping the lossy.');
  49. } else if (gainIsEnough(fileSize, newFileSize)) {
  50. if (entry.weightCheck.isOptimized !== false || newFileSize < entry.weightCheck.lossless) {
  51. entry.weightCheck.optimized = newFileSize;
  52. }
  53. entry.weightCheck.lossy = newFileSize;
  54. entry.weightCheck.isOptimized = false;
  55. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  56. }
  57. return entry;
  58. })
  59. .fail(function() {
  60. return entry;
  61. });
  62. } else if (isPNG(entry)) {
  63. debug('File is a PNG');
  64. // Starting softly with a lossless compression
  65. return compressPngLosslessly(entry.weightCheck.bodyBuffer)
  66. .then(function(newFile) {
  67. if (!newFile) {
  68. debug('Optimization didn\'t work');
  69. return entry;
  70. }
  71. var newFileSize = newFile.length;
  72. debug('PNG lossless compression complete for %s', entry.url);
  73. debug('Old file size: %d', fileSize);
  74. debug('New file size: %d', newFileSize);
  75. debug('newgainIsEnough: %s', gainIsEnough(fileSize, newFileSize) ? 'true':'false');
  76. if (gainIsEnough(fileSize, newFileSize)) {
  77. entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
  78. entry.weightCheck.isOptimized = false;
  79. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  80. }
  81. return entry;
  82. })
  83. .fail(function() {
  84. return entry;
  85. });
  86. } else if (isSVG(entry)) {
  87. debug('File is an SVG');
  88. // Starting softly with a lossless compression
  89. return compressSvgLosslessly(entry.weightCheck.bodyBuffer)
  90. .then(function(newFile) {
  91. if (!newFile) {
  92. debug('Optimization didn\'t work');
  93. return entry;
  94. }
  95. var newFileSize = newFile.length;
  96. debug('SVG lossless compression complete for %s', entry.url);
  97. if (gainIsEnough(fileSize, newFileSize)) {
  98. entry.weightCheck.bodyAfterOptimization = newFile.toString();
  99. entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
  100. entry.weightCheck.isOptimized = false;
  101. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  102. }
  103. return entry;
  104. })
  105. .fail(function() {
  106. return entry;
  107. });
  108. } else {
  109. debug('File type %s is not an optimizable image', entry.contentType);
  110. deferred.resolve(entry);
  111. }
  112. return deferred.promise;
  113. }
  114. // The gain is estimated of enough value if it's over 2KB or over 20%,
  115. // but it's ignored if is below 100 bytes
  116. function gainIsEnough(oldWeight, newWeight) {
  117. var gain = oldWeight - newWeight;
  118. var ratio = gain / oldWeight;
  119. return (gain > 2048 || (ratio > 0.2 && gain > 100));
  120. }
  121. function isJPEG(entry) {
  122. return entry.isImage && entry.contentType === 'image/jpeg';
  123. }
  124. function isPNG(entry) {
  125. return entry.isImage && entry.contentType === 'image/png';
  126. }
  127. function isSVG(entry) {
  128. return entry.isImage && entry.isSVG;
  129. }
  130. function compressJpegLosslessly(imageBody) {
  131. return imageminLauncher(imageBody, 'jpeg', false);
  132. }
  133. function compressJpegLossly(imageBody) {
  134. return imageminLauncher(imageBody, 'jpeg', true);
  135. }
  136. function compressPngLosslessly(imageBody) {
  137. return imageminLauncher(imageBody, 'png', false);
  138. }
  139. function compressSvgLosslessly(imageBody) {
  140. return imageminLauncher(imageBody, 'svg', false);
  141. }
  142. function imageminLauncher(imageBody, type, lossy) {
  143. var deferred = Q.defer();
  144. var startTime = Date.now();
  145. debug('Starting %s %s optimization', type, lossy ? 'lossy' : 'lossless');
  146. var engine;
  147. if (type === 'jpeg' && !lossy) {
  148. engine = imageminJpegtran({progressive: true});
  149. } else if (type === 'jpeg' && lossy) {
  150. engine = imageminJpegoptim({progressive: true, max: MAX_JPEG_QUALITY});
  151. } else if (type === 'png' && !lossy) {
  152. engine = imageminOptipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
  153. } else if (type === 'svg' && !lossy) {
  154. engine = imageminSvgo({ plugins: [ { removeUselessDefs: false } ] });
  155. } else {
  156. deferred.reject('No optimization engine found for imagemin');
  157. }
  158. imagemin.buffer(imageBody, {plugins: [engine]})
  159. .then(function(file) {
  160. var endTime = Date.now();
  161. debug('Optimization for %s took %d ms', type, endTime - startTime);
  162. deferred.resolve(file);
  163. })
  164. .catch(function(err) {
  165. debug('Optimisation failed:');
  166. debug(err);
  167. deferred.resolve();
  168. });
  169. return deferred.promise;
  170. }
  171. function entryTypeCanBeOptimized(entry) {
  172. return isJPEG(entry) || isPNG(entry) || isSVG(entry);
  173. }
  174. return {
  175. optimizeImage: optimizeImage,
  176. compressJpegLosslessly: compressJpegLosslessly,
  177. compressJpegLossly: compressJpegLossly,
  178. compressPngLosslessly: compressPngLosslessly,
  179. compressSvgLosslessly: compressSvgLosslessly,
  180. gainIsEnough: gainIsEnough,
  181. entryTypeCanBeOptimized: entryTypeCanBeOptimized
  182. };
  183. };
  184. module.exports = new ImageOptimizer();