Skip to content

Commit

Permalink
Merge pull request #633 from assetgraph/slim-dependencies
Browse files Browse the repository at this point in the history
Reduce external dependencies
  • Loading branch information
Munter authored Mar 7, 2019
2 parents fb7f35d + 0c080b2 commit 4cb79dc
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 180 deletions.
6 changes: 2 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ node_js:
- "8"
- "10"
- "node"
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
before_install: "sudo apt-get update && sudo apt-get install -y libcairo2-dev libjpeg8-dev libgif-dev optipng pngcrush pngquant libpango1.0-dev graphicsmagick libjpeg-turbo-progs inkscape gifsicle"
- graphicsmagick
- inkscape
script: "npm run ci"
after_success: "<coverage/lcov.info ./node_modules/coveralls/bin/coveralls.js"

Expand Down
9 changes: 0 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends \
graphicsmagick \
inkscape \
libcairo2-dev \
libgif-dev \
libgsf-1-dev \
libjpeg-progs \
libpango1.0-dev \
libvips-dev \
optipng \
pngcrush \
pngquant \
&& rm -rf /var/lib/apt/lists/*

ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,21 @@ and optimization features, you need several libraries and command line
utilities installed. On Ubuntu you can grab them all by running:

```
sudo apt-get install -y libcairo2-dev libjpeg8-dev libgif-dev optipng pngcrush pngquant libpango1.0-dev graphicsmagick libjpeg-progs inkscape
sudo apt-get install -y graphicsmagick inkscape
```

Or on OS X, with [homebrew](http://brew.sh/):

```
brew install cairo jpeg giflib optipng pngcrush pngquant pango graphicsmagick jpeg-turbo homebrew/gui/inkscape
brew install homebrew/science/vips --with-webp --with-graphicsmagick
```sh
# Image manipulation
brew install graphicsmagick
brew install vips --with-webp --with-graphicsmagick

# SVG rengering with Inkscape
brew cask install xquartz
brew cask install inkscape

# Export PKG_CONFIG_PATH to make Inkscape work. Put this in your .profile or .bashrc
export PKG_CONFIG_PATH=/opt/X11/lib/pkgconfig
```

Expand Down
140 changes: 66 additions & 74 deletions lib/PngQuantWithHistogram.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,88 @@
/* global setImmediate:true */
// node 0.8 compat
if (typeof setImmediate === 'undefined') {
setImmediate = process.nextTick;
}

const Stream = require('stream');
const util = require('util');
const PngQuant = require('pngquant');
let histogram;
const histogram = require('histogram');

