imageOptimizer.js 8.2 KB

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