Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add effort directive #618

Merged
merged 7 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rare-eels-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'imagetools-core': minor
---

feat: add effort directive
36 changes: 32 additions & 4 deletions docs/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [Directives](#directives)
- [Background](#background)
- [Blur](#blur)
- [Effort](#effort)
- [Fit](#fit)
- [Flatten](#flatten)
- [Flip](#flip)
Expand Down Expand Up @@ -86,6 +87,33 @@ import Image from 'example.jpg?blur=100'

---

### Effort

• **Keyword**: `effort`<br> • **Type**: _integer_ | _"max"_ | _"min"_ <br>

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`<br> • **Type**: _cover_ \| _contain_ \| _fill_ \| _inside_ \| _outside_ <br>
Expand Down Expand Up @@ -142,7 +170,7 @@ import Image from 'exmaple.jpg?flop=true'

### Format

• **Keyword**: `format`<br> • **Type**: _heic_\| _heif_ \| _avif_ \| _jpeg_ \| _jpg_ \| _png_ \| _tiff_ \| _webp_ \|
• **Keyword**: `format`<br> • **Type**: _jxl_\| _heif_ \| _avif_ \| _jpeg_ \| _jpg_ \| _png_ \| _tiff_ \| _webp_ \|
_gif_<br>

Convert the image into the given format.
Expand All @@ -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'
```

---
Expand Down Expand Up @@ -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**:

Expand Down Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Git LFS file not shown
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions packages/core/src/transforms/__tests__/effort.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
15 changes: 15 additions & 0 deletions packages/core/src/transforms/__tests__/format.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
})
36 changes: 36 additions & 0 deletions packages/core/src/transforms/effort.ts
Original file line number Diff line number Diff line change
@@ -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<string, [number, number]> = {
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<EffortOptions, number> = ({ 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
}
6 changes: 4 additions & 2 deletions packages/core/src/transforms/format.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -23,9 +24,10 @@ export const format: TransformFactory<FormatOptions> = (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)
})
}
}