Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/cloudflare/src/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ describe('cloudflare', () => {
})

expect(image.srcSet.map(({ id }) => id)).toEqual(['webp600', 'png600'])
expect(image.src.id).toBe('png600')
expect(image.srcMap.png600).toBe('/cdn-cgi/image/width=600/https://cdn.example.com/picture.png')
})

Expand All @@ -131,7 +132,7 @@ describe('cloudflare', () => {
}).srcSet.length).toBe(1)
})

it('should select fallback src of the last format with the largest width', () => {
it('should select fallback src of the source format with the largest width', () => {
const image = cloudflare.image(sourceUrl, {
width: [300, 1200, 600],
format: ['avif', 'webp', 'jpg']
Expand All @@ -140,6 +141,24 @@ describe('cloudflare', () => {
expect(image.src.id).toBe('jpg1200')
})

it('should select fallback src of the source format wherever it is in the rule', () => {
const image = cloudflare.image(sourceUrl, {
width: [1200, 600],
format: ['jpg', 'webp']
})

expect(image.src.id).toBe('jpg1200')
})

it('should select fallback src of the first format without the source format', () => {
const image = cloudflare.image(sourceUrl, {
width: [1200, 600],
format: ['webp', 'avif']
})

expect(image.src.id).toBe('webp1200')
})

it('should use custom endpoint', () => {
const zone = new Cloudflare({
endpoint: 'https://example.com/cdn-cgi/image'
Expand Down
21 changes: 12 additions & 9 deletions packages/cloudflare/src/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
buildCloudflareUrl,
createDefaultProcessing
} from './url.ts'
import { toArray } from './utils.ts'
import { toUniqArray } from './utils.ts'

const FORMAT_PATTERN = /\.(\w+)$/
const QUERY_OR_FRAGMENT_PATTERN = /[?#]/
Expand All @@ -39,7 +39,8 @@ export interface CloudflareImage {
*/
url: string
/**
* Fallback image variant: the last format, the largest width.
* Fallback image variant: the source format, or the first format
* of the rule when the source format is not in it, at the largest width.
*/
src: SrcSetEntry
/**
Expand Down Expand Up @@ -91,18 +92,21 @@ export class Cloudflare {
// Cloudflare returns svg sources as is, ignoring all transformations,
// so an svg source always passes through; jpg drives the variant loop.
const passthrough = this.#passthrough || isSvgSource
const formats: ImageFormat[] = isSvgSource ? ['jpg'] : toArray(rule.format, sourceFormat)
const widths = toArray(rule.width)
const formats: ImageFormat[] = isSvgSource ? ['jpg'] : toUniqArray(rule.format, sourceFormat)
// Same selection as a build-time rule: the source format, or the first
// format of the list when the source format is not in it.
const srcFormat = formats.includes(sourceFormat) ? sourceFormat : formats[0]
const widths = toUniqArray(rule.width)
const srcSet: SrcSetEntry[] = []
const srcMap: Record<string, string> = {}
let src: SrcSetEntry | undefined

for (const format of new Set(formats)) {
for (const format of formats) {
if (!canOutputFormat(format, sourceFormat)) {
throw new TypeError(`Cloudflare can not force the ${format} output format.`)
}

for (const width of new Set(widths)) {
for (const width of widths) {
// The source size is unknown on the client, so multipliers can't be
// resolved, and `w` descriptors need integer pixel widths.
if (!Number.isInteger(width) || width <= 1) {
Expand All @@ -127,9 +131,8 @@ export class Cloudflare {
srcMap[entry.id] = entry.url
}

// Formats go first: on format change the entry starts the next format
// group, so the src candidate ends up in the last group, the largest width.
if (!src || src.format !== entry.format || entry.width > src.width) {
// The `src` variant is the largest width of the selected format.
if (entry.format === srcFormat && (!src || entry.width > src.width)) {
src = entry
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/cloudflare/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ export interface CloudflareOptions {
*/
export interface CloudflareRule {
/**
* Output image format(s). The last one is used as the `src` fallback.
* Defaults to the url file extension.
* Output image format(s). Defaults to the url file extension.
* The `src` fallback is the variant of the source format, or of the
* first format of the list when the source format is not in it.
*/
format?: ImageFormat | ImageFormat[]
/**
Expand Down
18 changes: 10 additions & 8 deletions packages/cloudflare/src/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@ import {
it,
expect
} from 'vitest'
import { toArray } from './utils.ts'
import { toUniqArray } from './utils.ts'

describe('cloudflare', () => {
describe('utils', () => {
describe('toArray', () => {
it('should return array as is', () => {
const value = [1, 2]
describe('toUniqArray', () => {
it('should deduplicate an array', () => {
expect(toUniqArray([1, 2, 1])).toEqual([1, 2])
})

expect(toArray(value)).toBe(value)
it('should keep the order of the first occurrences', () => {
expect(toUniqArray([2, 1, 2, 3])).toEqual([2, 1, 3])
})

it('should wrap single value', () => {
expect(toArray(1)).toEqual([1])
expect(toUniqArray(1)).toEqual([1])
})

it('should fall back for undefined', () => {
expect(toArray(undefined, 1)).toEqual([1])
expect(toArray(undefined)).toEqual([])
expect(toUniqArray(undefined, 1)).toEqual([1])
expect(toUniqArray(undefined)).toEqual([])
})
})
})
Expand Down
10 changes: 5 additions & 5 deletions packages/cloudflare/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Normalize an optional single-or-array value to an array.
* Normalize an optional single-or-array value to an array of unique values.
* @param value - Single value, array, or `undefined`.
* @param fallback - Value for the `undefined` case.
* @returns Array of values.
* @param fallback - Value for the `undefined` case, omitted for an empty array.
* @returns Array of unique values.
*/
export function toArray<T>(value: T | T[] | undefined, fallback?: T): T[] {
export function toUniqArray<T>(value: T | T[] | undefined, fallback?: T): T[] {
if (Array.isArray(value)) {
return value
return [...new Set(value)]
}

if (value !== undefined) {
Expand Down
20 changes: 19 additions & 1 deletion packages/imgproxy/src/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('imgproxy', () => {
}).srcSet.length).toBe(1)
})

it('should select fallback src of the last format with the largest width', () => {
it('should select fallback src of the source format with the largest width', () => {
const image = imgproxy.image(sourceUrl, {
width: [300, 1200, 600],
format: ['avif', 'webp', 'jpg']
Expand All @@ -111,6 +111,24 @@ describe('imgproxy', () => {
expect(image.src.id).toBe('jpg1200')
})

it('should select fallback src of the source format wherever it is in the rule', () => {
const image = imgproxy.image(sourceUrl, {
width: [1200, 600],
format: ['jpg', 'webp']
})

expect(image.src.id).toBe('jpg1200')
})

it('should select fallback src of the first format without the source format', () => {
const image = imgproxy.image(sourceUrl, {
width: [1200, 600],
format: ['webp', 'avif']
})

expect(image.src.id).toBe('webp1200')
})

it('should use custom processing builder', () => {
const presets = new Imgproxy({
endpoint: 'https://imgproxy.example.com',
Expand Down
28 changes: 14 additions & 14 deletions packages/imgproxy/src/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
buildImgproxyUrl,
createDefaultProcessing
} from './url.ts'
import { toArray } from './utils.ts'
import { toUniqArray } from './utils.ts'

const FORMAT_PATTERN = /\.(\w+)$/
const QUERY_OR_FRAGMENT_PATTERN = /[?#]/
Expand Down Expand Up @@ -50,7 +50,8 @@ export interface ImgproxyImage {
*/
url: string
/**
* Fallback image variant: the last format, the largest width.
* Fallback image variant: the source format, or the first format
* of the rule when the source format is not in it, at the largest width.
*/
src: SrcSetEntry
/**
Expand Down Expand Up @@ -99,19 +100,19 @@ export class Imgproxy {
* @returns Image srcset object.
*/
image(sourceUrl: string, rule: ImgproxyRule): ImgproxyImage {
const formats = toArray(rule.format, outputFormatFromUrl(sourceUrl))
const widths = toArray(rule.width)
const sourceFormat = outputFormatFromUrl(sourceUrl)
// Raster image can't be converted to SVG.
const formats = toUniqArray(rule.format, sourceFormat).filter(format => format !== 'svg')
// Same selection as a build-time rule: the source format, or the first
// format of the list when the source format is not in it.
const srcFormat = formats.includes(sourceFormat) ? sourceFormat : formats[0]
const widths = toUniqArray(rule.width)
const srcSet: SrcSetEntry[] = []
const srcMap: Record<string, string> = {}
let src: SrcSetEntry | undefined

for (const format of new Set(formats)) {
// Raster image can't be converted to SVG.
if (format === 'svg') {
continue
}

for (const width of new Set(widths)) {
for (const format of formats) {
for (const width of widths) {
// The source size is unknown on the client, so multipliers can't be
// resolved, and `w` descriptors need integer pixel widths.
if (!Number.isInteger(width) || width <= 1) {
Expand All @@ -136,9 +137,8 @@ export class Imgproxy {
srcMap[entry.id] = entry.url
}

// Formats go first: on format change the entry starts the next format
// group, so the src candidate ends up in the last group, the largest width.
if (!src || src.format !== entry.format || entry.width > src.width) {
// The `src` variant is the largest width of the selected format.
if (entry.format === srcFormat && (!src || entry.width > src.width)) {
src = entry
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/imgproxy/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ export interface ImgproxyOptions {
*/
export interface ImgproxyRule {
/**
* Output image format(s). The last one is used as the `src` fallback.
* Defaults to the url file extension.
* Output image format(s). Defaults to the url file extension.
* The `src` fallback is the variant of the source format, or of the
* first format of the list when the source format is not in it.
*/
format?: ImageFormat | ImageFormat[]
/**
Expand Down
18 changes: 10 additions & 8 deletions packages/imgproxy/src/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@ import {
it,
expect
} from 'vitest'
import { toArray } from './utils.ts'
import { toUniqArray } from './utils.ts'

describe('imgproxy', () => {
describe('utils', () => {
describe('toArray', () => {
it('should return array as is', () => {
const value = [1, 2]
describe('toUniqArray', () => {
it('should deduplicate an array', () => {
expect(toUniqArray([1, 2, 1])).toEqual([1, 2])
})

expect(toArray(value)).toBe(value)
it('should keep the order of the first occurrences', () => {
expect(toUniqArray([2, 1, 2, 3])).toEqual([2, 1, 3])
})

it('should wrap single value', () => {
expect(toArray(1)).toEqual([1])
expect(toUniqArray(1)).toEqual([1])
})

it('should fall back for undefined', () => {
expect(toArray(undefined, 1)).toEqual([1])
expect(toArray(undefined)).toEqual([])
expect(toUniqArray(undefined, 1)).toEqual([1])
expect(toUniqArray(undefined)).toEqual([])
})
})
})
Expand Down
8 changes: 4 additions & 4 deletions packages/imgproxy/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Normalize an optional single-or-array value to an array.
* Normalize an optional single-or-array value to an array of unique values.
* @param value - Single value, array, or `undefined`.
* @param fallback - Value for the `undefined` case, omitted for an empty array.
* @returns Array of values.
* @returns Array of unique values.
*/
export function toArray<T>(value: T | T[] | undefined, fallback?: T): T[] {
export function toUniqArray<T>(value: T | T[] | undefined, fallback?: T): T[] {
if (Array.isArray(value)) {
return value
return [...new Set(value)]
}

if (value !== undefined) {
Expand Down