Add WebP 'exact' option for control over transparent pixels

This commit is contained in:
Lovell Fuller
2026-01-01 19:19:20 +00:00
parent 1cf4b7f04d
commit 0d872bd13a
9 changed files with 47 additions and 4 deletions

View File

@@ -563,6 +563,7 @@ Use these WebP options for output image.
| [options.delay] | <code>number</code> \| <code>Array.&lt;number&gt;</code> | | delay(s) between animation frames (in milliseconds) |
| [options.minSize] | <code>boolean</code> | <code>false</code> | prevent use of animation key frames to minimise file size (slow) |
| [options.mixed] | <code>boolean</code> | <code>false</code> | allow mixture of lossy and lossless animation frames (slow) |
| [options.exact] | <code>boolean</code> | <code>false</code> | preserve the colour data in transparent pixels |
| [options.force] | <code>boolean</code> | <code>true</code> | force WebP output, otherwise attempt to use input format |
**Example**

View File

@@ -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.

View File

@@ -350,6 +350,7 @@ const Sharp = function (input, options) {
webpEffort: 4,
webpMinSize: false,
webpMixed: false,
webpExact: false,
gifBitdepth: 8,
gifEffort: 7,
gifDither: 1,

6
lib/index.d.ts vendored
View File

@@ -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 {

View File

@@ -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);

View File

@@ -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<char*>(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");

View File

@@ -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),

View File

@@ -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' });

View File

@@ -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 });