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]>
  • Loading branch information
moorejs and benmccann authored Mar 27, 2024
1 parent 5b75d3f commit dc2f16f
Show file tree
Hide file tree
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
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)
})
}
}

0 comments on commit dc2f16f

Please sign in to comment.