try {
histogram = require('histogram');
} catch (err) {}
class PngQuantWithHistogram extends Stream {
constructor() {
super();

function PngQuantWithHistogram() {
// Fall back to quanting to 256 colors. If you don't want this behavior, check PngQuantWithHistogram.histogramAvailable
if (!histogram) {
return new PngQuant([256]);
this.writable = true;
this.bufferedChunks = [];
}

Stream.call(this);
this.bufferedChunks = [];
}

PngQuantWithHistogram.histogramAvailable = !!histogram;
write(chunk) {
this.bufferedChunks.push(chunk);
}

util.inherits(PngQuantWithHistogram, Stream);
end(chunk) {
if (chunk) {
this.write(chunk);
}
var rawSrc = Buffer.concat(this.bufferedChunks);

PngQuantWithHistogram.prototype.write = function(chunk) {
this.bufferedChunks.push(chunk);
};
if (rawSrc.length === 0) {
const err = new Error('PngQuantWithHistogram stream ended with no data');

PngQuantWithHistogram.prototype.end = function(chunk) {
if (chunk) {
this.write(chunk);
}
var rawSrc = Buffer.concat(this.bufferedChunks);
this.bufferedChunks = null;
histogram(rawSrc, (err, data) => {
if (err) {
err.message = 'histogram: ' + err.message;
return this.emit('error', err);
}
if (data.colors.rgba < 256) {
// The image has fewer than 256 colors, run rawSrc through a PngQuant filter and proxy its events:
this.pngQuant = new PngQuant([
data.colors.rgba < 2 ? 2 : data.colors.rgba
]);
this.__defineGetter__('commandLine', () => this.pngQuant.commandLine);
for (const eventName of ['data', 'end', 'error']) {
this.pngQuant.on(eventName, () => {
// ...
this.emit.apply(this, eventName.concat(arguments));
});

this.bufferedChunks = null;

histogram(rawSrc, (err, data) => {
if (err) {
err.message = 'histogram: ' + err.message;
return this.emit('error', err);
}
this.pngQuant.end(rawSrc);
} else {
// The image has too many colors. Emit all the buffered chunks at once when we aren't paused:
let hasEnded = false;
const emitRawSrcAndEnd = () => {
hasEnded = true;
this.emit('data', rawSrc);
this.emit('end');
};

if (this.isPaused) {
this.resume = function() {
setImmediate(function() {
if (!this.isPaused && !hasEnded) {
emitRawSrcAndEnd();
}
if (data.colors.rgba < 256) {
// The image has fewer than 256 colors, run rawSrc through a PngQuant filter and proxy its events:
this.pngQuant = new PngQuant([
data.colors.rgba < 2 ? 2 : data.colors.rgba
]);
this.__defineGetter__('commandLine', () => this.pngQuant.commandLine);
for (const eventName of ['data', 'end', 'error']) {
this.pngQuant.on(eventName, (...args) => {
this.emit(eventName, ...args);
});
};
}
this.pngQuant.end(rawSrc);
} else {
emitRawSrcAndEnd();
// The image has too many colors. Emit all the buffered chunks at once when we aren't paused:
let hasEnded = false;
const emitRawSrcAndEnd = () => {
hasEnded = true;
this.emit('data', rawSrc);
this.emit('end');
};

if (this.isPaused) {
this.resume = function() {
setImmediate(function() {
if (!this.isPaused && !hasEnded) {
emitRawSrcAndEnd();
}
});
};
} else {
emitRawSrcAndEnd();
}
}
}
});
};
});
}

PngQuantWithHistogram.prototype.pause = function() {
this.isPaused = true;
if (this.pngQuant) {
this.pngQuant.pause();
pause() {
this.isPaused = true;
if (this.pngQuant) {
this.pngQuant.pause();
}
}
};

PngQuantWithHistogram.prototype.resume = function() {
this.isPaused = false;
if (this.pngQuant) {
this.pngQuant.resume();
resume() {
this.isPaused = false;
if (this.pngQuant) {
this.pngQuant.resume();
}
}
};
}

module.exports = PngQuantWithHistogram;
86 changes: 20 additions & 66 deletions lib/transforms/processImages.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,35 @@
const Promise = require('bluebird');
const { promisify } = require('util');
const promiseMap = require('p-map');
const childProcess = require('child_process');
const _ = require('lodash');
const urlTools = require('urltools');
const PngCrush = require('pngcrush');
const PngQuantWithHistogram = require('../PngQuantWithHistogram');
const PngQuant = require('pngquant');
const OptiPng = require('optipng');
const JpegTran = require('jpegtran');

module.exports = (queryObj, options) => {
options = options || {};
const isAvailableByBinaryName = {
jpegtran: true,
pngcrush: true,
pngquant: true,
optipng: true,
gm: true
};
let isGmAvailable = true;

return async function processImages(assetGraph) {
const getFilterInfosAndTargetContentTypeFromQueryString = require('express-processimage/lib/getFilterInfosAndTargetContentTypeFromQueryString');

await Promise.map(
Object.keys(isAvailableByBinaryName),
async binaryName => {
if (
binaryName === 'jpegtran' ||
binaryName === 'optipng' ||
binaryName === 'pngcrush' ||
binaryName === 'pngquant'
) {
try {
await Promise.fromNode(cb =>
({
jpegtran: JpegTran,
optipng: OptiPng,
pngcrush: PngCrush,
pngquant: PngQuant
}[binaryName].getBinaryPath(cb))
);
} catch (err) {
assetGraph.warn(
new Error(
'processImages: ' +
binaryName +
' not installed. Install it to get smaller ' +
(binaryName === 'jpegtran' ? 'jpgs' : 'pngs')
)
);
isAvailableByBinaryName[binaryName] = false;
}
} else {
try {
await Promise.fromNode(cb => childProcess.execFile(binaryName, cb));
} catch (err) {
if (err.code === 127 || err.code === 'ENOENT') {
if (binaryName !== 'gm' && binaryName !== 'inkscape') {
assetGraph.warn(
new Error(
'processImages: ' +
binaryName +
' not installed. Install it to get smaller pngs'
)
);
}
isAvailableByBinaryName[binaryName] = false;
}
}
}
try {
await promisify(childProcess.execFile)('gm');
} catch (err) {
isGmAvailable = false;

if (err.code === 127 || err.code === 'ENOENT') {
assetGraph.warn(
new Error(
`processImages: Graphicsmagick not installed. Install it to get smaller pngs`
)
);
}
);
}

await Promise.map(
await promiseMap(
assetGraph.findAssets(
_.extend({ isImage: true, isInline: false }, queryObj)
),
Expand Down Expand Up @@ -163,36 +122,31 @@ module.exports = (queryObj, options) => {
if (targetContentType === 'image/png') {
if (
(options.pngquant || autoLossless) &&
isAvailableByBinaryName.pngquant &&
PngQuantWithHistogram.histogramIsAvailable &&
operationNames.indexOf('pngquant') === -1
) {
filters.push(new PngQuantWithHistogram());
}
if (
(options.optipng || autoLossless) &&
isAvailableByBinaryName.optipng &&
operationNames.indexOf('optipng') === -1
) {
filters.push(new OptiPng());
}
if (
(options.pngcrush || autoLossless) &&
isAvailableByBinaryName.pngcrush &&
operationNames.indexOf('pngcrush') === -1
) {
filters.push(new PngCrush(['-rem', 'alla']));
filters.push(new PngCrush(['-rem', 'alla', '-noreduce']));
}
} else if (
targetContentType === 'image/jpeg' &&
isAvailableByBinaryName.jpegtran &&
(options.jpegtran || autoLossless) &&
operationNames.indexOf('jpegtran') === -1
) {
filters.push(new JpegTran(['-optimize']));
}

if (!isAvailableByBinaryName.gm) {
if (isGmAvailable) {
for (let i = 0; i < filters.length; i += 1) {
const filter = filters[i];
if (filter.operationName === 'gm') {
Expand Down Expand Up @@ -221,6 +175,7 @@ module.exports = (queryObj, options) => {
(leftOverQueryString ? '?' + leftOverQueryString : '')
);
if (filters.length > 0) {
console.log('filters', filters.map(f => f.constructor.name));
await new Promise((resolve, reject) => {
for (const [i, filter] of filters.entries()) {
if (i < filters.length - 1) {
Expand All @@ -243,7 +198,6 @@ module.exports = (queryObj, options) => {
filterNameOrDescription +
': ' +
err.message;
console.log(assetGraph.warn, assetGraph.error);
if (hasNonAutoLosslessFilters) {
assetGraph.warn(err);
}
Expand Down
Loading

0 comments on commit 4cb79dc

Please sign in to comment.