From dc2f16f15cd4534b683cc73a82c5d5df195de5c2 Mon Sep 17 00:00:00 2001 From: Jared Moore Date: Wed, 27 Mar 2024 17:59:11 -0400 Subject: [PATCH] feat: add effort directive (#618) Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- .changeset/rare-eels-refuse.md | 5 ++ docs/directives.md | 36 ++++++++-- packages/core/src/index.ts | 1 + ...ts---format---transform---webp-w--effort-1 | 3 + ...s-format-transform-png-w-effort-1-snap.png | 3 + .../src/transforms/__tests__/effort.spec.ts | 71 +++++++++++++++++++ .../src/transforms/__tests__/format.spec.ts | 15 ++++ packages/core/src/transforms/effort.ts | 36 ++++++++++ packages/core/src/transforms/format.ts | 6 +- 9 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 .changeset/rare-eels-refuse.md create mode 100644 packages/core/src/transforms/__tests__/__file_snapshots__/src-transforms-__tests__-format.spec.ts---format---transform---webp-w--effort-1 create mode 100644 packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-png-w-effort-1-snap.png create mode 100644 packages/core/src/transforms/__tests__/effort.spec.ts create mode 100644 packages/core/src/transforms/effort.ts diff --git a/.changeset/rare-eels-refuse.md b/.changeset/rare-eels-refuse.md new file mode 100644 index 00000000..c95a51c7 --- /dev/null +++ b/.changeset/rare-eels-refuse.md @@ -0,0 +1,5 @@ +--- +'imagetools-core': minor +--- + +feat: add effort directive diff --git a/docs/directives.md b/docs/directives.md index 953e9512..908c98ae 100644 --- a/docs/directives.md +++ b/docs/directives.md @@ -5,6 +5,7 @@ - [Directives](#directives) - [Background](#background) - [Blur](#blur) + - [Effort](#effort) - [Fit](#fit) - [Flatten](#flatten) - [Flip](#flip) @@ -86,6 +87,33 @@ import Image from 'example.jpg?blur=100' --- +### Effort + +• **Keyword**: `effort`
• **Type**: _integer_ | _"max"_ | _"min"_
+ +Adjust the effort to spend encoding the image. +The effect of effort varies per format, but a lower value leads to faster encoding. + +The supported ranges by format: +- `png`: 1 to 10 (default 7) +- `webp`: 0 to 6 (default 4) +- `avif`/`heif`: 0 to 9 (default 4) +- `jxl`: 3 to 9 (default 7) +- `gif`: 1 to 10 (default 7) + +The keywords `"min"` and `"max"` apply the highest effort value for the given image format. + +> Search `options.effort` in [sharp's Output options documentation](https://sharp.pixelplumbing.com/api-output) for details. + +• **Example**: + +```js +import highestEffortWebp from 'example.jpg?format=webp&effort=max' +import quicklyGeneratingAvif from 'example.jpg?format=avif&effort=0' +``` + +--- + ### Fit • **Keyword**: `fit`
• **Type**: _cover_ \| _contain_ \| _fill_ \| _inside_ \| _outside_
@@ -142,7 +170,7 @@ import Image from 'exmaple.jpg?flop=true' ### Format -• **Keyword**: `format`
• **Type**: _heic_\| _heif_ \| _avif_ \| _jpeg_ \| _jpg_ \| _png_ \| _tiff_ \| _webp_ \| +• **Keyword**: `format`
• **Type**: _jxl_\| _heif_ \| _avif_ \| _jpeg_ \| _jpg_ \| _png_ \| _tiff_ \| _webp_ \| _gif_
Convert the image into the given format. @@ -154,7 +182,7 @@ Convert the image into the given format. ```js import Image from 'example.jpg?format=webp' -import Images from 'example.jpg?format=webp;avif;heic' +import Images from 'example.jpg?format=webp;avif;jxl' ``` --- @@ -233,7 +261,7 @@ Use this directive to set a different interpolation kernel when resizing the ima Use lossless compression mode. Formats that support this directive are: - `avif`, `heif`, `heic`, and `webp` + `avif`, `heif`, `jxl`, and `webp` • **Example**: @@ -295,7 +323,7 @@ See sharps [resize options](https://sharp.pixelplumbing.com/api-resize#resize) f All formats (except `gif`) allow the quality to be adjusted by setting this directive. -The argument must be a number between 0 and 100. +The argument must be a number between 1 and 100. > See sharps [Output options](https://sharp.pixelplumbing.com/api-output) for default quality values. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 148556c2..96edd03a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,6 @@ export * from './transforms/background.js' export * from './transforms/blur.js' +export * from './transforms/effort.js' export * from './transforms/fit.js' export * from './transforms/flatten.js' export * from './transforms/flip.js' diff --git a/packages/core/src/transforms/__tests__/__file_snapshots__/src-transforms-__tests__-format.spec.ts---format---transform---webp-w--effort-1 b/packages/core/src/transforms/__tests__/__file_snapshots__/src-transforms-__tests__-format.spec.ts---format---transform---webp-w--effort-1 new file mode 100644 index 00000000..0ee6545b --- /dev/null +++ b/packages/core/src/transforms/__tests__/__file_snapshots__/src-transforms-__tests__-format.spec.ts---format---transform---webp-w--effort-1 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edf97a52065e500d6d224e36222eb3083d691295643e550484c917e39136e0fb +size 14920 diff --git a/packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-png-w-effort-1-snap.png b/packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-png-w-effort-1-snap.png new file mode 100644 index 00000000..9fc902ea --- /dev/null +++ b/packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-png-w-effort-1-snap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cb50732d3aa1e40ba4745f9cf3156b18655b4550f808bf018459e28efd3cf59 +size 189976 diff --git a/packages/core/src/transforms/__tests__/effort.spec.ts b/packages/core/src/transforms/__tests__/effort.spec.ts new file mode 100644 index 00000000..c760145e --- /dev/null +++ b/packages/core/src/transforms/__tests__/effort.spec.ts @@ -0,0 +1,71 @@ +import { getEffort } from '../effort' +import sharp, { Sharp } from 'sharp' +import { join } from 'path' +import { describe, beforeEach, expect, test, it } from 'vitest' +import { METADATA } from '../../lib/metadata' + +describe('effort', () => { + let img: Sharp + beforeEach(() => { + img = sharp(join(__dirname, '../../__tests__/__fixtures__/pexels-allec-gomes-5195763.png')) + }) + + test('keyword "effort"', () => { + const res = getEffort({ effort: '3' }, img) + + expect(res).toEqual(3) + }) + + test('missing', () => { + const res = getEffort({}, img) + + expect(res).toBeUndefined() + }) + + describe('arguments', () => { + test('invalid', () => { + const res = getEffort({ effort: 'invalid' }, img) + + expect(res).toBeUndefined() + }) + + test('empty', () => { + const res = getEffort({ effort: '' }, img) + + expect(res).toBeUndefined() + }) + + test('integer', () => { + const res = getEffort({ effort: '3' }, img) + + expect(res).toEqual(3) + }) + + it('rounds float to int', () => { + const res = getEffort({ effort: '3.5' }, img) + + expect(res).toEqual(3) + }) + + it('sets to minimum effort with "min"', async () => { + img[METADATA] = { format: 'webp' } + const res = getEffort({ effort: 'min' }, img) + + expect(res).toEqual(0) + }) + + it('sets to maximum effort with "max"', async () => { + img[METADATA] = { format: 'webp' } + const res = getEffort({ effort: 'max' }, img) + + expect(res).toEqual(6) + }) + + it('ignores effort when not applicable', async () => { + img[METADATA] = { format: 'jpeg' } + const res = getEffort({ effort: 'max' }, img) + + expect(res).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/transforms/__tests__/format.spec.ts b/packages/core/src/transforms/__tests__/format.spec.ts index 9ba3a1b3..90c53dc3 100644 --- a/packages/core/src/transforms/__tests__/format.spec.ts +++ b/packages/core/src/transforms/__tests__/format.spec.ts @@ -162,5 +162,20 @@ describe('format', () => { expect(await image.toBuffer()).toMatchFile() }) + + test('png w/ effort', async () => { + const { image } = await applyTransforms([format({ format: 'png', effort: '1' }, dirCtx)!], img) + + expect(await image.toBuffer()).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent' + }) + }) + + test('webp w/ effort', async () => { + const { image } = await applyTransforms([format({ format: 'webp', effort: 'min' }, dirCtx)!], img) + + expect(await image.toBuffer()).toMatchFile() + }) }) }) diff --git a/packages/core/src/transforms/effort.ts b/packages/core/src/transforms/effort.ts new file mode 100644 index 00000000..646e33fe --- /dev/null +++ b/packages/core/src/transforms/effort.ts @@ -0,0 +1,36 @@ +import { TransformOption } from '../types.js' +import { getMetadata, setMetadata } from '../lib/metadata.js' + +export interface EffortOptions { + effort: string +} + +const FORMAT_TO_EFFORT_RANGE: Record = { + avif: [0, 9], + gif: [1, 10], + heif: [0, 9], + jxl: [3, 9], + png: [1, 10], + webp: [0, 6] +} + +function parseEffort(effort: string, format: string) { + if (effort === 'min') { + return FORMAT_TO_EFFORT_RANGE[format]?.[0] + } else if (effort === 'max') { + return FORMAT_TO_EFFORT_RANGE[format]?.[1] + } + return parseInt(effort) +} + +export const getEffort: TransformOption = ({ effort: _effort }, image) => { + if (!_effort) return + + const format = (getMetadata(image, 'format') ?? '') as string + const effort = parseEffort(_effort, format) + if (!Number.isInteger(effort)) return + + setMetadata(image, 'effort', effort) + + return effort +} diff --git a/packages/core/src/transforms/format.ts b/packages/core/src/transforms/format.ts index 02443cfe..f2e81a7b 100644 --- a/packages/core/src/transforms/format.ts +++ b/packages/core/src/transforms/format.ts @@ -1,5 +1,6 @@ import { TransformFactory } from '../types.js' import { METADATA } from '../lib/metadata.js' +import { getEffort } from './effort.js' import { getQuality } from './quality.js' import { getProgressive } from './progressive.js' import { getLossless } from './lossless.js' @@ -23,9 +24,10 @@ export const format: TransformFactory = (config) => { return image.toFormat(format, { compression: format == 'heif' ? 'av1' : undefined, - quality: getQuality(config, image), + effort: getEffort(config, image), lossless: getLossless(config, image) as boolean, - progressive: getProgressive(config, image) as boolean + progressive: getProgressive(config, image) as boolean, + quality: getQuality(config, image) }) } }