UNPKG

assetgraph-builder-esprima

Version:

Build system for web sites and applications

195 lines (181 loc) 10.9 kB
var childProcess = require('child_process'), async = require('async'), _ = require('lodash'), urlTools = require('urltools'), getFilterInfosAndTargetContentTypeFromQueryString = require('express-processimage/lib/getFilterInfosAndTargetContentTypeFromQueryString'), PngCrush = require('pngcrush'), PngQuantWithHistogram = require('../PngQuantWithHistogram'), PngQuant = require('pngquant'), OptiPng = require('optipng'), JpegTran = require('jpegtran'); module.exports = function (queryObj, options) { options = options || {}; var isAvailableByBinaryName = { jpegtran: true, pngcrush: true, pngquant: true, optipng: true, gm: true }; return function processImages(assetGraph, cb) { async.each(Object.keys(isAvailableByBinaryName), function (binaryName, cb) { if (binaryName === 'jpegtran' || binaryName === 'optipng' || binaryName === 'pngcrush' || binaryName === 'pngquant') { ({jpegtran: JpegTran, optipng: OptiPng, pngcrush: PngCrush, pngquant: PngQuant})[binaryName].getBinaryPath(function (err, binaryPath) { if (err) { assetGraph.emit('warn', new Error('processImages: ' + binaryName + ' not installed. Install it to get smaller ' + (binaryName === 'jpegtran' ? 'jpgs' : 'pngs'))); isAvailableByBinaryName[binaryName] = false; } cb(); }); } else { childProcess.execFile(binaryName, function (err) { if (err && (err.code === 127 || err.code === 'ENOENT')) { if (binaryName !== 'gm' && binaryName !== 'inkscape') { assetGraph.emit('warn', new Error('processImages: ' + binaryName + ' not installed. Install it to get smaller pngs')); } isAvailableByBinaryName[binaryName] = false; } cb(); }); } }, function () { async.eachLimit(assetGraph.findAssets(_.extend({isImage: true, isInline: false}, queryObj)), 10, function (imageAsset, cb) { var filters = [], operationNames = [], targetContentType = imageAsset.contentType, matchQueryString = imageAsset.url.match(/\?([^#]*)/), usedQueryString, leftOverQueryString, dpr = imageAsset.devicePixelRatio || 1, // Svg assets as images might not have a dpr autoLossless = options.autoLossless; if (matchQueryString) { var filterInfosAndTargetContentType = getFilterInfosAndTargetContentTypeFromQueryString(matchQueryString[1], {rootPath: urlTools.fileUrlToFsPath(assetGraph.root), sourceFilePath: urlTools.fileUrlToFsPath(imageAsset.nonInlineAncestor.url)}); // Pick out any device pixel ratio setter var leftOverQueryStringFragments = filterInfosAndTargetContentType.leftOverQueryStringFragments.filter(function (keyValuePair) { if (/^dpr=\d+(?:[\.,]\d+)?$/.test(keyValuePair)) { dpr = keyValuePair.split('=')[1]; return false; } else if (/^auto(?:=|$)/.test(keyValuePair)) { if (keyValuePair === 'auto') { autoLossless = true; } else { var matchValue = keyValuePair.match(/^auto=(.*)$/); if (matchValue) { var value = matchValue[1]; if (/^(?:true|on|yes|1)$/.test(value)) { autoLossless = true; } else if (/^(?:false|off|no|0)$/.test(value)) { autoLossless = false; } else { return true; } } else { return true; } } return false; } else { return true; } }); usedQueryString = filterInfosAndTargetContentType.usedQueryStringFragments.join('-').replace(/[^a-z0-9\.\-=,]/gi, '-'); leftOverQueryString = leftOverQueryStringFragments.join('&'); if (filterInfosAndTargetContentType.filterInfos.length > 0) { Array.prototype.push.apply(filters, filterInfosAndTargetContentType.filterInfos.map(function (filterInfo) { return filterInfo.create(); })); Array.prototype.push.apply(operationNames, filterInfosAndTargetContentType.operationNames); if (filterInfosAndTargetContentType.targetContentType) { targetContentType = filterInfosAndTargetContentType.targetContentType; } } } // Keep track of whether this image had explicit build instructions so we can emit errors if one of the filters fails (as opposed to warnings): var hasNonAutoLosslessFilters = filters.length > 0; // Add automatic filters if the Content-Type is correct, the relevant binary is available, // it's not explicitly turned off for this image via the auto=false parameter, // and the operation hasn't already been specificied explicitly in the query string: if (targetContentType === 'image/png') { if ((options.pngquant || autoLossless) && isAvailableByBinaryName.pngquant && PngQuantWithHistogram.histogramIsAvailable && operationNames.indexOf('pngquant') === -1) { filters.push(new PngQuantWithHistogram()); } if ((options.pngcrush || autoLossless) && isAvailableByBinaryName.pngcrush && operationNames.indexOf('pngcrush') === -1) { filters.push(new PngCrush(['-rem', 'alla'])); } if ((options.optipng || autoLossless) && isAvailableByBinaryName.optipng && operationNames.indexOf('optipng') === -1) { filters.push(new OptiPng()); } } else if (targetContentType === 'image/jpeg' && isAvailableByBinaryName.jpegtran && (options.jpegtran || autoLossless) && operationNames.indexOf('jpegtran') === -1) { filters.push(new JpegTran(['-optimize'])); } if (!isAvailableByBinaryName.gm) { for (var i = 0 ; i < filters.length ; i += 1) { var filter = filters[i]; if (filter.operationName === 'gm') { assetGraph.emit('warn', new Error('processImages: ' + imageAsset.url + '\nPlease install graphicsmagick to apply the following filters: ' + filter.usedQueryStringFragments.join(', '))); filters.splice(i, 1); i -= 1; } } } var newUrl = imageAsset.url.replace(/\/([^\.\/]*)([^\?\/]*)\?[^#]*/, '/$1' + (usedQueryString ? '.' + usedQueryString : '') + '$2' + (leftOverQueryString ? '?' + leftOverQueryString : '')); if (filters.length > 0) { var callbackCalled = false; filters.forEach(function (filter, i) { if (i < filters.length - 1) { filters[i].pipe(filters[i + 1]); } filter.on('error', function (err) { var filterNameOrDescription; if (filter.operationName) { filterNameOrDescription = 'GraphicsMagick ' + filter.operationName; } else { filterNameOrDescription = filter.commandLine || (filter.constructor && filter.constructor.name) || 'unknown operation'; } err.message = imageAsset.urlOrDescription + ': Error executing ' + filterNameOrDescription + ': ' + err.message; assetGraph.emit(hasNonAutoLosslessFilters ? 'error' : 'warn', err); if (!callbackCalled) { callbackCalled = true; cb(); } }); }); var chunks = []; filters[filters.length - 1] .on('data', function (chunk) { chunks.push(chunk); }) .on('end', function () { if (callbackCalled) { // An error occurred in one of the filters return; } callbackCalled = true; var rawSrc = Buffer.concat(chunks); if (targetContentType && targetContentType !== imageAsset.contentType) { var replacementImageAsset = assetGraph.createAsset({ contentType: targetContentType, type: assetGraph.typeByContentType[targetContentType] || 'Image', url: newUrl, rawSrc: rawSrc, devicePixelRatio: dpr }); imageAsset.replaceWith(replacementImageAsset); replacementImageAsset.extension = replacementImageAsset.defaultExtension; } else { imageAsset.rawSrc = rawSrc; imageAsset.url = newUrl; imageAsset.devicePixelRatio = dpr; } cb(); }); filters[0].end(imageAsset.rawSrc); } else { imageAsset.url = newUrl; imageAsset.devicePixelRatio = dpr; cb(); } }, cb); }); }; };