Skip to content

Commit

Permalink
Expose PNG output options requiring libimagequant #1484
Browse files Browse the repository at this point in the history
  • Loading branch information
lovell committed Jan 13, 2019
1 parent bd37743 commit 9879744
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 74 deletions.
5 changes: 5 additions & 0 deletions docs/api-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ Indexed PNG input at 1, 2 or 4 bits per pixel is converted to 8 bits per pixel.
- `options.progressive` **[Boolean][6]** use progressive (interlace) scan (optional, default `false`)
- `options.compressionLevel` **[Number][8]** zlib compression level, 0-9 (optional, default `9`)
- `options.adaptiveFiltering` **[Boolean][6]** use adaptive row filtering (optional, default `false`)
- `options.palette` **[Boolean][6]** quantise to a palette-based image with alpha transparency support, requires libimagequant (optional, default `false`)
- `options.quality` **[Number][8]** use the lowest number of colours needed to achieve given quality, requires libimagequant (optional, default `100`)
- `options.colours` **[Number][8]** maximum number of palette entries, requires libimagequant (optional, default `256`)
- `options.colors` **[Number][8]** alternative spelling of `options.colours`, requires libimagequant (optional, default `256`)
- `options.dither` **[Number][8]** level of Floyd-Steinberg error diffusion, requires libimagequant (optional, default `1.0`)
- `options.force` **[Boolean][6]** force PNG output, otherwise attempt to use input format (optional, default `true`)

