Skip to content

Commit

Permalink
feat: add picture-lqip format with low quality base64 image
Browse files Browse the repository at this point in the history
  • Loading branch information
pzerelles committed Nov 29, 2023
1 parent ce8a285 commit e835256
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-chicken-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'imagetools-core': patch
---

feat: add picture-lqip format with low quality base64 image
21 changes: 21 additions & 0 deletions docs/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- [Tint](#tint)
- [Metadata](#metadata)
- [Picture](#picture)
- [Picture with low quality inplace image](#picture-with-low-quality-inplace-image)
- [Source](#source)
- [Srcset](#srcset)
- [URL](#url)
Expand Down Expand Up @@ -456,6 +457,26 @@ for (const [format, images] of Object.entries(picture.sources)) {
html += `<img src={picture.img.src} /></picture>`
```

### Picture with low quality inplace image

**Keyword**: `picture-lqip`<br> • **Type**: _boolean_<br>

Returns information about the image necessary to render a `picture` tag as a JavaScript object.
Includes a base64 encoded inplace representation of the image using the smallest requested size
and the fallback format. The smallest requested size will be excluded from the sources.

**Example**:

```js
import picture from 'example.jpg?w=50;500;900;1200&format=avif;webp;jpg&as=picture-lqip'

let html = '<picture>';
for (const [format, images] of Object.entries(picture.sources)) {
html += `<source srcset={images.map((i) => `${i.src}`).join(', ')} type={'image/' + format} />`;
}
html += `<img src={picture.lqip} /></picture>`
```

### Source

**Keyword**: `url`<br> • **Type**: _boolean_<br>
Expand Down
11 changes: 11 additions & 0 deletions docs/interfaces/core_src.Picture.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The picture output format.

- [img](core_src.Picture.md#img)
- [sources](core_src.Picture.md#sources)
- [lqip](core_src.Picture.md#lqip)

## Properties

Expand Down Expand Up @@ -42,3 +43,13 @@ Key is format. Value is srcset.
#### Defined in

[packages/core/src/types.ts:97](https://github.com/JonasKruckenberg/imagetools/blob/4ebc88f/packages/core/src/types.ts#L97)

### lqip

`Optional` **lqip**: `string`

Low quality inplace image, base64 encoded, prepared for use with `src` attribute.

#### Defined in

[packages/core/src/types.ts:103](https://github.com/JonasKruckenberg/imagetools/blob/4ebc88f/packages/core/src/types.ts#L103)
35 changes: 35 additions & 0 deletions docs/modules/core_src.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
- [imgFormat](core_src.md#imgformat)
- [invert](core_src.md#invert)
- [loadImage](core_src.md#loadimage)
- [lqipPictureFormat](core_src.md#lqipPictureformat)
- [median](core_src.md#median)
- [metadataFormat](core_src.md#metadataformat)
- [normalize](core_src.md#normalize)
Expand Down Expand Up @@ -832,6 +833,40 @@ ___

___

### lqipPictureFormat

**lqipPictureFormat**(`args?`): (`metadata`: [`ProcessedImageMetadata`](../interfaces/core_src.ProcessedImageMetadata.md)[]) => `unknown`

fallback format should be specified last

#### Parameters

| Name | Type |
| :------ | :------ |
| `args?` | `string`[] |

#### Returns

`fn`

▸ (`metadata`): `unknown`

##### Parameters

| Name | Type |
| :------ | :------ |
| `metadata` | [`ProcessedImageMetadata`](../interfaces/core_src.ProcessedImageMetadata.md)[] |

##### Returns

`unknown`

#### Defined in

[packages/core/src/types.ts:71](https://github.com/JonasKruckenberg/imagetools/blob/4ebc88f/packages/core/src/types.ts#L71)

___

### median

**median**(`metadata`, `ctx`): `undefined` \| [`ImageTransformation`](core_src.md#imagetransformation)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 32 additions & 1 deletion packages/core/src/__tests__/output-formats.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { urlFormat, metadataFormat, imgFormat, pictureFormat, srcsetFormat } from '../output-formats'
import { urlFormat, metadataFormat, imgFormat, pictureFormat, lqipPictureFormat, srcsetFormat } from '../output-formats'
import { describe, test, expect } from 'vitest'
import sharp from 'sharp'
import { join } from 'path'

describe('url format', () => {
test('single image', () => {
Expand Down Expand Up @@ -117,6 +119,35 @@ describe('picture format', () => {
}
})
})

test('multiple image formats and sizes with low quality inplace picture', async () => {
const image = sharp(join(__dirname, './__fixtures__/with-metadata-lqip.png'))
const output = await lqipPictureFormat()([
{ src: '/foo-100.avif', format: 'avif', width: 100, height: 50 },
{ src: '/foo-100.webp', format: 'webp', width: 100, height: 50 },
{ src: '/foo-100.jpg', format: 'jpg', width: 100, height: 50 },
{ src: '/foo-50.avif', format: 'avif', width: 50, height: 25 },
{ src: '/foo-50.webp', format: 'webp', width: 50, height: 25 },
{ src: '/foo-50.jpg', format: 'jpg', width: 50, height: 25 },
{ src: '/foo-10.avif', format: 'avif', width: 10, height: 5, image },
{ src: '/foo-10.webp', format: 'webp', width: 10, height: 5, image },
{ src: '/foo-10.jpg', format: 'jpg', width: 10, height: 5, image }
])

expect(output).toStrictEqual({
sources: {
avif: '/foo-100.avif 100w, /foo-50.avif 50w',
webp: '/foo-100.webp 100w, /foo-50.webp 50w',
jpeg: '/foo-100.jpg 100w, /foo-50.jpg 50w'
},
img: {
src: '/foo-100.jpg',
w: 100,
h: 50
},
lqip: 'data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAACXBIWXMAABuvAAAbrwFeGpEcAAABHklEQVR4nAXBjU6CQAAA4NuaZoGHnAcccN0hBx4HIalgYZr9TavNbKvWnDWz93+Ivg9Y0qBjyxLQ6DR9R5sUrC6VFCgJYa4wcHNiS+QTSD1NCay4xd22CszpuK+ECZys653joAeFZ+R9GhM4kl6V9xZ1niUuwMGxy/TA1gOzkTGzEDTldn0RzcrkaihB127F3KJY+9uu1o9VWajDfvd0P5mP+tPUAwTr1NYpgS/L+U0lI25XRVwkbDqMSuUD0tVCisx2I2I45paDtOsqPey/n5d3kmGAO60kdMsiUcL9fN9sXtfb3e/yYREyJ+YIQOMoi/231ezMw7ezSVlWq/Xu4+unvhxkYw5Ir0nJST3gHb0JTxsY6cRGXBA5ZG5q/gN38SygSTScDwAAAABJRU5ErkJggg=='
})
})
})

describe('srcset format', () => {
Expand Down
34 changes: 33 additions & 1 deletion packages/core/src/output-formats.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ImageMetadata, Img, OutputFormat, Picture } from './types.js'
import { readFile } from 'fs/promises'
import type { ImageMetadata, Img, OutputFormat, Picture, TransformResult } from './types.js'

export const urlFormat: OutputFormat = () => (metadatas) => {
const urls: string[] = metadatas.map((metadata) => metadata.src as string)
Expand Down Expand Up @@ -109,11 +110,42 @@ export const pictureFormat: OutputFormat = () => (metadatas) => {
return result
}

export const lqipPictureFormat: OutputFormat = () => async (metadatas) => {
const fallbackFormat = [...new Set(metadatas.map((m) => getFormat(m)))].pop()

let smallestFallback
let smallestFallbackSize = 0
for (const m of metadatas) {
if (m.format?.replace('jpg', 'jpeg') === fallbackFormat) {
if (m.width && (!smallestFallbackSize || m.width < smallestFallbackSize)) {
smallestFallback = m
smallestFallbackSize = m.width
}
}
}

const filteredMetadatas = metadatas.filter((m) => m.width && m.width > smallestFallbackSize)
if (filteredMetadatas.length > 0) {
metadatas = filteredMetadatas
}

const result = pictureFormat()(metadatas) as Picture

if (smallestFallback) {
const image = smallestFallback.image as string | TransformResult['image']
const data = (await (typeof image === 'string' ? readFile(image) : image.toBuffer())).toString('base64')
result.lqip = `data:image/${smallestFallback.format};base64,${data}`
}

return result
}

export const builtinOutputFormats = {
url: urlFormat,
srcset: srcsetFormat,
img: imgFormat,
picture: pictureFormat,
'picture-lqip': lqipPictureFormat,
metadata: metadataFormat,
meta: metadataFormat
}
3 changes: 2 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Metadata, Sharp } from 'sharp'
import type { Metadata, Sharp } from 'sharp'
import { kernelValues } from './transforms/kernel.js'
import { positionValues } from './transforms/position.js'

Expand Down Expand Up @@ -100,4 +100,5 @@ export interface Picture {
w: number
h: number
}
lqip?: string
}

0 comments on commit e835256

Please sign in to comment.