diff --git a/docs/src/content/docs/api-output.md b/docs/src/content/docs/api-output.md index d103ae03..b2a25c86 100644 --- a/docs/src/content/docs/api-output.md +++ b/docs/src/content/docs/api-output.md @@ -563,6 +563,7 @@ Use these WebP options for output image. | [options.delay] | number \| Array.<number> | | delay(s) between animation frames (in milliseconds) | | [options.minSize] | boolean | false | prevent use of animation key frames to minimise file size (slow) | | [options.mixed] | boolean | false | allow mixture of lossy and lossless animation frames (slow) | +| [options.exact] | boolean | false | preserve the colour data in transparent pixels | | [options.force] | boolean | true | force WebP output, otherwise attempt to use input format | **Example** diff --git a/docs/src/content/docs/changelog/v0.35.0.md b/docs/src/content/docs/changelog/v0.35.0.md index de234244..84f78a0a 100644 --- a/docs/src/content/docs/changelog/v0.35.0.md +++ b/docs/src/content/docs/changelog/v0.35.0.md @@ -23,3 +23,5 @@ slug: changelog/v0.35.0 * Add `toUint8Array` for output image as a `TypedArray` backed by a transferable `ArrayBuffer`. [#4355](https://github.com/lovell/sharp/issues/4355) + +* Add WebP `exact` option for control over transparent pixel colour values. diff --git a/lib/constructor.js b/lib/constructor.js index 915e02df..ae22dff0 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -350,6 +350,7 @@ const Sharp = function (input, options) { webpEffort: 4, webpMinSize: false, webpMixed: false, + webpExact: false, gifBitdepth: 8, gifEffort: 7, gifDither: 1, diff --git a/lib/index.d.ts b/lib/index.d.ts index 9c4aab52..7c9fa0c5 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1394,11 +1394,13 @@ declare namespace sharp { /** Level of CPU effort to reduce file size, integer 0-6 (optional, default 4) */ effort?: number | undefined; /** Prevent use of animation key frames to minimise file size (slow) (optional, default false) */ - minSize?: boolean; + minSize?: boolean | undefined; /** Allow mixture of lossy and lossless animation frames (slow) (optional, default false) */ - mixed?: boolean; + mixed?: boolean | undefined; /** Preset options: one of default, photo, picture, drawing, icon, text (optional, default 'default') */ preset?: keyof PresetEnum | undefined; + /** Preserve the colour data in transparent pixels (optional, default false) */ + exact?: boolean | undefined; } interface AvifOptions extends OutputOptions { diff --git a/lib/output.js b/lib/output.js index 2f33df86..fbd09bfb 100644 --- a/lib/output.js +++ b/lib/output.js @@ -749,6 +749,7 @@ function png (options) { * @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds) * @param {boolean} [options.minSize=false] - prevent use of animation key frames to minimise file size (slow) * @param {boolean} [options.mixed=false] - allow mixture of lossy and lossless animation frames (slow) + * @param {boolean} [options.exact=false] - preserve the colour data in transparent pixels * @param {boolean} [options.force=true] - force WebP output, otherwise attempt to use input format * @returns {Sharp} * @throws {Error} Invalid options @@ -801,6 +802,9 @@ function webp (options) { if (is.defined(options.mixed)) { this._setBooleanOption('webpMixed', options.mixed); } + if (is.defined(options.exact)) { + this._setBooleanOption('webpExact', options.exact); + } } trySetAnimationOptions(options, this.options); return this._updateFormatOut('webp', options); diff --git a/src/pipeline.cc b/src/pipeline.cc index 66ceb6b5..891494df 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -965,6 +965,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("effort", baton->webpEffort) ->set("min_size", baton->webpMinSize) ->set("mixed", baton->webpMixed) + ->set("exact", baton->webpExact) ->set("alpha_q", baton->webpAlphaQuality))); baton->bufferOut = static_cast(area->data); baton->bufferOutLength = area->length; @@ -1176,6 +1177,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("effort", baton->webpEffort) ->set("min_size", baton->webpMinSize) ->set("mixed", baton->webpMixed) + ->set("exact", baton->webpExact) ->set("alpha_q", baton->webpAlphaQuality)); baton->formatOut = "webp"; } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) || @@ -1486,6 +1488,7 @@ class PipelineWorker : public Napi::AsyncWorker { {"preset", vips_enum_nick(VIPS_TYPE_FOREIGN_WEBP_PRESET, baton->webpPreset)}, {"min_size", baton->webpMinSize ? "true" : "false"}, {"mixed", baton->webpMixed ? "true" : "false"}, + {"exact", baton->webpExact ? "true" : "false"}, {"effort", std::to_string(baton->webpEffort)} }; suffix = AssembleSuffixString(".webp", options); @@ -1760,6 +1763,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort"); baton->webpMinSize = sharp::AttrAsBool(options, "webpMinSize"); baton->webpMixed = sharp::AttrAsBool(options, "webpMixed"); + baton->webpExact = sharp::AttrAsBool(options, "webpExact"); baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth"); baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort"); baton->gifDither = sharp::AttrAsDouble(options, "gifDither"); diff --git a/src/pipeline.h b/src/pipeline.h index c9ff7102..16464d3c 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -168,6 +168,7 @@ struct PipelineBaton { int webpEffort; bool webpMinSize; bool webpMixed; + bool webpExact; int gifBitdepth; int gifEffort; double gifDither; @@ -347,6 +348,7 @@ struct PipelineBaton { webpEffort(4), webpMinSize(false), webpMixed(false), + webpExact(false), gifBitdepth(8), gifEffort(7), gifDither(1.0), diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts index f854e3d5..22165af3 100644 --- a/test/types/sharp.test-d.ts +++ b/test/types/sharp.test-d.ts @@ -548,8 +548,8 @@ sharp('input.tiff').jxl({ decodingTier: 4 }).toFile('out.jxl'); sharp('input.tiff').jxl({ lossless: true }).toFile('out.jxl'); sharp('input.tiff').jxl({ effort: 7 }).toFile('out.jxl'); -// Support `minSize` and `mixed` webp options -sharp('input.tiff').webp({ minSize: true, mixed: true }).toFile('out.gif'); +// Support webp options +sharp('input.tiff').webp({ minSize: true, mixed: true, exact: true }).toFile('out.webp'); // 'failOn' input param sharp('input.tiff', { failOn: 'none' }); diff --git a/test/unit/webp.js b/test/unit/webp.js index 22d37a52..09932327 100644 --- a/test/unit/webp.js +++ b/test/unit/webp.js @@ -213,6 +213,33 @@ describe('WebP', () => { ); }); + it('valid exact', () => { + assert.doesNotThrow(() => sharp().webp({ exact: true })); + }); + + it('invalid exact throws', () => { + assert.throws( + () => sharp().webp({ exact: 'fail' }), + /Expected boolean for webpExact but received fail of type string/ + ); + }); + + it('saving exact pixel colour values produces larger file size', async () => { + const withExact = await + sharp(fixtures.inputPngAlphaPremultiplicationSmall) + .resize(8, 8) + .webp({ exact: true, effort: 0 }) + .toBuffer(); + + const withoutExact = await + sharp(fixtures.inputPngAlphaPremultiplicationSmall) + .resize(8, 8) + .webp({ exact: false, effort: 0 }) + .toBuffer() + + assert.strictEqual(true, withExact.length > withoutExact.length); + }); + it('invalid loop throws', () => { assert.throws(() => { sharp().webp({ loop: -1 });