diff --git a/docs/api-output.md b/docs/api-output.md index e41f011b..8a91f6a5 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -282,7 +282,7 @@ Use these WebP options for output image. * `options.lossless` **[boolean][7]** use lossless compression mode (optional, default `false`) * `options.nearLossless` **[boolean][7]** use near_lossless compression mode (optional, default `false`) * `options.smartSubsample` **[boolean][7]** use high quality chroma subsampling (optional, default `false`) - * `options.effort` **[number][9]** level of CPU effort to reduce file size, integer 0-6 (optional, default `4`) + * `options.effort` **[number][9]** CPU effort, between 0 (fastest) and 6 (slowest) (optional, default `4`) * `options.pageHeight` **[number][9]?** page height for animated output * `options.loop` **[number][9]** number of animation iterations, use 0 for infinite animation (optional, default `0`) * `options.delay` **[Array][10]<[number][9]>?** list of delays between animation frames (in milliseconds) @@ -460,7 +460,7 @@ AVIF image sequences are not supported. * `options.quality` **[number][9]** quality, integer 1-100 (optional, default `50`) * `options.lossless` **[boolean][7]** use lossless compression (optional, default `false`) - * `options.speed` **[number][9]** CPU effort vs file size, 0 (slowest/smallest) to 9 (fastest/largest) (optional, default `5`) + * `options.effort` **[number][9]** CPU effort, between 0 (fastest) and 9 (slowest) (optional, default `4`) * `options.chromaSubsampling` **[string][2]** set to '4:2:0' to use chroma subsampling (optional, default `'4:4:4'`) @@ -487,7 +487,7 @@ globally-installed libvips compiled with support for libheif, libde265 and x265. * `options.quality` **[number][9]** quality, integer 1-100 (optional, default `50`) * `options.compression` **[string][2]** compression format: av1, hevc (optional, default `'av1'`) * `options.lossless` **[boolean][7]** use lossless compression (optional, default `false`) - * `options.speed` **[number][9]** CPU effort vs file size, 0 (slowest/smallest) to 9 (fastest/largest) (optional, default `5`) + * `options.effort` **[number][9]** CPU effort, between 0 (fastest) and 9 (slowest) (optional, default `4`) * `options.chromaSubsampling` **[string][2]** set to '4:2:0' to use chroma subsampling (optional, default `'4:4:4'`) diff --git a/docs/changelog.md b/docs/changelog.md index e5a842cd..d69f1ddb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,8 @@ Requires libvips v8.12.0 * Standardise WebP `effort` option name, deprecate `reductionEffort`. +* Standardise HEIF `effort` option name, deprecate `speed`. + * Expose control over CPU effort for palette-based PNG output. [#2541](https://github.com/lovell/sharp/issues/2541) diff --git a/lib/constructor.js b/lib/constructor.js index 833beac8..eb25abfd 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -264,7 +264,7 @@ const Sharp = function (input, options) { heifQuality: 50, heifLossless: false, heifCompression: 'av1', - heifSpeed: 5, + heifEffort: 4, heifChromaSubsampling: '4:4:4', rawDepth: 'uchar', tileSize: 256, diff --git a/lib/output.js b/lib/output.js index daa15ef2..0c28d195 100644 --- a/lib/output.js +++ b/lib/output.js @@ -457,7 +457,7 @@ function png (options) { * @param {boolean} [options.lossless=false] - use lossless compression mode * @param {boolean} [options.nearLossless=false] - use near_lossless compression mode * @param {boolean} [options.smartSubsample=false] - use high quality chroma subsampling - * @param {number} [options.effort=4] - level of CPU effort to reduce file size, integer 0-6 + * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 6 (slowest) * @param {number} [options.pageHeight] - page height for animated output * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation * @param {number[]} [options.delay] - list of delays between animation frames (in milliseconds) @@ -806,7 +806,7 @@ function tiff (options) { * @param {Object} [options] - output options * @param {number} [options.quality=50] - quality, integer 1-100 * @param {boolean} [options.lossless=false] - use lossless compression - * @param {number} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 9 (fastest/largest) + * @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 * @returns {Sharp} * @throws {Error} Invalid options @@ -827,7 +827,7 @@ function avif (options) { * @param {number} [options.quality=50] - quality, integer 1-100 * @param {string} [options.compression='av1'] - compression format: av1, hevc * @param {boolean} [options.lossless=false] - use lossless compression - * @param {number} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 9 (fastest/largest) + * @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 * @returns {Sharp} * @throws {Error} Invalid options @@ -855,9 +855,15 @@ function heif (options) { throw is.invalidParameterError('compression', 'one of: av1, hevc', options.compression); } } - if (is.defined(options.speed)) { + if (is.defined(options.effort)) { + if (is.integer(options.effort) && is.inRange(options.effort, 0, 9)) { + this.options.heifEffort = options.effort; + } else { + throw is.invalidParameterError('effort', 'integer between 0 and 9', options.effort); + } + } else if (is.defined(options.speed)) { if (is.integer(options.speed) && is.inRange(options.speed, 0, 9)) { - this.options.heifSpeed = options.speed; + this.options.heifEffort = 9 - options.speed; } else { throw is.invalidParameterError('speed', 'integer between 0 and 9', options.speed); } diff --git a/src/pipeline.cc b/src/pipeline.cc index 24e25103..74a8b148 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -899,7 +899,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("strip", !baton->withMetadata) ->set("Q", baton->heifQuality) ->set("compression", baton->heifCompression) - ->set("speed", baton->heifSpeed) + ->set("effort", baton->heifEffort) ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4" ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON) ->set("lossless", baton->heifLossless))); @@ -1053,7 +1053,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("strip", !baton->withMetadata) ->set("Q", baton->heifQuality) ->set("compression", baton->heifCompression) - ->set("speed", baton->heifSpeed) + ->set("effort", baton->heifEffort) ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4" ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON) ->set("lossless", baton->heifLossless)); @@ -1512,7 +1512,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->heifCompression = static_cast( vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION, sharp::AttrAsStr(options, "heifCompression").data())); - baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed"); + baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort"); baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling"); // Raw output diff --git a/src/pipeline.h b/src/pipeline.h index 0f0443e6..d461a64d 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -176,7 +176,7 @@ struct PipelineBaton { double tiffYres; int heifQuality; VipsForeignHeifCompression heifCompression; - int heifSpeed; + int heifEffort; std::string heifChromaSubsampling; bool heifLossless; VipsBandFormat rawDepth; @@ -314,7 +314,7 @@ struct PipelineBaton { tiffYres(1.0), heifQuality(50), heifCompression(VIPS_FOREIGN_HEIF_COMPRESSION_AV1), - heifSpeed(5), + heifEffort(4), heifChromaSubsampling("4:4:4"), heifLossless(false), rawDepth(VIPS_FORMAT_UCHAR), diff --git a/test/unit/avif.js b/test/unit/avif.js index cd699149..98c37702 100644 --- a/test/unit/avif.js +++ b/test/unit/avif.js @@ -17,10 +17,9 @@ describe('AVIF', () => { .resize(32) .jpeg() .toBuffer(); - const metadata = await sharp(data) + const { size, ...metadata } = await sharp(data) .metadata(); - const { compression, size, ...metadataWithoutSize } = metadata; - assert.deepStrictEqual(metadataWithoutSize, { + assert.deepStrictEqual(metadata, { channels: 3, chromaSubsampling: '4:2:0', density: 72, @@ -38,13 +37,13 @@ describe('AVIF', () => { it('can convert JPEG to AVIF', async () => { const data = await sharp(inputJpg) .resize(32) - .avif() + .avif({ effort: 0 }) .toBuffer(); - const metadata = await sharp(data) + const { size, ...metadata } = await sharp(data) .metadata(); - const { compression, size, ...metadataWithoutSize } = metadata; - assert.deepStrictEqual(metadataWithoutSize, { + assert.deepStrictEqual(metadata, { channels: 3, + compression: 'av1', depth: 'uchar', format: 'heif', hasAlpha: false, @@ -63,11 +62,11 @@ describe('AVIF', () => { const data = await sharp(inputAvif) .resize(32) .toBuffer(); - const metadata = await sharp(data) + const { size, ...metadata } = await sharp(data) .metadata(); - const { compression, size, ...metadataWithoutSize } = metadata; - assert.deepStrictEqual(metadataWithoutSize, { + assert.deepStrictEqual(metadata, { channels: 3, + compression: 'av1', depth: 'uchar', format: 'heif', hasAlpha: false, @@ -85,12 +84,11 @@ describe('AVIF', () => { it('can convert animated GIF to non-animated AVIF', async () => { const data = await sharp(inputGifAnimated, { animated: true }) .resize(10) - .avif({ speed: 8 }) + .avif({ effort: 0 }) .toBuffer(); - const metadata = await sharp(data) + const { size, ...metadata } = await sharp(data) .metadata(); - const { size, ...metadataWithoutSize } = metadata; - assert.deepStrictEqual(metadataWithoutSize, { + assert.deepStrictEqual(metadata, { channels: 4, compression: 'av1', depth: 'uchar', diff --git a/test/unit/heif.js b/test/unit/heif.js index 52314562..36a73e3a 100644 --- a/test/unit/heif.js +++ b/test/unit/heif.js @@ -50,6 +50,21 @@ describe('HEIF', () => { sharp().heif({ compression: 1 }); }); }); + it('valid effort does not throw an error', () => { + assert.doesNotThrow(() => { + sharp().heif({ speed: 6 }); + }); + }); + it('out of range effort should throw an error', () => { + assert.throws(() => { + sharp().heif({ effort: 10 }); + }); + }); + it('invalid effort should throw an error', () => { + assert.throws(() => { + sharp().heif({ effort: 'fail' }); + }); + }); it('valid speed does not throw an error', () => { assert.doesNotThrow(() => { sharp().heif({ speed: 6 }); @@ -62,7 +77,7 @@ describe('HEIF', () => { }); it('invalid speed should throw an error', () => { assert.throws(() => { - sharp().heif({ compression: 'fail' }); + sharp().heif({ speed: 'fail' }); }); }); it('invalid chromaSubsampling should throw an error', () => {