diff --git a/.nuxtrc b/.nuxtrc index 1e1fe8339..59379b578 100644 --- a/.nuxtrc +++ b/.nuxtrc @@ -1 +1 @@ -setups.@nuxt/test-utils="4.0.0" \ No newline at end of file +setups.@nuxt/test-utils="4.0.0" diff --git a/docs/content/3.providers/imgproxy.md b/docs/content/3.providers/imgproxy.md new file mode 100644 index 000000000..b9b9b87d9 --- /dev/null +++ b/docs/content/3.providers/imgproxy.md @@ -0,0 +1,328 @@ +--- +title: Imgproxy +description: Nuxt Image has first class integration with Imgproxy. +links: + - label: Source + icon: i-simple-icons-github + to: https://github.com/nuxt/image/blob/main/src/runtime/providers/imgproxy.ts + size: xs +--- + +Integration between [Imgproxy](https://imgproxy.net/) and the image module. + +At a minimum, you must configure the `imgproxy` provider with the `baseURL`, `key` and `salt` set to your Imgproxy +instance: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + image: { + imgproxy: { + baseURL: 'http://localhost:8080/', + key: 'ee3b0e07dfc9ec20d5d9588a558753547a8a88c48291ae96171330daf4ce2800', + salt: '8dd0e39bb7b14eeaf02d49e5dc76d2bc0abd9e09d52e7049e791acd3558db68e', + } + } +}) +``` + +## Processing of `fit` values by the imgproxy provider + +The `fit` modifier controls how images are resized using **imgproxy**. +It maps to the following imgproxy-related fields: + +- `resizingType` — defines the resizing strategy (`fill`, `fit`, `force`) +- `extend` - enables canvas extension (letterboxing) when required + +The behavior depends on whether valid `width` and/or `height` values are provided. + +--- + +### Dimension Handling + +Before applying any `fit` behavior: + +- If both `width` and `height` are positive numbers → **both dimensions are considered defined**. +- If only one dimension is provided → behavior falls back to proportional resizing. +- If neither dimension is provided → resizing defaults to proportional behavior. + +Exact box-based behaviors (`cover`, `contain`, `fill`, `outside`) fully apply only when both dimensions are defined. + +--- + +### Supported `fit` Values + +#### `cover` + +**Description** + +Preserves an aspect ratio and ensures the image fully covers the target box. +When both dimensions are defined, parts of the image may be cropped. + +**Parameters Used** + +- `resizingType = 'fill'` when both dimensions are provided +- `resizingType = 'fit'` when one or both dimensions are missing + +**Behavior** + +- With both dimensions: full coverage of the box (cropping allowed). +- With partial or no dimensions: proportional resizing without enforced coverage. + +--- + +#### `contain` + +**Description** + +Preserves aspect ratio and fits the image within the target box. +When both dimensions are defined, padding (letterboxing) is applied so the final image matches the exact dimensions. + +**Parameters Used** + +- `resizingType = 'fit'` +- `extend = true` when both dimensions are provided +- `extend = false` (or omitted) when not + +**Behavior** + +- Proportional scaling within bounds. +- Padding is applied only when both width and height are defined. +- With a single dimension, padding is not applied. + +--- + +#### `fill` + +**Description** + +Ignores the original aspect ratio and stretches the image to exactly match the provided dimensions. + +**Parameters Used** + +- `resizingType = 'force'` when both dimensions are provided +- `resizingType = 'fit'` when one or both dimensions are missing + +**Behavior** + +- With both dimensions: image is stretched (aspect ratio ignored). +- Otherwise: proportional resizing is used instead of distortion. + +--- + +#### `inside` + +**Description** + +Preserves an aspect ratio and resizes the image to be as large as possible while ensuring its dimensions are less than or +equal to the specified box. + +**Parameters Used** + +- `resizingType = 'fit'` + +**Behavior** + +- Always performs proportional scaling. +- Works consistently with one, two, or no defined dimensions. + +--- + +#### `outside` + +**Description** + +Preserves an aspect ratio and resizes the image so that both dimensions are greater than or equal to the specified box. + +**Implementation Note** + +imgproxy does not provide a dedicated resizing strategy that guarantees true `outside` behavior without inspecting the +source image’s aspect ratio. +This implementation approximates the behavior. + +**Parameters Used** + +- `resizingType = 'fill'` when both dimensions are provided +- `resizingType = 'fit'` when one or both dimensions are missing + +**Behavior** + +- With both dimensions: behaves similarly to `cover` (may crop). +- Otherwise: falls back to proportional resizing. + +--- + +### Summary Table + +| fit value | Both Dimensions Provided | resizingType | extend | +|-----------|--------------------------|--------------|--------| +| cover | Yes | fill | — | +| cover | No | fit | — | +| contain | Yes | fit | true | +| contain | No | fit | false | +| fill | Yes | force | — | +| fill | No | fit | — | +| inside | Any | fit | — | +| outside | Yes | fill | — | +| outside | No | fit | — | + +--- + +### Dimension Edge Cases + +- If neither `width` nor `height` is provided, resizing defaults to proportional behavior. +- If only one dimension is provided, proportional scaling is applied. +- Exact box behaviors are guaranteed only when both dimensions are defined. + +## Imgproxy Modifiers + +By default, the Imgproxy provider has the following settings for modifiers + +```typescript +const defaultModifiers: Partial = { + resizingType: 'auto', + gravity: 'ce', + format: 'webp', +} + ``` + +If you want to change them, you can define them in your `nuxt.config.ts` file: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + image: { + imgproxy: { + baseURL: 'http://localhost:8080/', + key: 'ee3b0e07dfc9ec20d5d9588a558753547a8a88c48291ae96171330daf4ce2800', + salt: '8dd0e39bb7b14eeaf02d49e5dc76d2bc0abd9e09d52e7049e791acd3558db68e', + modifiers: { + resizingType: 'fit', + gravity: 'no', + format: 'png', + } + } + } +}) +``` + +In addition to the [standard modifiers](/usage/nuxt-img#modifiers), you can also use most +of [Imgproxy Options](https://docs.imgproxy.net/usage/processing#processing-options) by adding them to the `modifiers` +property with the following attribute names: + +- `format` +- `resizingType` +- `resize` +- `size` +- `minWidth` +- `minHeight` +- `zoom` +- `dpr` +- `enlarge` +- `extend` +- `extendAspectRatio` +- `gravity` +- `crop` +- `autoRotate` +- `rotate` +- `background` +- `sharpen` +- `pixelate` +- `stripMetadata` +- `keepCopyright` +- `stripColorProfile` +- `enforceThumbnail` +- `maxBytes` +- `raw` +- `cachebuster` +- `expires` +- `filename` +- `returnAttachment` +- `preset` +- `maxSrcResolution` +- `maxSrcFileSize` +- `maxAnimationFrames` +- `maxAnimationFrameResolution` +- `maxResultDimension` + +> The provider does not verify the accuracy of your data entry. If you see “Invalid URL” from Imgproxy, check that the +> parameters are correct. Some parameters (e.g., `crop`) accept an object as input and convert the request into a valid +> string for your server. You can find more detailed information about all image processing options on +> the [Imgproxy](https://docs.imgproxy.net/usage/processing#processing-options) website. + +## Examples + +Example 1: Cropping an image to a width and height of 500x500 and rotate by 180 degrees: + +```vue + + +``` + +Example 2: Add blur to an image: + +```vue + + +``` + +Example 3: Using [presets](https://docs.imgproxy.net/configuration/options#presets): + +```vue + + +``` + +Example 4: Advanced image manipulation: + +```vue + + +``` + +### Contributing + +When developing this provider, a locally installed version of imgproxy was used with the following settings: + +```yaml [docker-compose.yml] + +services: + app: + image: ghcr.io/imgproxy/imgproxy + environment: + - IMGPROXY_KEY=ee3b0e07dfc9ec20d5d9588a558753547a8a88c48291ae96171330daf4ce2800 + - IMGPROXY_SALT=8dd0e39bb7b14eeaf02d49e5dc76d2bc0abd9e09d52e7049e791acd3558db68e + - IMGPROXY_PRESETS=default=resizing_type:fill/enlarge:1,sharp=sharpen:0.7,blurry=blur:100 + ports: + - 8080:8080 +``` + + + diff --git a/package.json b/package.json index 9ac4e3e91..d1a15d1c9 100755 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "test:built:types": "nuxt typecheck playground && nuxt typecheck example" }, "dependencies": { + "@noble/hashes": "^1.8.0", "@nuxt/kit": "^4.3.1", "consola": "^3.4.2", "defu": "^6.1.4", diff --git a/playground/app/providers.ts b/playground/app/providers.ts index 6417d39fb..e4fb4002b 100644 --- a/playground/app/providers.ts +++ b/playground/app/providers.ts @@ -437,6 +437,67 @@ export const providers: Provider[] = [ }, ], }, + // imgproxy + { + name: 'imgproxy', + samples: [ + { + src: 'https://mars.nasa.gov/system/downloadable_items/39099_Mars-MRO-orbiter-fresh-crater-sirenum-fossae.jpg', + width: 300, + height: 300, + }, + { + src: 'https://mars.nasa.gov/system/downloadable_items/39099_Mars-MRO-orbiter-fresh-crater-sirenum-fossae.jpg', + width: 300, + height: 300, + quality: 10, + }, + { + src: 'https://mars.nasa.gov/system/downloadable_items/39099_Mars-MRO-orbiter-fresh-crater-sirenum-fossae.jpg', + background: 'FFCC00', + modifiers: { + resize: 'fit:500:500:1:1', + }, + }, + { + src: 'https://mars.nasa.gov/system/downloadable_items/39099_Mars-MRO-orbiter-fresh-crater-sirenum-fossae.jpg', + width: 500, + height: 500, + modifiers: { + rotate: 180, + }, + }, + { + src: 'https://mars.nasa.gov/system/downloadable_items/39099_Mars-MRO-orbiter-fresh-crater-sirenum-fossae.jpg', + width: 500, + height: 500, + modifiers: { + blur: 100, + }, + }, + { + src: 'https://mars.nasa.gov/system/downloadable_items/39099_Mars-MRO-orbiter-fresh-crater-sirenum-fossae.jpg', + width: 500, + height: 500, + modifiers: { + dpr: 0.1, + extend: true, + extendAspectRatio: '1:no:0:1', + rotate: 180, + background: '255:255:0', + sharpen: 10, + pixelate: 10, + stripMetadata: true, + keepCopyright: false, + stripColorProfile: true, + maxBytes: 10, + cachebuster: 'test', + expires: 4106340630, + filename: 'test', + }, + }, + ], + }, // imgix { name: 'imgix', diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 9e9db4485..81e0febfa 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -82,6 +82,11 @@ export default defineNuxtConfig({ imgix: { baseURL: 'https://assets.imgix.net', }, + imgproxy: { + baseURL: 'http://localhost:8080', + key: 'ee3b0e07dfc9ec20d5d9588a558753547a8a88c48291ae96171330daf4ce2800', + salt: '8dd0e39bb7b14eeaf02d49e5dc76d2bc0abd9e09d52e7049e791acd3558db68e', + }, imagekit: { baseURL: 'https://ik.imagekit.io/demo', }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df87bdfec..045857d2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: .: dependencies: + '@noble/hashes': + specifier: ^1.8.0 + version: 1.8.0 '@nuxt/kit': specifier: ^4.3.1 version: 4.3.1(magicast@0.5.1) @@ -1304,6 +1307,10 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -9158,6 +9165,8 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 diff --git a/src/provider.ts b/src/provider.ts index e41451151..edbec91cd 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -32,6 +32,7 @@ export const BuiltInProviders = [ 'imageengine', 'imagekit', 'imgix', + 'imgproxy', 'ipx', 'ipxStatic', 'netlify', diff --git a/src/runtime/providers/imgproxy.ts b/src/runtime/providers/imgproxy.ts new file mode 100644 index 000000000..c90e01775 --- /dev/null +++ b/src/runtime/providers/imgproxy.ts @@ -0,0 +1,290 @@ +import { joinURL } from 'ufo' +import { createOperationsGenerator } from '../utils/index' +import { defineProvider } from '../utils/provider' +import { hmac } from '@noble/hashes/hmac.js' +import { sha256 } from '@noble/hashes/sha2.js' +import { defu } from 'defu' +import type { ImageModifiers } from '@nuxt/image' + +// https://docs.imgproxy.net/usage/processing#resizing-type +export type ImgproxyResizingType = 'fit' | 'fill' | 'fill-down' | 'force' | 'auto' + +export type ImgproxyGravityType = 'ce' | 'no' | 'so' | 'ea' | 'we' | 'noea' | 'nowe' | 'soea' | 'sowe' + +export interface ImgproxyCrop { + width: number + height: number + gravity?: ImgproxyGravityType +} + +export type ImgproxyFormat = 'webp' | 'png' | 'jpg' | 'jpeg' | 'jxl' | 'avif' | 'gif' | 'ico' | 'svg' | 'heic' | 'bmp' | 'tiff' | 'pdf' | 'psd' | 'mp4' + +export type ImgproxyBooleanPrimitive = string | number | boolean | 't' + +export interface ImgproxyModifiers extends Omit { + width: number + height: number + format: ImgproxyFormat + fit: 'cover' | 'contain' | 'fill' | 'inside' | 'outside' + resizingType: ImgproxyResizingType + resize: string + size: string + minWidth: number + minHeight: number + zoom: string | number + dpr: number + enlarge: boolean + extend: boolean + extendAspectRatio: string + gravity: ImgproxyGravityType | string + crop: ImgproxyCrop + autoRotate: boolean + rotate: number + background: string + sharpen: number + pixelate: number + stripMetadata: boolean + keepCopyright: boolean + stripColorProfile: boolean + enforceThumbnail: boolean + maxBytes: number + raw: boolean + cachebuster: string + expires: number + filename: string + returnAttachment: boolean + preset: string + maxSrcResolution: number + maxSrcFileSize: number + maxAnimationFrames: number + maxAnimationFrameResolution: string + maxResultDimension: string +} + +interface ImgproxyOptions { + baseURL: string + salt: string + key: string + modifiers?: Partial +} + +/** + * Convert boolean, number, string or ImgproxyCrop to Imgproxy boolean primitive + * + * @param value + */ +const booleanMap = (value: string | number | boolean | ImgproxyCrop | ImgproxyBooleanPrimitive): number => { + if (typeof value === 'boolean') { + return value ? 1 : 0 + } + + if (typeof value === 'object') { + return 0 + } + + switch (value) { + case 't': + case 1: + case 'true': + return 1 + default: + return 0 + } +} + +// https://docs.imgproxy.net/usage/processing +const operationsGenerator = createOperationsGenerator({ + keyMap: { + resize: 'rs', + size: 's', + resizingType: 'rt', + width: 'w', + height: 'h', + minWidth: 'mw', + minHeight: 'mh', + zoom: 'z', + dpr: 'dpr', + enlarge: 'el', + extend: 'ex', + extendAspectRatio: 'exar', + gravity: 'g', + crop: 'c', + autoRotate: 'ar', + rotate: 'rot', + background: 'bg', + blur: 'bl', + sharpen: 'sh', + pixelate: 'pix', + stripMetadata: 'sm', + keepCopyright: 'kcr', + stripColorProfile: 'scp', + enforceThumbnail: 'eth', + quality: 'q', + maxBytes: 'mb', + format: 'f', + raw: 'raw', + cachebuster: 'cb', + expires: 'exp', + filename: 'fn', + returnAttachment: 'att', + preset: 'pr', + maxSrcResolution: 'msr', + maxSrcFileSize: 'msfs', + maxAnimationFrames: 'maf', + maxAnimationFrameResolution: 'mafr', + maxResultDimension: 'mrd', + }, + valueMap: { + /** + * Converting the image cropping configuration object into primitives for imgproxy. + * + * @param value + */ + crop: (value: string | number | boolean | ImgproxyCrop | ImgproxyBooleanPrimitive): string => { + if (typeof value === 'string') { + return value + } + + if (typeof value === 'object') { + return `${value.width}:${value.height}${value?.gravity ? `:${value.gravity}` : ''}` + } + + throw new Error('Wrong crop format') + }, + enlarge: booleanMap, + extend: booleanMap, + autoRotate: booleanMap, + stripMetadata: booleanMap, + keepCopyright: booleanMap, + stripColorProfile: booleanMap, + enforceThumbnail: booleanMap, + raw: booleanMap, + returnAttachment: booleanMap, + /** + * Checking and calculating only permitted rotation angles. Only positive numbers in the range from 0 to 360 are permitted. + * The function converts the value to the nearest smaller value. + * + * @see https://docs.imgproxy.net/usage/processing#rotate + * + * @param value + */ + rotate: (value): number => { + if (typeof value !== 'number' || value < 0 || value > 359) { + throw new TypeError('Wrong rotate format') + } + + return value - (value % 90) + }, + }, + formatter: (key, value) => `${key}:${value}`, + joinWith: '/', +}) + +/** + * Convert hex string to Uint8Array + * + * @param hex + */ +function hexToBytes(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2) + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16) + } + return bytes +} + +/** + * Convert Uint8Array or string to URL-safe base64 + * + * @param input + */ +function urlSafeBase64(input: string | Uint8Array): string { + const bytes = typeof input === 'string' + ? new TextEncoder().encode(input) + : input + + const binaryString = String.fromCharCode(...bytes) + const base64 = btoa(binaryString) + + return base64 + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_') +} + +/** + * Sign target with salt and secret using HMAC-SHA256 + * @see https://docs.imgproxy.net/usage/signing_url#calculating-url-signature + * + * @param salt + * @param target + * @param secret + */ +function sign(salt: string, target: string, secret: string) { + const signature = hmac.create(sha256, hexToBytes(secret)) + signature.update(hexToBytes(salt)) + signature.update(new TextEncoder().encode(target)) + + return urlSafeBase64(signature.digest()) +} + +const defaultModifiers: Partial = { + resizingType: 'auto', + gravity: 'ce', + format: 'webp', +} + +/** + * Convert @nuxt/image modifiers to Imgproxy modifiers + * + * @param modifiers + */ +function resolveModifiers(modifiers: Partial): Partial { + if (modifiers?.fit) { + const hasW = typeof modifiers?.width === 'number' && modifiers.width > 0 + const hasH = typeof modifiers?.height === 'number' && modifiers.height > 0 + const hasBoth = hasW && hasH + + switch (modifiers.fit) { + case 'cover': + modifiers.resizingType = hasBoth ? 'fill' : 'fit' + break + case 'contain': + modifiers.resizingType = 'fit' + modifiers.extend = hasBoth + break + case 'fill': + modifiers.resizingType = hasBoth ? 'force' : 'fit' + break + case 'inside': + modifiers.resizingType = 'fit' + break + case 'outside': + modifiers.resizingType = hasBoth ? 'fill' : 'fit' + break + default: + throw new TypeError('Unsupported fit format') + } + delete modifiers.fit + } + + return modifiers +} + +/** + * Imgproxy provider + * @see https://imgproxy.net/ + */ +export default defineProvider({ + getImage: (src, { modifiers, baseURL, key, salt }) => { + const mergeModifiers = resolveModifiers(defu(modifiers, defaultModifiers)) + + const encodedUrl = urlSafeBase64(src) + const path = joinURL('/', operationsGenerator(mergeModifiers), encodedUrl) + const signature = sign(salt, path, key) + + return { + url: joinURL(baseURL, signature, path), + } + }, +}) diff --git a/test/e2e/__snapshots__/imgproxy.json5 b/test/e2e/__snapshots__/imgproxy.json5 new file mode 100644 index 000000000..905f81f35 --- /dev/null +++ b/test/e2e/__snapshots__/imgproxy.json5 @@ -0,0 +1,18 @@ +{ + "requests": [ + "http://localhost:8080/1iW16unhWC6fdIuuk5gVNEq48L4MGEepYK6DjeFwWmo/rt:auto/g:ce/f:webp/rot:180/w:500/h:500/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/U0eyKFEnztOizBuOIb5abnDeKWBVW-b_Ke1qVQwqm-Y/rt:auto/g:ce/f:webp/bl:100/w:500/h:500/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/Ylc_Rt5-wznwDNG9DYlu0I7YJBKQ_3fd1IW3UdJ-qFk/rt:auto/g:ce/f:webp/rs:fit:500:500:1:1/bg:FFCC00/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/biYdMhkZl8V5vnTlYnYsAoXLP9NnHBVkFT0cGLjxdVU/rt:auto/g:ce/f:webp/dpr:0.1/ex:1/exar:1:no:0:1/rot:180/sh:10/pix:10/sm:1/kcr:0/scp:1/mb:10/cb:test/exp:4106340630/fn:test/w:500/h:500/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/cboFXjcBPZOl8x-rxAeX-uWPjY3xqpFfYQDOsBLVoGs/rt:auto/g:ce/f:webp/w:300/h:300/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/zTIsniZNIm8038SWjYOnlQClTBbrPyebnl62k1gjze8/rt:auto/g:ce/f:webp/w:300/h:300/q:10/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + ], + "sources": [ + "http://localhost:8080/cboFXjcBPZOl8x-rxAeX-uWPjY3xqpFfYQDOsBLVoGs/rt:auto/g:ce/f:webp/w:300/h:300/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/zTIsniZNIm8038SWjYOnlQClTBbrPyebnl62k1gjze8/rt:auto/g:ce/f:webp/w:300/h:300/q:10/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/Ylc_Rt5-wznwDNG9DYlu0I7YJBKQ_3fd1IW3UdJ-qFk/rt:auto/g:ce/f:webp/rs:fit:500:500:1:1/bg:FFCC00/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/1iW16unhWC6fdIuuk5gVNEq48L4MGEepYK6DjeFwWmo/rt:auto/g:ce/f:webp/rot:180/w:500/h:500/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/U0eyKFEnztOizBuOIb5abnDeKWBVW-b_Ke1qVQwqm-Y/rt:auto/g:ce/f:webp/bl:100/w:500/h:500/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + "http://localhost:8080/biYdMhkZl8V5vnTlYnYsAoXLP9NnHBVkFT0cGLjxdVU/rt:auto/g:ce/f:webp/dpr:0.1/ex:1/exar:1:no:0:1/rot:180/sh:10/pix:10/sm:1/kcr:0/scp:1/mb:10/cb:test/exp:4106340630/fn:test/w:500/h:500/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn", + ], +} \ No newline at end of file diff --git a/test/nuxt/providers.test.ts b/test/nuxt/providers.test.ts index 21e7c6b25..bc0886790 100644 --- a/test/nuxt/providers.test.ts +++ b/test/nuxt/providers.test.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest' import { images } from '../providers' +import type { ImgproxyModifiers } from '../../dist/runtime/providers/imgproxy' + import { useNuxtApp } from '#imports' import ipx from '../../dist/runtime/providers/ipx' import none from '../../dist/runtime/providers/none' @@ -16,6 +18,7 @@ import picsum from '../../dist/runtime/providers/picsum' import prepr from '../../dist/runtime/providers/prepr' import glide from '../../dist/runtime/providers/glide' import imgix from '../../dist/runtime/providers/imgix' +import imgproxy from '../../dist/runtime/providers/imgproxy' import gumlet from '../../dist/runtime/providers/gumlet' import imageengine from '../../dist/runtime/providers/imageengine' import unsplash from '../../dist/runtime/providers/unsplash' @@ -263,6 +266,220 @@ describe('Providers', () => { } }) + it('imgproxy', () => { + const providerOptions = { + baseURL: 'http://localhost:8080/', + key: 'ee3b0e07dfc9ec20d5d9588a558753547a8a88c48291ae96171330daf4ce2800', + salt: '8dd0e39bb7b14eeaf02d49e5dc76d2bc0abd9e09d52e7049e791acd3558db68e', + } + for (const image of images) { + const [_src, modifiers] = image.args + const generated = imgproxy().getImage('https://mars.nasa.gov/system/downloadable_items/39099_Mars-MRO-orbiter-fresh-crater-sirenum-fossae.jpg', { modifiers, ...providerOptions }, getEmptyContext()) + + expect(generated.url).toBe(image.imgproxy.url) + } + }) + + it('imgproxy modifiers', () => { + const providerOptions = { + baseURL: 'http://localhost:8080/', + key: 'ee3b0e07dfc9ec20d5d9588a558753547a8a88c48291ae96171330daf4ce2800', + salt: '8dd0e39bb7b14eeaf02d49e5dc76d2bc0abd9e09d52e7049e791acd3558db68e', + } + + const sourceUrl: string = 'https://mars.nasa.gov/system/downloadable_items/39099_Mars-MRO-orbiter-fresh-crater-sirenum-fossae.jpg' + + const testCases: { + modifiers: Partial + expected: { + url: string + } + }[] = [ + { + modifiers: { + fit: 'contain', + width: 100, + height: 100, + }, + expected: { + url: 'http://localhost:8080/WDEF0lvBmcLhtzT11ww-9XMtA9YY2BiMWACintty0yg/rt:fit/g:ce/f:webp/w:100/h:100/ex:1/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + fit: 'cover', + width: 100, + height: 100, + }, + expected: { + url: 'http://localhost:8080/xnHZGPNtwahuGtmDh7vD5PQYjsxB8vpvyGr3eiq3jGo/rt:fill/g:ce/f:webp/w:100/h:100/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + fit: 'outside', + width: 100, + height: 100, + }, + expected: { + url: 'http://localhost:8080/xnHZGPNtwahuGtmDh7vD5PQYjsxB8vpvyGr3eiq3jGo/rt:fill/g:ce/f:webp/w:100/h:100/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + fit: 'inside', + width: 100, + height: 100, + }, + expected: { + url: 'http://localhost:8080/J-5_ixsKBIzg0qwYtypqPcC6MnEp2a5iErUKCbNqID8/rt:fit/g:ce/f:webp/w:100/h:100/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + width: 100, + height: 100, + fit: 'contain', + format: 'jpeg', + }, + expected: { + url: 'http://localhost:8080/0s7rNhOEt_rzLVU0CdGXMbkV6j5fH2VOI6PQlxaQbsc/rt:fit/g:ce/f:jpeg/w:100/h:100/ex:1/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + resizingType: 'fit', + width: 100, + height: 100, + }, + expected: { + url: 'http://localhost:8080/J-5_ixsKBIzg0qwYtypqPcC6MnEp2a5iErUKCbNqID8/rt:fit/g:ce/f:webp/w:100/h:100/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + resize: 'fit:100:100:1:1', + background: 'FFCC00', + }, + expected: { + url: 'http://localhost:8080/0RHxLESHHIkkMAv8z4RlWIG6smintg8G9wtf0l5hCR0/rt:auto/g:ce/f:webp/rs:fit:100:100:1:1/bg:FFCC00/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + width: 100, + height: 100, + minWidth: 2000, + minHeight: 2000, + }, + expected: { + url: 'http://localhost:8080/Zbqd-IqLtLZnIiHtFKAYPEpo6FuQOPraNd4nsL52Isg/rt:auto/g:ce/f:webp/w:100/h:100/mw:2000/mh:2000/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + zoom: '0.2:0.2', + }, + expected: { + url: 'http://localhost:8080/3GanjhskAMK8HRDfxSmIk1e_pE7BgjRptfSaokW1mDM/rt:auto/g:ce/f:webp/z:0.2:0.2/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + enlarge: true, + width: 500, + height: 500, + fit: 'fill', + }, + expected: { + url: 'http://localhost:8080/brfso4JHXYB1bTSWet-LEmhVhZbPr_T6kgZi2gRs5CM/rt:force/g:ce/f:webp/el:1/w:500/h:500/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + gravity: 'noea', + crop: { + width: 500, + height: 500, + }, + fit: 'fill', + }, + expected: { + url: 'http://localhost:8080/h90gjXPIuMedfFTptj3YBVx6wQybWqirYzoN_w3DfqM/rt:fit/g:noea/f:webp/c:500:500/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + autoRotate: true, + }, + expected: { + url: 'http://localhost:8080/YJn9J2b4RzApSgFXomIHirK3lt1oxhVj-FR7odgPGE0/rt:auto/g:ce/f:webp/ar:1/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + dpr: 0.1, + extend: true, + extendAspectRatio: '1:no:0:1', + rotate: 180, + background: '255:255:0', + sharpen: 10, + pixelate: 10, + stripMetadata: true, + keepCopyright: false, + stripColorProfile: true, + maxBytes: 10, + cachebuster: 'test', + expires: 4106340630, + filename: 'test', + blur: 100, + }, + expected: { + url: 'http://localhost:8080/2M-s-AwAAAm54MhfBSM8luh3_6fadayCOd8LHqsSD10/rt:auto/g:ce/f:webp/dpr:0.1/ex:1/exar:1:no:0:1/rot:180/bg:255:255:0/sh:10/pix:10/sm:1/kcr:0/scp:1/mb:10/cb:test/exp:4106340630/fn:test/bl:100/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + returnAttachment: true, + }, + expected: { + url: 'http://localhost:8080/u_mLtYCSpOu7VawiphkQ51pZeNb0PFcP-3lRSIVALAg/rt:auto/g:ce/f:webp/att:1/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + preset: 'blurry', + }, + expected: { + url: 'http://localhost:8080/DJ4Naz9SLYbQOIJDeq83o5RBj-7u4gcFoB-eFhHbU3o/rt:auto/g:ce/f:webp/pr:blurry/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + { + modifiers: { + width: 100, + height: 100, + raw: true, + }, + expected: { + url: 'http://localhost:8080/MQLhXkyO7bXibkqMUJL7RffHQ9Ks_jMwjWTPvAG9vhM/rt:auto/g:ce/f:webp/w:100/h:100/raw:1/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn', + }, + }, + ] + + for (const { modifiers, expected } of testCases) { + const generated = imgproxy().getImage( + sourceUrl, + { + modifiers, + ...providerOptions, + }, + getEmptyContext(), + ) + + expect(generated).toMatchObject(expected) + } + }) + it('imageengine', () => { const providerOptions = { baseURL: '', diff --git a/test/providers.ts b/test/providers.ts index 122d0183e..2b6967f20 100644 --- a/test/providers.ts +++ b/test/providers.ts @@ -36,6 +36,7 @@ export const images = [ weserv: { url: 'https://wsrv.nl/?filename=test.png&we&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, sirv: { url: 'https://demo.sirv.com/test.png' }, hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/auto_image/cltsrex89477t08unlckqx9ue' }, + imgproxy: { url: 'http://localhost:8080/nMRxrWjsgSmUuYkivM76-7f1WaCa2GCxFOx8zqJnRmY/rt:auto/g:ce/f:webp/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net' }, builderio: { url: 'https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F869bfbaec9c64415ae68235d9b7b1425' }, @@ -78,6 +79,7 @@ export const images = [ weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, sirv: { url: 'https://demo.sirv.com/test.png?w=200' }, hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200/auto_image/cltsrex89477t08unlckqx9ue' }, + imgproxy: { url: 'http://localhost:8080/jBlUz9woW03iM4Vz0jHSO_2Z2tEKxtBtp5gQFcmlqWs/rt:auto/g:ce/f:webp/w:200/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200' }, builderio: { url: 'https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F869bfbaec9c64415ae68235d9b7b1425?width=200' }, @@ -119,6 +121,7 @@ export const images = [ weserv: { url: 'https://wsrv.nl/?filename=test.png&we&h=200&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, sirv: { url: 'https://demo.sirv.com/test.png?h=200' }, hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=height:200/auto_image/cltsrex89477t08unlckqx9ue' }, + imgproxy: { url: 'http://localhost:8080/dtMPxKBvt7NNP8oDcJoSP1CPORtvk_FW__-EeVgYuHQ/rt:auto/g:ce/f:webp/h:200/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?height=200' }, builderio: { url: 'https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F869bfbaec9c64415ae68235d9b7b1425?height=200' }, @@ -160,6 +163,7 @@ export const images = [ weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, sirv: { url: 'https://demo.sirv.com/test.png?w=200&h=200' }, hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200/auto_image/cltsrex89477t08unlckqx9ue' }, + imgproxy: { url: 'http://localhost:8080/KtShqkyGorHRam6uqGtvd0fNn7oDWod4pbdezs7v2Co/rt:auto/g:ce/f:webp/w:200/h:200/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }, builderio: { url: 'https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F869bfbaec9c64415ae68235d9b7b1425?width=200&height=200' }, @@ -201,6 +205,7 @@ export const images = [ weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&fit=contain&url=https%3A%2F%2Fmy-website.com%2Ftest.png' }, sirv: { url: 'https://demo.sirv.com/test.png?w=200&h=200&scale.option=fit' }, hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200,fit:max/auto_image/cltsrex89477t08unlckqx9ue' }, + imgproxy: { url: 'http://localhost:8080/VV3y1JTp_YUqWy0Lrpu6-GDJfpehkO7Zx7kTYMgER-g/rt:fit/g:ce/f:webp/w:200/h:200/ex:1/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }, builderio: { url: 'https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F869bfbaec9c64415ae68235d9b7b1425?width=200&height=200&fit=contain' }, @@ -242,6 +247,7 @@ export const images = [ 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' }, sirv: { url: 'https://demo.sirv.com/test.png?w=200&h=200&scale.option=fit&format=jpg' }, hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200,fit:max/output=format:jpeg/cltsrex89477t08unlckqx9ue' }, + imgproxy: { url: 'http://localhost:8080/Ry5p-XqKl7_kgwAm1Ir1V8aCKdax2jnzDXtscG85zmg/rt:fit/g:ce/f:jpeg/w:200/h:200/ex:1/aHR0cHM6Ly9tYXJzLm5hc2EuZ292L3N5c3RlbS9kb3dubG9hZGFibGVfaXRlbXMvMzkwOTlfTWFycy1NUk8tb3JiaXRlci1mcmVzaC1jcmF0ZXItc2lyZW51bS1mb3NzYWUuanBn' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }, builderio: { url: 'https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F869bfbaec9c64415ae68235d9b7b1425?width=200&height=200&fit=contain&format=jpeg' },