Skip to content

Commit

Permalink
Expose GIF opts: interFrameMaxError, interPaletteMaxError #3401
Browse files Browse the repository at this point in the history
  • Loading branch information
lovell committed Nov 14, 2022
1 parent a9d692f commit 5740f45
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 1 deletion.
9 changes: 9 additions & 0 deletions docs/api-output.md
Expand Up @@ -334,6 +334,8 @@ The palette of the input image will be re-used if possible.
* `options.colors` **[number][12]** alternative spelling of `options.colours` (optional, default `256`)
* `options.effort` **[number][12]** CPU effort, between 1 (fastest) and 10 (slowest) (optional, default `7`)
* `options.dither` **[number][12]** level of Floyd-Steinberg error diffusion, between 0 (least) and 1 (most) (optional, default `1.0`)
* `options.interFrameMaxError` **[number][12]** maximum inter-frame error for transparency, between 0 (lossless) and 32 (optional, default `0`)
* `options.interPaletteMaxError` **[number][12]** maximum inter-palette error for palette reuse, between 0 and 256 (optional, default `3`)
* `options.loop` **[number][12]** number of animation iterations, use 0 for infinite animation (optional, default `0`)
* `options.delay` **([number][12] | [Array][13]<[number][12]>)?** delay(s) between animation frames (in milliseconds)
* `options.force` **[boolean][10]** force GIF output, otherwise attempt to use input format (optional, default `true`)
Expand Down Expand Up @@ -361,6 +363,13 @@ const out = await sharp('in.gif', { animated: true })
.toBuffer();
```

```javascript
// Lossy file size reduction of animated GIF
await sharp('in.gif', { animated: true })
.gif({ interFrameMaxError: 8 })
.toFile('optim.gif');
```

* Throws **[Error][4]** Invalid options

Returns **Sharp**&#x20;
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.md
Expand Up @@ -6,6 +6,9 @@ Requires libvips v8.13.3

### v0.31.3 - TBD

* Expose `interFrameMaxError` and `interPaletteMaxError` GIF optimisation properties.
[#3401](https://github.com/lovell/sharp/issues/3401)

* Prevent possible race condition awaiting metadata of Stream-based input.
[#3451](https://github.com/lovell/sharp/issues/3451)

Expand Down
2 changes: 1 addition & 1 deletion docs/search-index.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lib/constructor.js
Expand Up @@ -289,6 +289,8 @@ const Sharp = function (input, options) {
gifBitdepth: 8,
gifEffort: 7,
gifDither: 1,
gifInterFrameMaxError: 0,
gifInterPaletteMaxError: 3,
gifReoptimise: false,
tiffQuality: 80,
tiffCompression: 'jpeg',
Expand Down
22 changes: 22 additions & 0 deletions lib/output.js
Expand Up @@ -551,13 +551,21 @@ function webp (options) {
* .gif({ dither: 0 })
* .toBuffer();
*
* @example
* // Lossy file size reduction of animated GIF
* await sharp('in.gif', { animated: true })
* .gif({ interFrameMaxError: 8 })
* .toFile('optim.gif');
*
* @param {Object} [options] - output options
* @param {boolean} [options.reoptimise=false] - always generate new palettes (slow), re-use existing by default
* @param {boolean} [options.reoptimize=false] - alternative spelling of `options.reoptimise`
* @param {number} [options.colours=256] - maximum number of palette entries, including transparency, between 2 and 256
* @param {number} [options.colors=256] - alternative spelling of `options.colours`
* @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest)
* @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, between 0 (least) and 1 (most)
* @param {number} [options.interFrameMaxError=0] - maximum inter-frame error for transparency, between 0 (lossless) and 32
* @param {number} [options.interPaletteMaxError=3] - maximum inter-palette error for palette reuse, between 0 and 256
* @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation
* @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds)
* @param {boolean} [options.force=true] - force GIF output, otherwise attempt to use input format
Expand Down Expand Up @@ -593,6 +601,20 @@ function gif (options) {
throw is.invalidParameterError('dither', 'number between 0.0 and 1.0', options.dither);
}
}
if (is.defined(options.interFrameMaxError)) {
if (is.number(options.interFrameMaxError) && is.inRange(options.interFrameMaxError, 0, 32)) {
this.options.gifInterFrameMaxError = options.interFrameMaxError;
} else {
throw is.invalidParameterError('interFrameMaxError', 'number between 0.0 and 32.0', options.interFrameMaxError);
}
}
if (is.defined(options.interPaletteMaxError)) {
if (is.number(options.interPaletteMaxError) && is.inRange(options.interPaletteMaxError, 0, 256)) {
this.options.gifInterPaletteMaxError = options.interPaletteMaxError;
} else {
throw is.invalidParameterError('interPaletteMaxError', 'number between 0.0 and 256.0', options.interPaletteMaxError);
}
}
}
trySetAnimationOptions(options, this.options);
return this._updateFormatOut('gif', options);
Expand Down
4 changes: 4 additions & 0 deletions src/pipeline.cc
Expand Up @@ -869,6 +869,8 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("bitdepth", baton->gifBitdepth)
->set("effort", baton->gifEffort)
->set("reoptimise", baton->gifReoptimise)
->set("interframe_maxerror", baton->gifInterFrameMaxError)
->set("interpalette_maxerror", baton->gifInterPaletteMaxError)
->set("dither", baton->gifDither)));
baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length;
Expand Down Expand Up @@ -1549,6 +1551,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
baton->gifInterFrameMaxError = sharp::AttrAsDouble(options, "gifInterFrameMaxError");
baton->gifInterPaletteMaxError = sharp::AttrAsDouble(options, "gifInterPaletteMaxError");
baton->gifReoptimise = sharp::AttrAsBool(options, "gifReoptimise");
baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
Expand Down
4 changes: 4 additions & 0 deletions src/pipeline.h
Expand Up @@ -163,6 +163,8 @@ struct PipelineBaton {
int gifBitdepth;
int gifEffort;
double gifDither;
double gifInterFrameMaxError;
double gifInterPaletteMaxError;
bool gifReoptimise;
int tiffQuality;
VipsForeignTiffCompression tiffCompression;
Expand Down Expand Up @@ -314,6 +316,8 @@ struct PipelineBaton {
gifBitdepth(8),
gifEffort(7),
gifDither(1.0),
gifInterFrameMaxError(0.0),
gifInterPaletteMaxError(3.0),
gifReoptimise(false),
tiffQuality(80),
tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG),
Expand Down
36 changes: 36 additions & 0 deletions test/unit/gif.js
Expand Up @@ -141,6 +141,28 @@ describe('GIF input', () => {
});
});

it('invalid interFrameMaxError throws', () => {
assert.throws(
() => sharp().gif({ interFrameMaxError: 33 }),
/Expected number between 0.0 and 32.0 for interFrameMaxError but received 33 of type number/
);
assert.throws(
() => sharp().gif({ interFrameMaxError: 'fail' }),
/Expected number between 0.0 and 32.0 for interFrameMaxError but received fail of type string/
);
});

it('invalid interPaletteMaxError throws', () => {
assert.throws(
() => sharp().gif({ interPaletteMaxError: 257 }),
/Expected number between 0.0 and 256.0 for interPaletteMaxError but received 257 of type number/
);
assert.throws(
() => sharp().gif({ interPaletteMaxError: 'fail' }),
/Expected number between 0.0 and 256.0 for interPaletteMaxError but received fail of type string/
);
});

it('should work with streams when only animated is set', function (done) {
fs.createReadStream(fixtures.inputGifAnimated)
.pipe(sharp({ animated: true }))
Expand All @@ -164,4 +186,18 @@ describe('GIF input', () => {
fixtures.assertSimilar(fixtures.inputGifAnimated, data, done);
});
});

it('should optimise file size via interFrameMaxError', async () => {
const input = sharp(fixtures.inputGifAnimated, { animated: true });
const before = await input.gif({ interFrameMaxError: 0 }).toBuffer();
const after = await input.gif({ interFrameMaxError: 10 }).toBuffer();
assert.strict(before.length > after.length);
});

it('should optimise file size via interPaletteMaxError', async () => {
const input = sharp(fixtures.inputGifAnimated, { animated: true });
const before = await input.gif({ interPaletteMaxError: 0 }).toBuffer();
const after = await input.gif({ interPaletteMaxError: 100 }).toBuffer();
assert.strict(before.length > after.length);
});
});

0 comments on commit 5740f45

Please sign in to comment.