imageOptimizer.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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.lossless = entry.weightCheck.optimized = newFileSize;
  94. entry.weightCheck.isOptimized = false;
  95. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  96. }
  97. return entry;
  98. })
  99. .fail(function() {
  100. return entry;
  101. });
  102. } else {
  103. debug('File type %s is not an (optimizable) image', entry.contentType);
  104. deferred.resolve(entry);
  105. }
  106. return deferred.promise;
  107. }
  108. // The gain is estimated of enough value if it's over 2KB or over 20%,
  109. // but it's ignored if is below 100 bytes
  110. function gainIsEnough(oldWeight, newWeight) {
  111. var gain = oldWeight - newWeight;
  112. var ratio = gain / oldWeight;
  113. return (gain > 2048 || (ratio > 0.2 && gain > 100));
  114. }
  115. function isJpeg(entry) {
  116. return entry.isImage && entry.contentType === 'image/jpeg';
  117. }
  118. function isPNG(entry) {
  119. return entry.isImage && entry.contentType === 'image/png';
  120. }
  121. function isSVG(entry) {
  122. return entry.isImage && entry.contentType === 'image/svg+xml';
  123. }
  124. function compressJpegLosslessly(imageBody) {
  125. return imageminLauncher(imageBody, 'jpeg', false);
  126. }
  127. function compressJpegLossly(imageBody) {
  128. return imageminLauncher(imageBody, 'jpeg', true);
  129. }
  130. function compressPngLosslessly(imageBody) {
  131. return imageminLauncher(imageBody, 'png', false);
  132. }
  133. function compressSvgLosslessly(imageBody) {
  134. return imageminLauncher(imageBody, 'svg', false);
  135. }
  136. function imageminLauncher(imageBody, type, lossy) {
  137. var deferred = Q.defer();
  138. var startTime = Date.now();
  139. debug('Starting %s %s optimization', type, lossy ? 'lossy' : 'lossless');
  140. var engine;
  141. if (type === 'jpeg' && !lossy) {
  142. engine = Imagemin.jpegtran();
  143. } else if (type === 'jpeg' && lossy) {
  144. engine = jpegoptim({max: MAX_JPEG_QUALITY});
  145. } else if (type === 'png' && !lossy) {
  146. engine = Imagemin.optipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
  147. } else if (type === 'svg' && !lossy) {
  148. engine = Imagemin.svgo();
  149. } else {
  150. deferred.reject('No optimization engine found for imagemin');
  151. }
  152. try {
  153. new Imagemin()
  154. .src(imageBody)
  155. .use(engine)
  156. .run(function (err, files) {
  157. if (err) {
  158. deferred.reject(err);
  159. } else {
  160. deferred.resolve(files[0]);
  161. var endTime = Date.now();
  162. debug('Optimization for %s took %d ms', type, endTime - startTime);
  163. }
  164. });
  165. } catch(err) {
  166. deferred.reject(err);
  167. }
  168. return deferred.promise;
  169. }
  170. return {
  171. optimizeImage: optimizeImage,
  172. compressJpegLosslessly: compressJpegLosslessly,
  173. compressJpegLossly: compressJpegLossly,
  174. compressPngLosslessly: compressPngLosslessly,
  175. compressSvgLosslessly: compressSvgLosslessly,
  176. gainIsEnough: gainIsEnough
  177. };
  178. };
  179. module.exports = new ImageOptimizer();