Skip to content

Commit

Permalink
feat: add effort directive (#618)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben McCann <[email protected]>
moorejs and benmccann authored Mar 27, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 5b75d3f commit dc2f16f
Showing 9 changed files with 170 additions and 6 deletions.
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
@@ -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`<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>
@@ -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.
@@ -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.
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'
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
@@ -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'
@@ -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)
})
}
}

0 comments on commit dc2f16f

Please sign in to comment.