From 75487055d8054e44496c7b3ff90e44c8fd8a78f6 Mon Sep 17 00:00:00 2001 From: Jared Moore Date: Wed, 27 Sep 2023 17:08:29 -0400 Subject: [PATCH 1/6] feat: add effort directive --- .changeset/rare-eels-refuse.md | 5 ++ docs/directives.md | 27 ++++++++- packages/core/src/index.ts | 1 + ...s-format-transform-png-w-effort-1-snap.png | 3 + ...-format-transform-webp-w-effort-1-snap.png | 3 + .../src/transforms/__tests__/effort.spec.ts | 55 +++++++++++++++++++ .../src/transforms/__tests__/format.spec.ts | 12 ++++ packages/core/src/transforms/effort.ts | 16 ++++++ packages/core/src/transforms/format.ts | 2 + 9 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 .changeset/rare-eels-refuse.md 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__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-webp-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 f96b0fb4..1b9fef4b 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) @@ -85,6 +86,30 @@ import Image from 'example.jpg?blur=100' --- +### Effort + +• **Keyword**: `effort`
• **Type**: _integer_
+ +Adjust the effort to spend encoding the image. +The effect of effort varies per encoding, but a lower value leads to faster encoding. + +The supported range varies per encoding: +- `png`: 1 to 10 (default 7) +- `webp`: 0 to 6 (default 4) +- `avif`/`heif`/`heic`: 0 to 9 (default 4) +- `gif`: 1 to 10 (default 7) + +> Search `options.effort` in [sharp's Output options documentation](https://sharp.pixelplumbing.com/api-output) for details. + +• **Example**: + +```js +import GenerateQuicklyWebp from 'example.jpg?format=webp&effort=1' +import GenerateQuicklyAvif from 'example.jpg?format=avif&effort=0' +``` + +--- + ### Fit • **Keyword**: `fit`
• **Type**: _cover_ \| _contain_ \| _fill_ \| _inside_ \| _outside_
@@ -294,7 +319,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 55b1acaf..89b22bd6 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__/__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..22923b0d --- /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:affc5551c29fa5fa062f068fc70e950c8c2743a68b1209737f4032ee4ba91d19 +size 187068 diff --git a/packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-webp-w-effort-1-snap.png b/packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-webp-w-effort-1-snap.png new file mode 100644 index 00000000..1cafad7c --- /dev/null +++ b/packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-webp-w-effort-1-snap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bcacda7957390d6d01c1a8d2787079802986909c4c84a761160fb2bbf3276f2 +size 11776 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..622a8426 --- /dev/null +++ b/packages/core/src/transforms/__tests__/effort.spec.ts @@ -0,0 +1,55 @@ +import { getEffort } from '../effort' +import sharp, { Sharp } from 'sharp' +import { join } from 'path' +import { describe, beforeEach, expect, test, it } from 'vitest' + +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('rounds float to int', () => { + const res = getEffort({ effort: '3.5' }, img) + + expect(res).toEqual(3) + }) + }) +}) diff --git a/packages/core/src/transforms/__tests__/format.spec.ts b/packages/core/src/transforms/__tests__/format.spec.ts index 9ba3a1b3..027e9179 100644 --- a/packages/core/src/transforms/__tests__/format.spec.ts +++ b/packages/core/src/transforms/__tests__/format.spec.ts @@ -162,5 +162,17 @@ 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() + }) + + test('webp w/ effort', async () => { + const { image } = await applyTransforms([format({ format: 'webp', effort: '0' }, dirCtx)!], img) + + expect(await image.toBuffer()).toMatchImageSnapshot() + }) }) }) diff --git a/packages/core/src/transforms/effort.ts b/packages/core/src/transforms/effort.ts new file mode 100644 index 00000000..9ab9f814 --- /dev/null +++ b/packages/core/src/transforms/effort.ts @@ -0,0 +1,16 @@ +import { TransformOption } from '../types.js' +import { setMetadata } from '../lib/metadata.js' + +export interface EffortOptions { + effort: string +} + +export const getEffort: TransformOption = ({ effort: _effort }, image) => { + const effort = _effort && parseInt(_effort) + + if (!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 343f7cff..4890b0e3 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' @@ -22,6 +23,7 @@ export const format: TransformFactory = (config) => { image[METADATA].format = format return image.toFormat(format, { + effort: getEffort(config, image), quality: getQuality(config, image), lossless: getLossless(config, image) as boolean, progressive: getProgressive(config, image) as boolean From 63dd7bb8f8ec922145be0d3fd9934495f600aa3d Mon Sep 17 00:00:00 2001 From: Jared Moore Date: Tue, 10 Oct 2023 09:55:58 -0400 Subject: [PATCH 2/6] feat: improve usability of effort (add min+max) --- docs/directives.md | 12 +++++---- .../src/transforms/__tests__/effort.spec.ts | 22 +++++++++++++--- packages/core/src/transforms/effort.ts | 26 ++++++++++++++++--- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/docs/directives.md b/docs/directives.md index 1b9fef4b..1e2ad1f6 100644 --- a/docs/directives.md +++ b/docs/directives.md @@ -88,24 +88,26 @@ import Image from 'example.jpg?blur=100' ### Effort -• **Keyword**: `effort`
• **Type**: _integer_
+• **Keyword**: `effort`
• **Type**: _integer_ | _"max"_ | _"min"_
Adjust the effort to spend encoding the image. -The effect of effort varies per encoding, but a lower value leads to faster encoding. +The effect of effort varies per format, but a lower value leads to faster encoding. -The supported range varies per encoding: +The supported ranges by format: - `png`: 1 to 10 (default 7) - `webp`: 0 to 6 (default 4) - `avif`/`heif`/`heic`: 0 to 9 (default 4) - `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 GenerateQuicklyWebp from 'example.jpg?format=webp&effort=1' -import GenerateQuicklyAvif from 'example.jpg?format=avif&effort=0' +import highestEffortWebp from 'example.jpg?format=webp&effort=max' +import quicklyGeneratingAvif from 'example.jpg?format=avif&effort=0' ``` --- diff --git a/packages/core/src/transforms/__tests__/effort.spec.ts b/packages/core/src/transforms/__tests__/effort.spec.ts index 622a8426..c760145e 100644 --- a/packages/core/src/transforms/__tests__/effort.spec.ts +++ b/packages/core/src/transforms/__tests__/effort.spec.ts @@ -2,6 +2,7 @@ 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 @@ -46,10 +47,25 @@ describe('effort', () => { expect(res).toEqual(3) }) - it('rounds float to int', () => { - const res = getEffort({ effort: '3.5' }, img) + it('sets to minimum effort with "min"', async () => { + img[METADATA] = { format: 'webp' } + const res = getEffort({ effort: 'min' }, img) - expect(res).toEqual(3) + 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/effort.ts b/packages/core/src/transforms/effort.ts index 9ab9f814..950be180 100644 --- a/packages/core/src/transforms/effort.ts +++ b/packages/core/src/transforms/effort.ts @@ -1,14 +1,34 @@ import { TransformOption } from '../types.js' -import { setMetadata } from '../lib/metadata.js' +import { getMetadata, setMetadata } from '../lib/metadata.js' export interface EffortOptions { effort: string } +const FORMAT_TO_EFFORT_RANGE: Record = { + png: [1, 10], + webp: [0, 6], + avif: [0, 9], + heif: [0, 9], + heic: [0, 9], + gif: [1, 10] +} + +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) => { - const effort = _effort && parseInt(_effort) + if (!_effort) return - if (!effort) return + const format = (getMetadata(image, 'format') ?? '') as string + const effort = parseEffort(_effort, format) + if (!Number.isInteger(effort)) return setMetadata(image, 'effort', effort) From 47b1693cb54d00312ff2ac6539331b8ec2e2fda3 Mon Sep 17 00:00:00 2001 From: Jared Moore Date: Tue, 10 Oct 2023 09:59:21 -0400 Subject: [PATCH 3/6] test(format): use effort keyword --- packages/core/src/transforms/__tests__/format.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/transforms/__tests__/format.spec.ts b/packages/core/src/transforms/__tests__/format.spec.ts index 027e9179..3c175c17 100644 --- a/packages/core/src/transforms/__tests__/format.spec.ts +++ b/packages/core/src/transforms/__tests__/format.spec.ts @@ -170,7 +170,7 @@ describe('format', () => { }) test('webp w/ effort', async () => { - const { image } = await applyTransforms([format({ format: 'webp', effort: '0' }, dirCtx)!], img) + const { image } = await applyTransforms([format({ format: 'webp', effort: 'min' }, dirCtx)!], img) expect(await image.toBuffer()).toMatchImageSnapshot() }) From 7752c28640313564e4de4642f06447919f069dff Mon Sep 17 00:00:00 2001 From: Jared Moore Date: Tue, 10 Oct 2023 10:14:50 -0400 Subject: [PATCH 4/6] test(effort): update snapshot image --- ...s__-format.spec.ts---format---transform---webp-w--effort-1 | 3 +++ ...ts-format-spec-ts-format-transform-png-w-effort-1-snap.png | 4 ++-- ...s-format-spec-ts-format-transform-webp-w-effort-1-snap.png | 3 --- packages/core/src/transforms/__tests__/format.spec.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/transforms/__tests__/__file_snapshots__/src-transforms-__tests__-format.spec.ts---format---transform---webp-w--effort-1 delete mode 100644 packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-webp-w-effort-1-snap.png 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 index 22923b0d..9fc902ea 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:affc5551c29fa5fa062f068fc70e950c8c2743a68b1209737f4032ee4ba91d19 -size 187068 +oid sha256:1cb50732d3aa1e40ba4745f9cf3156b18655b4550f808bf018459e28efd3cf59 +size 189976 diff --git a/packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-webp-w-effort-1-snap.png b/packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-webp-w-effort-1-snap.png deleted file mode 100644 index 1cafad7c..00000000 --- a/packages/core/src/transforms/__tests__/__image_snapshots__/format-spec-ts-src-transforms-tests-format-spec-ts-format-transform-webp-w-effort-1-snap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6bcacda7957390d6d01c1a8d2787079802986909c4c84a761160fb2bbf3276f2 -size 11776 diff --git a/packages/core/src/transforms/__tests__/format.spec.ts b/packages/core/src/transforms/__tests__/format.spec.ts index 3c175c17..e73c695d 100644 --- a/packages/core/src/transforms/__tests__/format.spec.ts +++ b/packages/core/src/transforms/__tests__/format.spec.ts @@ -172,7 +172,7 @@ describe('format', () => { test('webp w/ effort', async () => { const { image } = await applyTransforms([format({ format: 'webp', effort: 'min' }, dirCtx)!], img) - expect(await image.toBuffer()).toMatchImageSnapshot() + expect(await image.toBuffer()).toMatchFile() }) }) }) From befee19bdd22bd0b2de4a30a8cbaa33afcad8993 Mon Sep 17 00:00:00 2001 From: Jared Moore Date: Wed, 1 Nov 2023 13:25:55 -0400 Subject: [PATCH 5/6] test: allow for higher difference threshold in PNG effort result Covers platform difference between mac and linux (sharp#3783) This pattern is already used elsewhere --- packages/core/src/transforms/__tests__/format.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/transforms/__tests__/format.spec.ts b/packages/core/src/transforms/__tests__/format.spec.ts index e73c695d..90c53dc3 100644 --- a/packages/core/src/transforms/__tests__/format.spec.ts +++ b/packages/core/src/transforms/__tests__/format.spec.ts @@ -166,7 +166,10 @@ describe('format', () => { test('png w/ effort', async () => { const { image } = await applyTransforms([format({ format: 'png', effort: '1' }, dirCtx)!], img) - expect(await image.toBuffer()).toMatchImageSnapshot() + expect(await image.toBuffer()).toMatchImageSnapshot({ + failureThreshold: 0.05, + failureThresholdType: 'percent' + }) }) test('webp w/ effort', async () => { From d57c5d76f4f0de384f03010be0012911bf6a1877 Mon Sep 17 00:00:00 2001 From: Jared Moore Date: Wed, 1 Nov 2023 16:06:40 -0400 Subject: [PATCH 6/6] add jxl, nix hiec --- docs/directives.md | 9 +++++---- packages/core/src/transforms/effort.ts | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/directives.md b/docs/directives.md index 1e2ad1f6..6b0ee736 100644 --- a/docs/directives.md +++ b/docs/directives.md @@ -96,7 +96,8 @@ The effect of effort varies per format, but a lower value leads to faster encodi The supported ranges by format: - `png`: 1 to 10 (default 7) - `webp`: 0 to 6 (default 4) -- `avif`/`heif`/`heic`: 0 to 9 (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. @@ -168,7 +169,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. @@ -180,7 +181,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' ``` --- @@ -259,7 +260,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**: diff --git a/packages/core/src/transforms/effort.ts b/packages/core/src/transforms/effort.ts index 950be180..646e33fe 100644 --- a/packages/core/src/transforms/effort.ts +++ b/packages/core/src/transforms/effort.ts @@ -6,12 +6,12 @@ export interface EffortOptions { } const FORMAT_TO_EFFORT_RANGE: Record = { - png: [1, 10], - webp: [0, 6], avif: [0, 9], + gif: [1, 10], heif: [0, 9], - heic: [0, 9], - gif: [1, 10] + jxl: [3, 9], + png: [1, 10], + webp: [0, 6] } function parseEffort(effort: string, format: string) {