From 0468c1be9f42cc57f88d2937651ed0f8a7a657f7 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Wed, 7 Jan 2026 20:39:53 +0000 Subject: [PATCH] Encoding lossless AVIF is mutually exclusive with iq tuning --- docs/src/content/docs/api-output.md | 2 +- lib/output.js | 8 ++++++-- test/unit/avif.js | 31 ++++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/docs/src/content/docs/api-output.md b/docs/src/content/docs/api-output.md index 357dada9..b6729c63 100644 --- a/docs/src/content/docs/api-output.md +++ b/docs/src/content/docs/api-output.md @@ -758,7 +758,7 @@ When using Windows ARM64, this feature requires a CPU with ARM64v8.4 or later. | [options.effort] | number | 4 | CPU effort, between 0 (fastest) and 9 (slowest) | | [options.chromaSubsampling] | string | "'4:4:4'" | set to '4:2:0' to use chroma subsampling | | [options.bitdepth] | number | 8 | set bitdepth to 8, 10 or 12 bit | -| [options.tune] | string | "'iq'" | tune output for a quality metric, one of 'iq' (default), 'ssim' or 'psnr' | +| [options.tune] | string | "'iq'" | tune output for a quality metric, one of 'iq' (default), 'ssim' (default when lossless) or 'psnr' | **Example** ```js diff --git a/lib/output.js b/lib/output.js index 7de53ed1..d0e1d3d5 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1175,7 +1175,7 @@ function tiff (options) { * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest) * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling * @param {number} [options.bitdepth=8] - set bitdepth to 8, 10 or 12 bit - * @param {string} [options.tune='iq'] - tune output for a quality metric, one of 'iq' (default), 'ssim' or 'psnr' + * @param {string} [options.tune='iq'] - tune output for a quality metric, one of 'iq' (default), 'ssim' (default when lossless) or 'psnr' * @returns {Sharp} * @throws {Error} Invalid options */ @@ -1255,7 +1255,11 @@ function heif (options) { } if (is.defined(options.tune)) { if (is.string(options.tune) && is.inArray(options.tune, ['iq', 'ssim', 'psnr'])) { - this.options.heifTune = options.tune; + if (this.options.heifLossless && options.tune === 'iq') { + this.options.heifTune = 'ssim'; + } else { + this.options.heifTune = options.tune; + } } else { throw is.invalidParameterError('tune', 'one of: psnr, ssim, iq', options.tune); } diff --git a/test/unit/avif.js b/test/unit/avif.js index 0a47b4e0..aa08b438 100644 --- a/test/unit/avif.js +++ b/test/unit/avif.js @@ -7,7 +7,7 @@ const { describe, it } = require('node:test'); const assert = require('node:assert'); const sharp = require('../../'); -const { inputAvif, inputJpg, inputGifAnimated } = require('../fixtures'); +const { inputAvif, inputJpg, inputGifAnimated, inputPng } = require('../fixtures'); describe('AVIF', () => { it('called without options does not throw an error', () => { @@ -74,6 +74,35 @@ describe('AVIF', () => { }); }); + it('can convert PNG to lossless AVIF', async () => { + const data = await sharp(inputPng) + .resize(32) + .avif({ lossless: true, effort: 0 }) + .toBuffer(); + const { size, ...metadata } = await sharp(data).metadata(); + void size; + assert.deepStrictEqual(metadata, { + autoOrient: { + height: 24, + width: 32 + }, + channels: 3, + compression: 'av1', + depth: 'uchar', + format: 'heif', + hasAlpha: false, + hasProfile: false, + height: 24, + isProgressive: false, + isPalette: false, + bitsPerSample: 8, + pagePrimary: 0, + pages: 1, + space: 'srgb', + width: 32 + }); + }); + it('can passthrough AVIF', async () => { const data = await sharp(inputAvif) .resize(32)