From 3b638543c0a479d27031a6f8347e5adf9478b6a3 Mon Sep 17 00:00:00 2001 From: kheiner Date: Tue, 7 Apr 2026 15:10:51 -0700 Subject: [PATCH 01/10] feat: Add Sharp Operations for Directus Types Intentionally omitting Resizing operations at the moment due to coverage in Nuxt/Image and abstraction available to Directus. TODO: Fix duplication of transform object. --- src/runtime/providers/directus.ts | 156 +++++++++++++++++++++++++++--- 1 file changed, 142 insertions(+), 14 deletions(-) diff --git a/src/runtime/providers/directus.ts b/src/runtime/providers/directus.ts index 2baffeb75..141b4369e 100644 --- a/src/runtime/providers/directus.ts +++ b/src/runtime/providers/directus.ts @@ -1,30 +1,158 @@ +/** +* SharpOperations +* [ ] Resize // These operations are advanced and add a lot of complexity to the code base; they are already abstracted in Directus to be simpler +* +* [X] Image +* [X] Color +* [X] Channel +* +*/ + + import { joinURL } from 'ufo' import { createOperationsGenerator } from '../utils/index' import { defineProvider } from '../utils/provider' -const operationsGenerator = createOperationsGenerator() +type SharpOperationMap = { + // Image Operations https://sharp.pixelplumbing.com/api-operation/ + rotate: [angle?: number, options?: { background: Color }] + flip: [] + flop: [] + sharpen: [sigma?: number] | [options?: { sigma?: number, m1?: number, m2?: number, x1?: number, y2?: number, y3?: number }] + median: [size?: number] + blur: [sigma?: number] | [options?: { sigma?: number, precision?: 'integer' | 'float' | 'approximate', minAmplitude?: number }] + flatten: [options?: { background: Color }] + unflatten: [] + gamma: [gamma?: number, gammaOut?: number] + negate: [options?: { alpha?: boolean }] + normalize: [lower?: number, upper?: number] + normalise: [lower?: number, upper?: number] // Alias + clahe: [options?: { width: number, height: number, maxSlope?: number }] + convolve: [kernel: { width: number, height: number, kernel: number[], offset?: number }] + threshold: [value?: number, options?: { grayscale?: boolean }] + linear: [a?: number | number[], b?: number | number[]] + recomb: [matrix: number[][]] + modulate: [options?: { + brightness?: number + saturation?: number + hue?: number + lightness?: number + }] + + // Color Manipulation https://sharp.pixelplumbing.com/api-colour/ + tint: [color: Color] + grayscale: [] + greyscale: [] // Alias + pipelineColorspace: [colorspace: SharpColorspace] + pipelineColourspace: [colourspace: SharpColorspace] // Alias + toColorspace: [colorspace: SharpColorspace] + toColourspace: [colourspace: SharpColorspace] // Alias + + // Channel Manipulation https://sharp.pixelplumbing.com/api-channel/ + removeAlpha: [] + ensureAlpha: [alpha?: number] + extractChannel: [channel: 'red' | 'green' | 'blue' | 'alpha'] // Restricting to string channel extraction to avoid complexity. +} + +type SharpColorspace + = | 'srgb' + | 'rgb' + | 'scrgb' + | 'rgb16' + | 'cmyk' + | 'lab' + | 'b-w' + | string // retain advanced options for advanced users + +type Color + = | string + | { r: number, g: number, b: number, alpha?: number } + | { h: number, s: number, l: number, alpha?: number } + | { h: number, s: number, v: number, alpha?: number } + | { c: number, m: number, y: number, k: number, alpha?: number } + | { h: number, w: number, b: number } + | { l: number, c: number, h: number } + | { l: number, a: number, b: number } + | { h: number, c: number, g: number } + +type KnownSharpOperation + = { + [K in keyof SharpOperationMap]: + SharpOperationMap[K] extends [] + ? [K] + : [K, ...SharpOperationMap[K]] + }[keyof SharpOperationMap] + +type CustomSharpOperation + = K extends keyof SharpOperationMap + ? never + : [key: K, ...args: any[]] + +type SharpOperation + = | KnownSharpOperation + | CustomSharpOperation + +type DirectusModifiers + = | { key: string } + | { + key?: never + withoutEnlargement?: boolean + transforms?: SharpOperation[] + } interface DirectusOptions { baseURL: string - modifiers?: { - transforms?: string[] - withoutEnlargement?: boolean + width?: number + height?: number + quality?: number + format?: 'auto' | 'jpg' | 'png' | 'webp' | 'tiff' | 'avif' + fit?: 'cover' | 'contain' | 'inside' | 'outside' | 'fill' + modifiers?: DirectusModifiers +} + +function validateTransforms( + transforms?: SharpOperation[], +): string | undefined { + if (!transforms) return + if (!Array.isArray(transforms)) { + throw new TypeError('[nuxt][image][directus] transforms must be an array of tuples.') + } + for (let i = 0; i < transforms.length; i++) { + const t = transforms[i] + if (!Array.isArray(t) || t.length === 0) { + throw new TypeError(`[nuxt][image][directus] invalid transform at index ${i}.`) + } + if (typeof t[0] !== 'string') { + throw new TypeError(`[nuxt][image][directus] transform at index ${i} must start with string.`) + } + } + return JSON.stringify(transforms) +} + +const buildQueryParams = createOperationsGenerator({ + keyMap: (key: any) => key, // identity map for Directus keys + formatter: (key: string, value: any) => + key === 'transforms' ? `transforms=${value}` : `${key}=${value}`, +}) + +function buildQuery(modifiers: DirectusOptions['modifiers'] = {}) { + if ('key' in modifiers && modifiers.key) { + return `key=${encodeURIComponent(modifiers.key)}` } + const { transforms, ...rest } = modifiers as Exclude + const validatedTransforms = validateTransforms(transforms) + return buildQueryParams({ + ...rest, + ...(validatedTransforms ? { transforms: validatedTransforms } : {}), + }) } export default defineProvider({ getImage: (src, { modifiers, baseURL }) => { - // Separating the transforms from the rest of the modifiers - const transforms = modifiers.transforms && Array.isArray(modifiers.transforms) && modifiers.transforms.length > 0 - ? JSON.stringify(Array.from(new Set(modifiers.transforms.map(obj => JSON.stringify(obj)))).map(value => JSON.parse(value))) - : undefined - - const operations = operationsGenerator({ - ...modifiers, - transforms, - }) + console.log('modifiers: ', modifiers) + const query = buildQuery(modifiers) return { - url: joinURL(baseURL, src + (operations ? ('?' + operations.replace(/=+$/, '')) : '')), + url: joinURL(baseURL, query ? `${src}?${query}` : src), } }, }) From 6c4ff0ad9399eb80d58e674e2cd3b9a0480128ce Mon Sep 17 00:00:00 2001 From: Kevin Heiner Date: Wed, 8 Apr 2026 20:10:42 -0700 Subject: [PATCH 02/10] Use Directus Sandbox Updated Directus playground/e2e_snapshots to use the Directus Sandbox. --- playground/app/providers.ts | 8 ++++---- playground/nuxt.config.ts | 2 +- test/e2e/__snapshots__/directus.json5 | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/playground/app/providers.ts b/playground/app/providers.ts index 694dff051..ad057ed36 100644 --- a/playground/app/providers.ts +++ b/playground/app/providers.ts @@ -1297,11 +1297,11 @@ export const providers: Provider[] = [ name: 'directus', samples: [ { - src: 'ad514db1-eb90-4523-8183-46781437e7ee', + src: '8f748634-d77b-4985-b27e-7e1f3559881a', alt: 'Image 1', }, { - src: 'ad514db1-eb90-4523-8183-46781437e7ee.jpg', + src: '8f748634-d77b-4985-b27e-7e1f3559881a.jpg', alt: '1024px width', width: 1024, height: 256, @@ -1309,13 +1309,13 @@ export const providers: Provider[] = [ modifiers: { withoutEnlargement: 'true' }, }, { - src: 'ad514db1-eb90-4523-8183-46781437e7ee', + src: '8f748634-d77b-4985-b27e-7e1f3559881a', alt: '256px width, webp', width: 256, format: 'webp', }, { - src: 'ad514db1-eb90-4523-8183-46781437e7ee', + src: '8f748634-d77b-4985-b27e-7e1f3559881a', alt: '256px width, webp', width: 256, format: 'webp', diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index c4dbf392d..b6fb20f55 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -67,7 +67,7 @@ export default defineNuxtConfig({ apiVersion: 'v7', }, directus: { - baseURL: 'http://localhost:8055/assets/', + baseURL: 'https://sandbox.directus.io/assets/', }, fastly: { baseURL: 'https://www.fastly.io', diff --git a/test/e2e/__snapshots__/directus.json5 b/test/e2e/__snapshots__/directus.json5 index 8dc47c7e1..3df46be4b 100644 --- a/test/e2e/__snapshots__/directus.json5 +++ b/test/e2e/__snapshots__/directus.json5 @@ -1,14 +1,14 @@ { "requests": [ - "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee", - "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover", - "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?width=256&format=webp", - "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?width=256&format=webp", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp", ], "sources": [ - "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee", - "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover", - "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?width=256&format=webp", - "http://localhost:8055/assets/ad514db1-eb90-4523-8183-46781437e7ee?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?width=256&format=webp", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp", ], -} \ No newline at end of file +} From ded484339518aa0341e430ff9d7d178ba4085a94 Mon Sep 17 00:00:00 2001 From: Kevin Heiner Date: Wed, 8 Apr 2026 23:12:28 -0700 Subject: [PATCH 03/10] feat: Add `key`ed transform functionality, add types for Sharp Transforms Fixes #2194 * Implements the ability to use a `key` to use a preconfigured Directus Transform set. * Adds Types for Sharp Transforms [intentionally omitted resizing since the basic functionality is abstracted in the nuxt/image module] * Fixes issue of incorrect typing for transforms to allow for `[string, ...any[]]` (was incorrectly typed as `string[]`) --- src/runtime/providers/directus.ts | 78 +++++++++++++------------------ 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/src/runtime/providers/directus.ts b/src/runtime/providers/directus.ts index 141b4369e..9bfcb2c0d 100644 --- a/src/runtime/providers/directus.ts +++ b/src/runtime/providers/directus.ts @@ -1,13 +1,12 @@ /** -* SharpOperations -* [ ] Resize // These operations are advanced and add a lot of complexity to the code base; they are already abstracted in Directus to be simpler -* -* [X] Image -* [X] Color -* [X] Channel -* -*/ - + * SharpOperations + * [ ] Resize // These operations are advanced and add a lot of complexity to the code base; they are already abstracted in Directus to be simpler + * + * [X] Image + * [X] Color + * [X] Channel + * + */ import { joinURL } from 'ufo' import { createOperationsGenerator } from '../utils/index' @@ -110,49 +109,38 @@ interface DirectusOptions { modifiers?: DirectusModifiers } -function validateTransforms( - transforms?: SharpOperation[], -): string | undefined { - if (!transforms) return - if (!Array.isArray(transforms)) { - throw new TypeError('[nuxt][image][directus] transforms must be an array of tuples.') - } - for (let i = 0; i < transforms.length; i++) { - const t = transforms[i] - if (!Array.isArray(t) || t.length === 0) { - throw new TypeError(`[nuxt][image][directus] invalid transform at index ${i}.`) - } - if (typeof t[0] !== 'string') { - throw new TypeError(`[nuxt][image][directus] transform at index ${i} must start with string.`) - } - } - return JSON.stringify(transforms) -} - -const buildQueryParams = createOperationsGenerator({ - keyMap: (key: any) => key, // identity map for Directus keys - formatter: (key: string, value: any) => - key === 'transforms' ? `transforms=${value}` : `${key}=${value}`, +// HACK: See Discussion #2206 +const operationsGenerator = createOperationsGenerator({ + valueMap: { + transforms(value: SharpOperation[]) { + return value.length > 0 + ? JSON.stringify( + Array.from(new Set(value.map(v => JSON.stringify(v)))) + .map(v => JSON.parse(v)), + ) + : undefined + }, + }, }) -function buildQuery(modifiers: DirectusOptions['modifiers'] = {}) { - if ('key' in modifiers && modifiers.key) { - return `key=${encodeURIComponent(modifiers.key)}` - } - const { transforms, ...rest } = modifiers as Exclude - const validatedTransforms = validateTransforms(transforms) - return buildQueryParams({ - ...rest, - ...(validatedTransforms ? { transforms: validatedTransforms } : {}), - }) +function isKeyModifier( + mod: DirectusModifiers | undefined, +): mod is { key: string } { + return !!mod && 'key' in mod && typeof mod.key === 'string' } export default defineProvider({ getImage: (src, { modifiers, baseURL }) => { - console.log('modifiers: ', modifiers) - const query = buildQuery(modifiers) + if (isKeyModifier(modifiers)) { + return { + url: joinURL(baseURL, src + `?key=${modifiers.key}`), + } + } + + const operations = operationsGenerator(modifiers) + return { - url: joinURL(baseURL, query ? `${src}?${query}` : src), + url: joinURL(baseURL, src + (operations ? `?${operations}` : '')), } }, }) From 0821b246ab865113b1c3716e9e16e106e2ce26f0 Mon Sep 17 00:00:00 2001 From: Kevin Heiner Date: Thu, 9 Apr 2026 01:46:36 -0700 Subject: [PATCH 04/10] test: fix whitespace in snapshot for directus Whitespace was causing tests to fail; --- test/e2e/__snapshots__/directus.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/__snapshots__/directus.json5 b/test/e2e/__snapshots__/directus.json5 index 3df46be4b..65c44cfce 100644 --- a/test/e2e/__snapshots__/directus.json5 +++ b/test/e2e/__snapshots__/directus.json5 @@ -11,4 +11,4 @@ "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?width=256&format=webp", "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp", ], -} +} \ No newline at end of file From 4a8caa1e5a51583ad24e370356bc48ffa9f748b6 Mon Sep 17 00:00:00 2001 From: Kevin Heiner Date: Thu, 9 Apr 2026 04:38:52 -0700 Subject: [PATCH 05/10] test: Add Directus Tests * Sort import for `providers.test.ts` (NOTE: PR #2193 greatly improves test and readability) * Add E2E snapshot for `key` modifier. * Fix missing test in `providers.test.ts` * Add `key` test for `providers.test.ts` --- playground/app/providers.ts | 9 ++++- test/e2e/__snapshots__/directus.json5 | 2 + test/nuxt/providers.test.ts | 53 ++++++++++++++++++++------- test/providers.ts | 12 +++--- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/playground/app/providers.ts b/playground/app/providers.ts index ad057ed36..b42b23a80 100644 --- a/playground/app/providers.ts +++ b/playground/app/providers.ts @@ -1298,7 +1298,7 @@ export const providers: Provider[] = [ samples: [ { src: '8f748634-d77b-4985-b27e-7e1f3559881a', - alt: 'Image 1', + alt: 'Raw Image (no modifiers or transforms)', }, { src: '8f748634-d77b-4985-b27e-7e1f3559881a.jpg', @@ -1314,6 +1314,13 @@ export const providers: Provider[] = [ width: 256, format: 'webp', }, + { + src: '8f748634-d77b-4985-b27e-7e1f3559881a', + alt: 'keyed transform (all other inputs ignored)', + format: 'tiff', + modifiers: { key: 'system-large-cover' }, + }, + { src: '8f748634-d77b-4985-b27e-7e1f3559881a', alt: '256px width, webp', diff --git a/test/e2e/__snapshots__/directus.json5 b/test/e2e/__snapshots__/directus.json5 index 65c44cfce..b56885374 100644 --- a/test/e2e/__snapshots__/directus.json5 +++ b/test/e2e/__snapshots__/directus.json5 @@ -2,6 +2,7 @@ "requests": [ "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a", "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?key=system-large-cover", "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?width=256&format=webp", "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp", ], @@ -9,6 +10,7 @@ "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a", "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a.jpg?withoutEnlargement=true&width=1024&height=256&fit=cover", "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?width=256&format=webp", + "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?key=system-large-cover", "https://sandbox.directus.io/assets/8f748634-d77b-4985-b27e-7e1f3559881a?withoutEnlargement=true&transforms=%5B%5B%22blur%22%2C4%5D%2C%5B%22negate%22%5D%5D&width=256&format=webp", ], } \ No newline at end of file diff --git a/test/nuxt/providers.test.ts b/test/nuxt/providers.test.ts index b7a284636..f78e4d916 100644 --- a/test/nuxt/providers.test.ts +++ b/test/nuxt/providers.test.ts @@ -5,41 +5,42 @@ import { images } from '../providers' import { useNuxtApp } from '#imports' import ipx from '../../dist/runtime/providers/ipx' import none from '../../dist/runtime/providers/none' -import weserv from '../../dist/runtime/providers/weserv' import aliyun from '../../dist/runtime/providers/aliyun' import awsAmplify from '../../dist/runtime/providers/awsAmplify' +import builderio from '../../dist/runtime/providers/builderio' import cloudflare from '../../dist/runtime/providers/cloudflare' import cloudflareimages from '../../dist/runtime/providers/cloudflareimages' +import cloudimage from '../../dist/runtime/providers/cloudimage' import cloudinary from '../../dist/runtime/providers/cloudinary' -import twicpics from '../../dist/runtime/providers/twicpics' +import contentful from '../../dist/runtime/providers/contentful' +import directus from '../../dist/runtime/providers/directus' import fastly from '../../dist/runtime/providers/fastly' -import picsum from '../../dist/runtime/providers/picsum' -import prepr from '../../dist/runtime/providers/prepr' +import flyimg from '../../dist/runtime/providers/flyimg' import glide from '../../dist/runtime/providers/glide' -import imgix from '../../dist/runtime/providers/imgix' import gumlet from '../../dist/runtime/providers/gumlet' +import hygraph from '../../dist/runtime/providers/hygraph' import imageengine from '../../dist/runtime/providers/imageengine' -import unsplash from '../../dist/runtime/providers/unsplash' import imagekit from '../../dist/runtime/providers/imagekit' +import imgix from '../../dist/runtime/providers/imgix' import netlifyImageCdn from '../../dist/runtime/providers/netlifyImageCdn' import netlifyLargeMedia from '../../dist/runtime/providers/netlifyLargeMedia' +import picsum from '../../dist/runtime/providers/picsum' +import prepr from '../../dist/runtime/providers/prepr' import prismic from '../../dist/runtime/providers/prismic' import sanity from '../../dist/runtime/providers/sanity' import shopify from '../../dist/runtime/providers/shopify' -import builderio from '../../dist/runtime/providers/builderio' -import contentful from '../../dist/runtime/providers/contentful' -import cloudimage from '../../dist/runtime/providers/cloudimage' +import sirv from '../../dist/runtime/providers/sirv' import storyblok from '../../dist/runtime/providers/storyblok' import strapi from '../../dist/runtime/providers/strapi' import strapi5 from '../../dist/runtime/providers/strapi5' import supabase from '../../dist/runtime/providers/supabase' +import twicpics from '../../dist/runtime/providers/twicpics' +import umbraco from '../../dist/runtime/providers/umbraco' +import unsplash from '../../dist/runtime/providers/unsplash' +import uploadcare from '../../dist/runtime/providers/uploadcare' import vercel from '../../dist/runtime/providers/vercel' import wagtail from '../../dist/runtime/providers/wagtail' -import uploadcare from '../../dist/runtime/providers/uploadcare' -import sirv from '../../dist/runtime/providers/sirv' -import hygraph from '../../dist/runtime/providers/hygraph' -import umbraco from '../../dist/runtime/providers/umbraco' -import flyimg from '../../dist/runtime/providers/flyimg' +import weserv from '../../dist/runtime/providers/weserv' function getEmptyContext() { return { @@ -546,6 +547,30 @@ describe('Providers', () => { } }) + it('directus', () => { + const providerOptions = { + baseURL: '/assets/', + } + for (const image of images) { + const [src, modifiers] = image.args + const generated = directus().getImage(src, { modifiers, ...providerOptions }, getEmptyContext()) + expect(generated).toMatchObject(image.directus) + } + }) + + it('directus w/ key modifier', () => { + const providerOptions = { + baseURL: '/assets/', + width: 200, + modifiers: { + key: 'system-small-cover', + allOtherModifiers: 'ignored', + }, + } + const generated = directus().getImage('uuid-example', { ...providerOptions }, getEmptyContext()) + expect(generated.url).toBe('/assets/uuid-example?key=system-small-cover') + }) + it('cloudimage', () => { const providerOptions = { token: 'demo', diff --git a/test/providers.ts b/test/providers.ts index bc276ebc3..395684f1d 100644 --- a/test/providers.ts +++ b/test/providers.ts @@ -32,7 +32,7 @@ export const images = [ supabase: { url: 'https://ovzjdhllnxrizgszqlsi.supabase.co/storage/v1/render/image/public/nuxt/test.png' }, vercel: { url: '/_vercel/image?url=%2Ftest.png&w=1536&q=100' }, wagtail: { url: '329944/original|format-webp|webpquality-70' }, - directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4' }, + directus: { url: '/assets/test.png' }, uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/' }, weserv: { url: 'https://wsrv.nl/?filename=test.png&we&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, flyimg: { url: 'https://demo.flyimg.io/upload/-/https://my-website.com/test.png' }, @@ -77,7 +77,7 @@ export const images = [ supabase: { url: 'https://ovzjdhllnxrizgszqlsi.supabase.co/storage/v1/render/image/public/nuxt/test.png?width=200' }, vercel: { url: '/_vercel/image?url=%2Ftest.png&w=640&q=100' }, wagtail: { url: '329944/width-200|format-webp|webpquality-70' }, - directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200' }, + directus: { url: '/assets/test.png?width=200' }, uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/resize/200x/' }, weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, flyimg: { url: 'https://demo.flyimg.io/upload/w_200/https://my-website.com/test.png' }, @@ -121,7 +121,7 @@ export const images = [ supabase: { url: 'https://ovzjdhllnxrizgszqlsi.supabase.co/storage/v1/render/image/public/nuxt/test.png?height=200' }, vercel: { url: '/_vercel/image?url=%2Ftest.png&w=1536&q=100' }, wagtail: { url: '329944/height-200|format-webp|webpquality-70' }, - directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?height=200' }, + directus: { url: '/assets/test.png?height=200' }, uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/resize/x200/' }, weserv: { url: 'https://wsrv.nl/?filename=test.png&we&h=200&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, flyimg: { url: 'https://demo.flyimg.io/upload/h_200/https://my-website.com/test.png' }, @@ -165,7 +165,7 @@ export const images = [ supabase: { url: 'https://ovzjdhllnxrizgszqlsi.supabase.co/storage/v1/render/image/public/nuxt/test.png?width=200&height=200' }, vercel: { url: '/_vercel/image?url=%2Ftest.png&w=640&q=100' }, wagtail: { url: '329944/fill-200x200-c0|format-webp|webpquality-70' }, - directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200&height=200' }, + directus: { url: '/assets/test.png?width=200&height=200' }, uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/resize/200x200/' }, weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, flyimg: { url: 'https://demo.flyimg.io/upload/w_200,h_200/https://my-website.com/test.png' }, @@ -209,7 +209,7 @@ export const images = [ supabase: { url: 'https://ovzjdhllnxrizgszqlsi.supabase.co/storage/v1/render/image/public/nuxt/test.png?width=200&height=200&resize=contain' }, vercel: { url: '/_vercel/image?url=%2Ftest.png&w=640&q=100' }, wagtail: { url: '329944/fill-200x200-c0|format-webp|webpquality-70' }, - directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200&height=200&fit=contain' }, + directus: { url: '/assets/test.png?width=200&height=200&fit=contain' }, uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/resize/200x200/-/stretch/off/' }, weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&fit=contain&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, flyimg: { url: 'https://demo.flyimg.io/upload/w_200,h_200/https://my-website.com/test.png' }, @@ -253,7 +253,7 @@ export const images = [ supabase: { url: 'https://ovzjdhllnxrizgszqlsi.supabase.co/storage/v1/render/image/public/nuxt/test.png?width=200&height=200&resize=contain&format=jpeg' }, vercel: { url: '/_vercel/image?url=%2Ftest.png&w=640&q=100' }, wagtail: { url: '329944/fill-200x200-c0|format-jpeg|jpegquality-70' }, - directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200&height=200&fit=contain&format=jpg' }, + directus: { url: '/assets/test.png?width=200&height=200&fit=contain&format=jpeg' }, uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/format/jpeg/-/resize/200x200/-/stretch/off/' }, weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&fit=contain&output=jpg&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, flyimg: { url: 'https://demo.flyimg.io/upload/w_200,h_200,o_jpeg/https://my-website.com/test.png' }, From f5695baf28001004b03ba60747aef795606109b9 Mon Sep 17 00:00:00 2001 From: Kevin Heiner Date: Thu, 9 Apr 2026 04:40:50 -0700 Subject: [PATCH 06/10] docs: Update Directus Docs Updated Docs to include: * `key` :modifier * Explain Sharp Transforms and provide examples. --- docs/content/3.providers/directus.md | 102 ++++++++++++++++++++------- 1 file changed, 75 insertions(+), 27 deletions(-) diff --git a/docs/content/3.providers/directus.md b/docs/content/3.providers/directus.md index 9e5f2321b..0c7c940b4 100644 --- a/docs/content/3.providers/directus.md +++ b/docs/content/3.providers/directus.md @@ -8,9 +8,7 @@ links: size: xs --- -Integration between [Directus](https://directus.io) and the image module. - -To use this provider you just need to specify the base URL of your project. +To use this provider you only need to specify the base URL of your Directus instance. ```ts [nuxt.config.ts] export default defineNuxtConfig({ @@ -23,51 +21,101 @@ export default defineNuxtConfig({ }) ``` -You can easily override default options: - -```ts [nuxt.config.ts] -export default defineNuxtConfig({ - image: { - directus: { - baseURL: 'http://mydirectus-domain.com/assets', - modifiers: { - withoutEnlargement: 'true', - transforms: [['blur', 4], ['negate']] - } - } - } -}) -``` - -## Modifiers +## Props -All the default modifiers from [Directus documentation](https://docs.directus.io/reference/files.html#requesting-a-thumbnail) are available. +The NuxtImg Props map cleanly to the Directus Transforms. ```vue ``` -Since Directus exposes the full [sharp API](https://sharp.pixelplumbing.com/api-operation) through the `transforms` parameter, we can use it inside our `modifiers` prop: +### Modifiers + +The `modifiers` object is used for Directus specific features. All modifiers are optional. + +#### Examples of Modifiers + +::tabs{.w-full} +:::tabs-item{label="Modifiers"} ```vue ``` -::note -Note that the `transforms` parameter, as stated in the [Directus documentation](https://docs.directus.io/reference/files.html#advanced-transformations), is basically a list of lists. This is to facilitate the use of those sharp functions, like [`normalise`](https://sharp.pixelplumbing.com/api-operation#normalise), that would need multiple values in input: `transforms: [['normalise', 1, 99], ['blur', 4], ['negate']]`. +::: + +:::tabs-item{label="Keyed Modifier"} + +```vue + +``` + +::: +:: + +#### All Modifiers + +::field-group + +::field{name="withoutEnlargement" type="boolean"} +Disable automatically upscaling the image when true. +:: + +::field{name="transforms" type="['string', ...any][]"} +A pipeline of transforms to tell Directus how to modify the image before sending. +:::collapsible + +| **[Sharp Operation](https://sharp.pixelplumbing.com/api-operation/)**| **Options**| **Example Usage**| +|----------------------|--------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| +| **rotate**| \[angle?: number, options?: \{ background: Color \} \]| \['rotate', 90\] or \['rotate', 90, \{ background: 'white' \} \]| +| **flip**| \[]| \['flip'\]| +| **flop**| \[]| \['flop'\]| +| **sharpen**| \[sigma?: number\] \| \[options?: \{ sigma?: number, m1?: number, m2?: number, x1?: number, y2?: number, y3?: number \} \]| \['sharpen', 1.5\] or \['sharpen', \{ sigma: 1.5, m1: 0.5 \} \]| +| **median**| \[size?: number\]| \['median', 5\]| +| **blur**| \[sigma?: number\] \| \[options?: \{ sigma?: number, precision?: 'integer' \| 'float' \| 'approximate', minAmplitude?: number \} \]| \['blur', 2\] or \['blur', \{ sigma: 2, precision: 'float' \} \]| +| **flatten**| \[options?: \{ background: Color \} \]| \['flatten', \{ background: 'black' \} \]| +| **unflatten**| \[]| \['unflatten'\]| +| **gamma**| \[gamma?: number, gammaOut?: number\]| \['gamma', 2.2\] or \['gamma', 2.2, 1.8\]| +| **negate**| \[options?: \{ alpha?: boolean \} \]| \['negate', \{ alpha: true \} \]| +| **normalize** or **normalise**| \[lower?: number, upper?: number\]| \['normalize', 0, 255\]| +| **clahe**| \[options?: \{ width: number, height: number, maxSlope?: number \} \]| \['clahe', \{ width: 8, height: 8 \} \]| +| **convolve**| \[kernel: \{ width: number, height: number, kernel: number\[], offset?: number \} \]| \['convolve', \{ width: 3, height: 3, kernel: [1, 1, 1, 1, 1, 1, 1, 1, 1], offset: 0 \} \]| +| **threshold**| \[value?: number, options?: \{ grayscale?: boolean \} \]| \['threshold', 128\] or \['threshold', 128, \{ grayscale: true \} \]| +| **linear**| \[a?: number \| number\[], b?: number \| number\[]\]| \['linear', 1.2, 0\] or \['linear', [1.2, 1.0], [0, 255]\]| +| **recomb**| \[matrix: number\[\]\[\]\]| \['recomb', \[\[0.5, 0.5, 0.5\], [0.5, 0.5, 0.5\], [0.5, 0.5, 0.5\]\] \]| +| **modulate**| \[options?: \{ brightness?: number, saturation?: number, hue?: number, lightness?: number \} \]| \['modulate', \{ brightness: 1.2, saturation: 1.5 \} \]| +| **tint**| \[color: Color\]| \['tint', 'red'\]| +| **grayscale** or **greyscale**| \[]| \['grayscale'\]| +| **pipelineColorspace** or **pipelineColourspace**| \[colorspace: SharpColorspace\]| \['pipelineColorspace', 'srgb'\]| +| **toColorspace** or **toColourspace**| \[colorspace: SharpColorspace\]| \['toColorspace', 'rgb'\]| +| **removeAlpha**| \[]| \['removeAlpha'\]| +| **ensureAlpha**| \[alpha?: number\]| \['ensureAlpha', 0.5\]| +| **extractChannel**| \[channel: 'red' \| 'green' \| 'blue' \| 'alpha'\]| \['extractChannel', 'red'\]| + +::: + +:::note +Directus defaults `ASSETS_TRANSFORM_MAX_OPERATIONS` to `5`. If you need more, it is recommended that you utilize a `key`ed transform. You can modify your [Directus Configuration](https://directus.io/docs/configuration/files#assets) to accommodate more transforms if necessary. +::: + +::field{name="key" type="string"} +Sets a unique identifier for allowing faster and easier image transformation requests. :: From ae9d4e999088992d55d6f2c76a0f53fd1591ecc3 Mon Sep 17 00:00:00 2001 From: kheiner Date: Thu, 9 Apr 2026 04:51:43 -0700 Subject: [PATCH 07/10] Apply suggestion from @kheiner Remove unnecessary comments --- src/runtime/providers/directus.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/runtime/providers/directus.ts b/src/runtime/providers/directus.ts index 9bfcb2c0d..42d3bf168 100644 --- a/src/runtime/providers/directus.ts +++ b/src/runtime/providers/directus.ts @@ -1,13 +1,3 @@ -/** - * SharpOperations - * [ ] Resize // These operations are advanced and add a lot of complexity to the code base; they are already abstracted in Directus to be simpler - * - * [X] Image - * [X] Color - * [X] Channel - * - */ - import { joinURL } from 'ufo' import { createOperationsGenerator } from '../utils/index' import { defineProvider } from '../utils/provider' From bd516e23900a4f03e002897055461efb8ac8e5a0 Mon Sep 17 00:00:00 2001 From: K Heiner Date: Thu, 14 May 2026 04:44:20 -0700 Subject: [PATCH 08/10] Apply suggestion from @coderabbitai --- docs/content/3.providers/directus.md | 10 +++++----- playground/app/providers.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/content/3.providers/directus.md b/docs/content/3.providers/directus.md index 0c7c940b4..86d6b6bae 100644 --- a/docs/content/3.providers/directus.md +++ b/docs/content/3.providers/directus.md @@ -38,7 +38,7 @@ The NuxtImg Props map cleanly to the Directus Transforms. ### Modifiers -The `modifiers` object is used for Directus specific features. All modifiers are optional. +The `modifiers` object is used for Directus-specific features. All modifiers are optional. #### Examples of Modifiers @@ -49,7 +49,7 @@ The `modifiers` object is used for Directus specific features. All modifiers are @@ -63,7 +63,7 @@ The `modifiers` object is used for Directus specific features. All modifiers are ``` @@ -79,7 +79,7 @@ The `modifiers` object is used for Directus specific features. All modifiers are Disable automatically upscaling the image when true. :: -::field{name="transforms" type="['string', ...any][]"} +::field{name="transforms" type="[string, ...any[]][]"} A pipeline of transforms to tell Directus how to modify the image before sending. :::collapsible @@ -117,5 +117,5 @@ Directus defaults `ASSETS_TRANSFORM_MAX_OPERATIONS` to `5`. If you need more, it ::: ::field{name="key" type="string"} -Sets a unique identifier for allowing faster and easier image transformation requests. +Sets a unique identifier for a pre-configured Directus transform. When `key` is provided, do not combine it with other modifiers. :: diff --git a/playground/app/providers.ts b/playground/app/providers.ts index b42b23a80..e779f89e2 100644 --- a/playground/app/providers.ts +++ b/playground/app/providers.ts @@ -1306,7 +1306,7 @@ export const providers: Provider[] = [ width: 1024, height: 256, fit: 'cover', - modifiers: { withoutEnlargement: 'true' }, + modifiers: { withoutEnlargement: true }, }, { src: '8f748634-d77b-4985-b27e-7e1f3559881a', @@ -1326,7 +1326,7 @@ export const providers: Provider[] = [ alt: '256px width, webp', width: 256, format: 'webp', - modifiers: { withoutEnlargement: 'true', transforms: [['blur', 4], ['negate']] }, + modifiers: { withoutEnlargement: true, transforms: [['blur', 4], ['negate']] }, }, ], }, From 66ce0338e54d64c649c12b4a858cd87861be9670 Mon Sep 17 00:00:00 2001 From: kheiner Date: Tue, 2 Jun 2026 12:50:42 -0700 Subject: [PATCH 09/10] Apply suggestion from @coderabbitai Clarifies the NuxtImg Props mapping to Directus Transforms --- docs/content/3.providers/directus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/3.providers/directus.md b/docs/content/3.providers/directus.md index 86d6b6bae..032c17a62 100644 --- a/docs/content/3.providers/directus.md +++ b/docs/content/3.providers/directus.md @@ -23,7 +23,7 @@ export default defineNuxtConfig({ ## Props -The NuxtImg Props map cleanly to the Directus Transforms. +Supported NuxtImg props (such as `width`, `height`, `quality`, `format`, and `fit`) map to Directus transforms. ```vue Date: Tue, 21 Jul 2026 11:31:40 +0000 Subject: [PATCH 10/10] fix(directus): correct modifier types, map jpeg to jpg, encode key --- src/runtime/providers/directus.ts | 15 ++++++++------- test/providers.ts | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/runtime/providers/directus.ts b/src/runtime/providers/directus.ts index 42d3bf168..0321ae460 100644 --- a/src/runtime/providers/directus.ts +++ b/src/runtime/providers/directus.ts @@ -91,11 +91,6 @@ type DirectusModifiers interface DirectusOptions { baseURL: string - width?: number - height?: number - quality?: number - format?: 'auto' | 'jpg' | 'png' | 'webp' | 'tiff' | 'avif' - fit?: 'cover' | 'contain' | 'inside' | 'outside' | 'fill' modifiers?: DirectusModifiers } @@ -123,11 +118,17 @@ export default defineProvider({ getImage: (src, { modifiers, baseURL }) => { if (isKeyModifier(modifiers)) { return { - url: joinURL(baseURL, src + `?key=${modifiers.key}`), + url: joinURL(baseURL, src + `?key=${encodeURIComponent(modifiers.key)}`), } } - const operations = operationsGenerator(modifiers) + // Directus expects `jpg`; Nuxt Image normalises JPEG to `jpeg`. + const resolved = { + ...modifiers, + format: modifiers?.format === 'jpeg' ? 'jpg' : modifiers?.format, + } + + const operations = operationsGenerator(resolved) return { url: joinURL(baseURL, src + (operations ? `?${operations}` : '')), diff --git a/test/providers.ts b/test/providers.ts index 395684f1d..5032257b8 100644 --- a/test/providers.ts +++ b/test/providers.ts @@ -253,7 +253,7 @@ export const images = [ supabase: { url: 'https://ovzjdhllnxrizgszqlsi.supabase.co/storage/v1/render/image/public/nuxt/test.png?width=200&height=200&resize=contain&format=jpeg' }, vercel: { url: '/_vercel/image?url=%2Ftest.png&w=640&q=100' }, wagtail: { url: '329944/fill-200x200-c0|format-jpeg|jpegquality-70' }, - directus: { url: '/assets/test.png?width=200&height=200&fit=contain&format=jpeg' }, + directus: { url: '/assets/test.png?width=200&height=200&fit=contain&format=jpg' }, uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/format/jpeg/-/resize/200x200/-/stretch/off/' }, weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&fit=contain&output=jpg&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, flyimg: { url: 'https://demo.flyimg.io/upload/w_200,h_200,o_jpeg/https://my-website.com/test.png' },