### Examples
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Requires libvips v8.7.0.
* Expose `pages` and `pageHeight` metadata for multi-page input images.
[#1205](https://github.com/lovell/sharp/issues/1205)

* Expose PNG output options requiring libimagequant.
[#1484](https://github.com/lovell/sharp/issues/1484)

* Expose underlying error message for invalid input.
[#1505](https://github.com/lovell/sharp/issues/1505)

Expand Down
4 changes: 4 additions & 0 deletions lib/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ const Sharp = function (input, options) {
pngProgressive: false,
pngCompressionLevel: 9,
pngAdaptiveFiltering: false,
pngPalette: false,
pngQuality: 100,
pngColours: 256,
pngDither: 1,
webpQuality: 80,
webpAlphaQuality: 100,
webpLossless: false,
Expand Down
32 changes: 32 additions & 0 deletions lib/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ function jpeg (options) {
* @param {Boolean} [options.progressive=false] - use progressive (interlace) scan
* @param {Number} [options.compressionLevel=9] - zlib compression level, 0-9
* @param {Boolean} [options.adaptiveFiltering=false] - use adaptive row filtering
* @param {Boolean} [options.palette=false] - quantise to a palette-based image with alpha transparency support, requires libimagequant
* @param {Number} [options.quality=100] - use the lowest number of colours needed to achieve given quality, requires libimagequant
* @param {Number} [options.colours=256] - maximum number of palette entries, requires libimagequant
* @param {Number} [options.colors=256] - alternative spelling of `options.colours`, requires libimagequant
* @param {Number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, requires libimagequant
* @param {Boolean} [options.force=true] - force PNG output, otherwise attempt to use input format
* @returns {Sharp}
* @throws {Error} Invalid options
Expand All @@ -240,6 +245,33 @@ function png (options) {
if (is.defined(options.adaptiveFiltering)) {
this._setBooleanOption('pngAdaptiveFiltering', options.adaptiveFiltering);
}
if (is.defined(options.palette)) {
this._setBooleanOption('pngPalette', options.palette);
if (this.options.pngPalette) {
if (is.defined(options.quality)) {
if (is.integer(options.quality) && is.inRange(options.quality, 0, 100)) {
this.options.pngQuality = options.quality;
} else {
throw is.invalidParameterError('quality', 'integer between 0 and 100', options.quality);
}
}
const colours = options.colours || options.colors;
if (is.defined(colours)) {
if (is.integer(colours) && is.inRange(colours, 2, 256)) {
this.options.pngColours = colours;
} else {
throw is.invalidParameterError('colours', 'integer between 2 and 256', colours);
}
}
if (is.defined(options.dither)) {
if (is.number(options.dither) && is.inRange(options.dither, 0, 1)) {
this.options.pngDither = options.dither;
} else {
throw is.invalidParameterError('dither', 'number between 0.0 and 1.0', options.dither);
}
}
}
}
}
return this._updateFormatOut('png', options);
}
Expand Down
16 changes: 14 additions & 2 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,11 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("strip", !baton->withMetadata)
->set("interlace", baton->pngProgressive)
->set("compression", baton->pngCompressionLevel)
->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)));
->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
->set("palette", baton->pngPalette)
->set("Q", baton->pngQuality)
->set("colours", baton->pngColours)
->set("dither", baton->pngDither)));
baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length;
area->free_fn = nullptr;
Expand Down Expand Up @@ -849,7 +853,11 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("strip", !baton->withMetadata)
->set("interlace", baton->pngProgressive)
->set("compression", baton->pngCompressionLevel)
->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE));
->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
->set("palette", baton->pngPalette)
->set("Q", baton->pngQuality)
->set("colours", baton->pngColours)
->set("dither", baton->pngDither));
baton->formatOut = "png";
} else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) ||
(willMatchInput && inputImageType == ImageType::WEBP)) {
Expand Down Expand Up @@ -1284,6 +1292,10 @@ NAN_METHOD(pipeline) {
baton->pngProgressive = AttrTo<bool>(options, "pngProgressive");
baton->pngCompressionLevel = AttrTo<uint32_t>(options, "pngCompressionLevel");
baton->pngAdaptiveFiltering = AttrTo<bool>(options, "pngAdaptiveFiltering");
baton->pngPalette = AttrTo<bool>(options, "pngPalette");
baton->pngQuality = AttrTo<uint32_t>(options, "pngQuality");
baton->pngColours = AttrTo<uint32_t>(options, "pngColours");
baton->pngDither = AttrTo<double>(options, "pngDither");
baton->webpQuality = AttrTo<uint32_t>(options, "webpQuality");
baton->webpAlphaQuality = AttrTo<uint32_t>(options, "webpAlphaQuality");
baton->webpLossless = AttrTo<bool>(options, "webpLossless");
Expand Down
8 changes: 8 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ struct PipelineBaton {
bool pngProgressive;
int pngCompressionLevel;
bool pngAdaptiveFiltering;
bool pngPalette;
int pngQuality;
int pngColours;
double pngDither;
int webpQuality;
int webpAlphaQuality;
bool webpNearLossless;
Expand Down Expand Up @@ -216,6 +220,10 @@ struct PipelineBaton {
pngProgressive(false),
pngCompressionLevel(9),
pngAdaptiveFiltering(false),
pngPalette(false),
pngQuality(100),
pngColours(256),
pngDither(1.0),
webpQuality(80),
tiffQuality(80),
tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG),
Expand Down
72 changes: 0 additions & 72 deletions test/unit/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,78 +628,6 @@ describe('Input/output', function () {
});
});

describe('PNG output', function () {
it('compression level is valid', function () {
assert.doesNotThrow(function () {
sharp().png({ compressionLevel: 0 });
});
});

it('compression level is invalid', function () {
assert.throws(function () {
sharp().png({ compressionLevel: -1 });
});
});

it('default compressionLevel generates smaller file than compressionLevel=6', function (done) {
// First generate with default compressionLevel
sharp(fixtures.inputPng)
.resize(320, 240)
.png()
.toBuffer(function (err, defaultData, defaultInfo) {
if (err) throw err;
assert.strictEqual(true, defaultData.length > 0);
assert.strictEqual('png', defaultInfo.format);
// Then generate with compressionLevel=6
sharp(fixtures.inputPng)
.resize(320, 240)
.png({ compressionLevel: 6 })
.toBuffer(function (err, largerData, largerInfo) {
if (err) throw err;
assert.strictEqual(true, largerData.length > 0);
assert.strictEqual('png', largerInfo.format);
assert.strictEqual(true, defaultData.length < largerData.length);
done();
});
});
});

it('without adaptiveFiltering generates smaller file', function (done) {
// First generate with adaptive filtering
sharp(fixtures.inputPng)
.resize(320, 240)
.png({ adaptiveFiltering: true })
.toBuffer(function (err, adaptiveData, adaptiveInfo) {
if (err) throw err;
assert.strictEqual(true, adaptiveData.length > 0);
assert.strictEqual(adaptiveData.length, adaptiveInfo.size);
assert.strictEqual('png', adaptiveInfo.format);
assert.strictEqual(320, adaptiveInfo.width);
assert.strictEqual(240, adaptiveInfo.height);
// Then generate without
sharp(fixtures.inputPng)
.resize(320, 240)
.png({ adaptiveFiltering: false })
.toBuffer(function (err, withoutAdaptiveData, withoutAdaptiveInfo) {
if (err) throw err;
assert.strictEqual(true, withoutAdaptiveData.length > 0);
assert.strictEqual(withoutAdaptiveData.length, withoutAdaptiveInfo.size);
assert.strictEqual('png', withoutAdaptiveInfo.format);
assert.strictEqual(320, withoutAdaptiveInfo.width);
assert.strictEqual(240, withoutAdaptiveInfo.height);
assert.strictEqual(true, withoutAdaptiveData.length < adaptiveData.length);
done();
});
});
});

it('Invalid PNG adaptiveFiltering value throws error', function () {
assert.throws(function () {
sharp().png({ adaptiveFiltering: 1 });
});
});
});

it('Without chroma subsampling generates larger file', function (done) {
// First generate with chroma subsampling (default)
sharp(fixtures.inputJpg)
Expand Down
145 changes: 145 additions & 0 deletions test/unit/png.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
'use strict';

const fs = require('fs');
const assert = require('assert');

const sharp = require('../../');
const fixtures = require('../fixtures');

describe('PNG output', function () {
it('compression level is valid', function () {
assert.doesNotThrow(function () {
sharp().png({ compressionLevel: 0 });
});
});

it('compression level is invalid', function () {
assert.throws(function () {
sharp().png({ compressionLevel: -1 });
});
});

it('default compressionLevel generates smaller file than compressionLevel=6', function (done) {
// First generate with default compressionLevel
sharp(fixtures.inputPng)
.resize(320, 240)
.png()
.toBuffer(function (err, defaultData, defaultInfo) {
if (err) throw err;
assert.strictEqual(true, defaultData.length > 0);
assert.strictEqual('png', defaultInfo.format);
// Then generate with compressionLevel=6
sharp(fixtures.inputPng)
.resize(320, 240)
.png({ compressionLevel: 6 })
.toBuffer(function (err, largerData, largerInfo) {
if (err) throw err;
assert.strictEqual(true, largerData.length > 0);
assert.strictEqual('png', largerInfo.format);
assert.strictEqual(true, defaultData.length < largerData.length);
done();
});
});
});

it('without adaptiveFiltering generates smaller file', function (done) {
// First generate with adaptive filtering
sharp(fixtures.inputPng)
.resize(320, 240)
.png({ adaptiveFiltering: true })
.toBuffer(function (err, adaptiveData, adaptiveInfo) {
if (err) throw err;
assert.strictEqual(true, adaptiveData.length > 0);
assert.strictEqual(adaptiveData.length, adaptiveInfo.size);
assert.strictEqual('png', adaptiveInfo.format);
assert.strictEqual(320, adaptiveInfo.width);
assert.strictEqual(240, adaptiveInfo.height);
// Then generate without
sharp(fixtures.inputPng)
.resize(320, 240)
.png({ adaptiveFiltering: false })
.toBuffer(function (err, withoutAdaptiveData, withoutAdaptiveInfo) {
if (err) throw err;
assert.strictEqual(true, withoutAdaptiveData.length > 0);
assert.strictEqual(withoutAdaptiveData.length, withoutAdaptiveInfo.size);
assert.strictEqual('png', withoutAdaptiveInfo.format);
assert.strictEqual(320, withoutAdaptiveInfo.width);
assert.strictEqual(240, withoutAdaptiveInfo.height);
assert.strictEqual(true, withoutAdaptiveData.length < adaptiveData.length);
done();
});
});
});

it('Invalid PNG adaptiveFiltering value throws error', function () {
assert.throws(function () {
sharp().png({ adaptiveFiltering: 1 });
});
});

it('Valid PNG libimagequant palette value does not throw error', function () {
assert.doesNotThrow(function () {
sharp().png({ palette: false });
});
});

it('Invalid PNG libimagequant palette value throws error', function () {
assert.throws(function () {
sharp().png({ palette: 'fail' });
});
});

it('Valid PNG libimagequant quality value produces image of same size or smaller', function () {
const inputPngBuffer = fs.readFileSync(fixtures.inputPng);
return Promise.all([
sharp(inputPngBuffer).resize(10).png({ palette: true, quality: 80 }).toBuffer(),
sharp(inputPngBuffer).resize(10).png({ palette: true, quality: 100 }).toBuffer()
]).then(function (data) {
assert.strictEqual(true, data[0].length <= data[1].length);
});
});

it('Invalid PNG libimagequant quality value throws error', function () {
assert.throws(function () {
sharp().png({ palette: true, quality: 101 });
});
});

it('Valid PNG libimagequant colours value produces image of same size or smaller', function () {
const inputPngBuffer = fs.readFileSync(fixtures.inputPng);
return Promise.all([
sharp(inputPngBuffer).resize(10).png({ palette: true, colours: 100 }).toBuffer(),
sharp(inputPngBuffer).resize(10).png({ palette: true, colours: 200 }).toBuffer()
]).then(function (data) {
assert.strictEqual(true, data[0].length <= data[1].length);
});
});

it('Invalid PNG libimagequant colours value throws error', function () {
assert.throws(function () {
sharp().png({ palette: true, colours: -1 });
});
});

it('Invalid PNG libimagequant colors value throws error', function () {
assert.throws(function () {
sharp().png({ palette: true, colors: 0.1 });
});
});

it('Valid PNG libimagequant dither value produces image of same size or smaller', function () {
const inputPngBuffer = fs.readFileSync(fixtures.inputPng);
return Promise.all([
sharp(inputPngBuffer).resize(10).png({ palette: true, dither: 0.1 }).toBuffer(),
sharp(inputPngBuffer).resize(10).png({ palette: true, dither: 0.9 }).toBuffer()
]).then(function (data) {
assert.strictEqual(true, data[0].length <= data[1].length);
});
});

it('Invalid PNG libimagequant dither value throws error', function () {
assert.throws(function () {
sharp().png({ palette: true, dither: 'fail' });
});
});
});

0 comments on commit 9879744

Please sign in to comment.