imageOptimizer.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  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.body) {
  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(new Buffer(entry.weightCheck.body, 'binary'))
  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(new Buffer(entry.weightCheck.body, 'binary'));
  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 (gainIsEnough(fileSize, newFileSize)) {
  48. if (entry.weightCheck.isOptimized !== false || newFileSize < entry.weightCheck.lossless) {
  49. entry.weightCheck.optimized = newFileSize;
  50. }
  51. entry.weightCheck.lossy = newFileSize;
  52. entry.weightCheck.isOptimized = false;
  53. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  54. }
  55. return entry;
  56. })
  57. .fail(function() {
  58. return entry;
  59. });
  60. } else if (isPNG(entry)) {
  61. debug('File is a PNG');
  62. // Starting softly with a lossless compression
  63. return compressPngLosslessly(new Buffer(entry.weightCheck.body, 'binary'))
  64. .then(function(newFile) {
  65. if (!newFile) {
  66. debug('Optimization didn\'t work');
  67. return entry;
  68. }
  69. var newFileSize = newFile.length;
  70. debug('PNG lossless compression complete for %s', entry.url);
  71. debug('Old file size: %d', fileSize);
  72. debug('New file size: %d', newFileSize);
  73. debug('newgainIsEnough: %s', gainIsEnough(fileSize, newFileSize) ? 'true':'false');
  74. if (gainIsEnough(fileSize, newFileSize)) {
  75. entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
  76. entry.weightCheck.isOptimized = false;
  77. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  78. }
  79. return entry;
  80. })
  81. .fail(function() {
  82. return entry;
  83. });
  84. } else if (isSVG(entry)) {
  85. debug('File is an SVG');
  86. // Starting softly with a lossless compression
  87. return compressSvgLosslessly(new Buffer(entry.weightCheck.body, 'utf8'))
  88. .then(function(newFile) {
  89. if (!newFile) {
  90. debug('Optimization didn\'t work');
  91. return entry;
  92. }
  93. var newFileSize = newFile.length;
  94. debug('SVG lossless compression complete for %s', entry.url);
  95. if (gainIsEnough(fileSize, newFileSize)) {
  96. entry.weightCheck.bodyAfterOptimization = newFile.toString();
  97. entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
  98. entry.weightCheck.isOptimized = false;
  99. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  100. }
  101. return entry;
  102. })
  103. .fail(function() {
  104. return entry;
  105. });
  106. } else {
  107. debug('File type %s is not an optimizable image', entry.contentType);
  108. deferred.resolve(entry);
  109. }
  110. return deferred.promise;
  111. }
  112. // The gain is estimated of enough value if it's over 2KB or over 20%,
  113. // but it's ignored if is below 100 bytes
  114. function gainIsEnough(oldWeight, newWeight) {
  115. var gain = oldWeight - newWeight;
  116. var ratio = gain / oldWeight;
  117. return (gain > 2048 || (ratio > 0.2 && gain > 100));
  118. }
  119. function isJPEG(entry) {
  120. return entry.isImage && entry.contentType === 'image/jpeg';
  121. }
  122. function isPNG(entry) {
  123. return entry.isImage && entry.contentType === 'image/png';
  124. }
  125. function isSVG(entry) {
  126. return entry.isImage && entry.isSVG;
  127. }
  128. function compressJpegLosslessly(imageBody) {
  129. return imageminLauncher(imageBody, 'jpeg', false);
  130. }
  131. function compressJpegLossly(imageBody) {
  132. return imageminLauncher(imageBody, 'jpeg', true);
  133. }
  134. function compressPngLosslessly(imageBody) {
  135. return imageminLauncher(imageBody, 'png', false);
  136. }
  137. function compressSvgLosslessly(imageBody) {
  138. return imageminLauncher(imageBody, 'svg', false);
  139. }
  140. function imageminLauncher(imageBody, type, lossy) {
  141. var deferred = Q.defer();
  142. var startTime = Date.now();
  143. debug('Starting %s %s optimization', type, lossy ? 'lossy' : 'lossless');
  144. var engine;
  145. if (type === 'jpeg' && !lossy) {
  146. engine = imageminJpegtran({progressive: false});
  147. } else if (type === 'jpeg' && lossy) {
  148. engine = imageminJpegoptim({progressive: false, max: MAX_JPEG_QUALITY});
  149. } else if (type === 'png' && !lossy) {
  150. engine = imageminOptipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
  151. } else if (type === 'svg' && !lossy) {
  152. engine = imageminSvgo({ plugins: [ { removeUselessDefs: false } ] });
  153. } else {
  154. deferred.reject('No optimization engine found for imagemin');
  155. }
  156. imagemin.buffer(imageBody, {use: engine})
  157. .then(function(file) {
  158. var endTime = Date.now();
  159. debug('Optimization for %s took %d ms', type, endTime - startTime);
  160. deferred.resolve(file);
  161. console.log('ET HOP');
  162. })
  163. .catch(function(err) {
  164. deferred.reject(file);
  165. });
  166. return deferred.promise;
  167. }
  168. function entryTypeCanBeOptimized(entry) {
  169. return isJPEG(entry) || isPNG(entry) || isSVG(entry);
  170. }
  171. return {
  172. optimizeImage: optimizeImage,
  173. compressJpegLosslessly: compressJpegLosslessly,
  174. compressJpegLossly: compressJpegLossly,
  175. compressPngLosslessly: compressPngLosslessly,
  176. compressSvgLosslessly: compressSvgLosslessly,
  177. gainIsEnough: gainIsEnough,
  178. entryTypeCanBeOptimized: entryTypeCanBeOptimized
  179. };
  180. };
  181. module.exports = new ImageOptimizer();