diff --git a/.changeset/eight-chicken-worry.md b/.changeset/eight-chicken-worry.md
new file mode 100644
index 00000000..34995b9d
--- /dev/null
+++ b/.changeset/eight-chicken-worry.md
@@ -0,0 +1,5 @@
+---
+'imagetools-core': patch
+---
+
+feat: add picture-lqip format with low quality base64 image
diff --git a/docs/directives.md b/docs/directives.md
index 908c98ae..4cb84e9c 100644
--- a/docs/directives.md
+++ b/docs/directives.md
@@ -31,6 +31,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)
@@ -485,6 +486,26 @@ for (const [format, images] of Object.entries(picture.sources)) {
html += ``
```
+### Picture with low quality inplace image
+
+• **Keyword**: `picture-lqip`
• **Type**: _boolean_
+
+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 = '`
+```
+
### Source
• **Keyword**: `source`
• **Type**: _boolean_
diff --git a/docs/interfaces/core_src.Picture.md b/docs/interfaces/core_src.Picture.md
index a6a7901c..a4ce4c23 100644
--- a/docs/interfaces/core_src.Picture.md
+++ b/docs/interfaces/core_src.Picture.md
@@ -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
@@ -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)
diff --git a/docs/modules/core_src.md b/docs/modules/core_src.md
index 12baab7c..1fa1142a 100644
--- a/docs/modules/core_src.md
+++ b/docs/modules/core_src.md
@@ -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)
@@ -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)
diff --git a/packages/core/src/__tests__/__fixtures__/with-metadata-lqip.png b/packages/core/src/__tests__/__fixtures__/with-metadata-lqip.png
new file mode 100644
index 00000000..e5178bb3
--- /dev/null
+++ b/packages/core/src/__tests__/__fixtures__/with-metadata-lqip.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:30fc9dde17ee71a35d16a767507f4ebb08e6a1df3c23de15b0671da9cdedb9f3
+size 3894
diff --git a/packages/core/src/__tests__/output-formats.test.ts b/packages/core/src/__tests__/output-formats.test.ts
index 2468e294..9079aed2 100644
--- a/packages/core/src/__tests__/output-formats.test.ts
+++ b/packages/core/src/__tests__/output-formats.test.ts
@@ -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', () => {
@@ -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', () => {
diff --git a/packages/core/src/output-formats.ts b/packages/core/src/output-formats.ts
index 55d924b6..70af5fe4 100644
--- a/packages/core/src/output-formats.ts
+++ b/packages/core/src/output-formats.ts
@@ -1,3 +1,4 @@
+import { readFile } from 'fs/promises'
import type { ImageMetadata, Img, OutputFormat, Picture } from './types.js'
export const urlFormat: OutputFormat = () => (metadatas) => {
@@ -109,11 +110,44 @@ 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?.imagePath) {
+ const data = (await readFile(smallestFallback.imagePath as string)).toString('base64')
+ result.lqip = `data:image/${smallestFallback.format};base64,${data}`
+ } else if (smallestFallback?.image) {
+ const data = (await smallestFallback.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
}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 07149759..d913910e 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -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'
@@ -100,4 +100,5 @@ export interface Picture {
w: number
h: number
}
+ lqip?: string
}