Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce external dependencies #633

Merged
merged 19 commits into from
Mar 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
27b3821
Remove external dependencies on jpegtran, optipng, pngcrush and pngqu…
Munter Oct 18, 2018
9682f89
Remove unused global package dependencies
Munter Oct 19, 2018
54ce841
Update historgram to a version that doesn't depend on cairo
Munter Oct 20, 2018
e5e5ee3
Move historgram and assetgraph-sprite to direct depednencies and remo…
Munter Oct 20, 2018
970ac93
Update to assetgraph-sprite 3.0.1
Munter Oct 21, 2018
94201c4
Move assetgraph-sprite and historgram to direct dependencies
Munter Oct 21, 2018
e2e4d44
Updated OSX external dependency installation instructions
Munter Dec 1, 2018
2bb6413
Update express-processimage to 8.0.2 to avoid 'os.tmpDir' deprecation…
Munter Dec 1, 2018
d15158f
Replace bluebird with native promises and util.promsify. Use p-map wh…
Munter Dec 1, 2018
f53546e
Revert "Update express-processimage to 8.0.2 to avoid 'os.tmpDir' dep…
Munter Dec 1, 2018
7bc0769
Converted PngQuantWithHistogram to use class extension, added an empt…
Munter Dec 2, 2018
6f45ab4
Linting
Munter Dec 2, 2018
3742048
Update to express-processimage 8.0.2 to avoid os.tmpDir deprecation w…
Munter Dec 3, 2018
0da7a81
Update bin/buildProduction with updated imports
Munter Mar 5, 2019
b6aa34a
Desperate debugging logging
Munter Mar 5, 2019
70311aa
pngcrush -noreduce in autolossless mode and in the test cases
papandreou Mar 5, 2019
1213c80
PngQuantWithHistogram: Add missing writable flag
papandreou Mar 5, 2019
aff5d11
PngQuantWithHistogram: Fix emission of events
papandreou Mar 5, 2019
0c080b2
Merge branch 'master' into slim-dependencies
papandreou Mar 6, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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