Skip to content

Commit b1ec8cd

Browse files
committed
feat(common): Add custom transformations for Cloudflare and Cloudinary image loaders
Adds support for custom transformations to Cloudinary and Cloudflare image loaders via a `transform` parameter. Fixes #65191 and #64639
1 parent de234de commit b1ec8cd

File tree

4 files changed

+126
-0
lines changed

4 files changed

+126
-0
lines changed

packages/common/src/directives/ng_optimized_image/image_loaders/cloudflare_loader.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {Provider} from '@angular/core';
1010
import {PLACEHOLDER_QUALITY} from './constants';
1111
import {createImageLoader, ImageLoaderConfig} from './image_loader';
12+
import {normalizeLoaderTransform} from './normalized_options';
1213

1314
/**
1415
* Function that generates an ImageLoader for [Cloudflare Image
@@ -37,6 +38,12 @@ function createCloudflareUrl(path: string, config: ImageLoaderConfig) {
3738
params += `,quality=${PLACEHOLDER_QUALITY}`;
3839
}
3940

41+
// Support custom transformation parameters
42+
if (config.loaderParams?.['transform']) {
43+
const transformStr = normalizeLoaderTransform(config.loaderParams['transform'], '=');
44+
params += `,${transformStr}`;
45+
}
46+
4047
// Cloudflare image URLs format:
4148
// https://developers.cloudflare.com/images/image-resizing/url-format/
4249
return `${path}/cdn-cgi/image/${params}/${config.src}`;

packages/common/src/directives/ng_optimized_image/image_loaders/cloudinary_loader.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {Provider} from '@angular/core';
1010
import {createImageLoader, ImageLoaderConfig, ImageLoaderInfo} from './image_loader';
11+
import {normalizeLoaderTransform} from './normalized_options';
1112

1213
/**
1314
* Name and URL tester for Cloudinary.
@@ -67,5 +68,11 @@ function createCloudinaryUrl(path: string, config: ImageLoaderConfig) {
6768
params += `,r_max`;
6869
}
6970

71+
// Allows users to add any Cloudinary transformation parameters as a string or object
72+
if (config.loaderParams?.['transform']) {
73+
const transformStr = normalizeLoaderTransform(config.loaderParams['transform'], '_');
74+
params += `,${transformStr}`;
75+
}
76+
7077
return `${path}/image/upload/${params}/${config.src}`;
7178
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
/**
10+
* Converts transform parameter to URL parameter string.
11+
* @param transform The transform parameter as string or object
12+
* @param separator The separator between key and value ('_' for Cloudinary, '=' for Cloudflare)
13+
*/
14+
export function normalizeLoaderTransform(
15+
transform: string | Record<string, string>,
16+
separator: string,
17+
): string {
18+
if (typeof transform === 'string') {
19+
return transform;
20+
}
21+
22+
return Object.entries(transform)
23+
.map(([key, value]) => `${key}${separator}${value}`)
24+
.join(',');
25+
}

packages/common/test/image_loaders/image_loader_spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,45 @@ describe('Built-in image directive loaders', () => {
151151
`${path}/image/upload/f_auto,q_auto,r_max/img.png`,
152152
);
153153
});
154+
155+
it('should apply custom transformations when transform is provided as a string', () => {
156+
const path = 'https://res.cloudinary.com/mysite';
157+
const loader = createCloudinaryLoader(path);
158+
expect(loader({src: '/img.png', loaderParams: {transform: 'e_grayscale,r_10'}})).toBe(
159+
`${path}/image/upload/f_auto,q_auto,e_grayscale,r_10/img.png`,
160+
);
161+
});
162+
163+
it('should apply custom transformations when transform is provided as an object', () => {
164+
const path = 'https://res.cloudinary.com/mysite';
165+
const loader = createCloudinaryLoader(path);
166+
expect(
167+
loader({src: '/img.png', loaderParams: {transform: {e: 'grayscale', r: 10, f: 'webp'}}}),
168+
).toBe(`${path}/image/upload/f_auto,q_auto,e_grayscale,r_10,f_webp/img.png`);
169+
});
170+
171+
it('should combine rounded and transform parameters', () => {
172+
const path = 'https://res.cloudinary.com/mysite';
173+
const loader = createCloudinaryLoader(path);
174+
expect(
175+
loader({
176+
src: '/img.png',
177+
loaderParams: {rounded: true, transform: 'e_blur:300'},
178+
}),
179+
).toBe(`${path}/image/upload/f_auto,q_auto,r_max,e_blur:300/img.png`);
180+
});
181+
182+
it('should apply width and transform parameters together', () => {
183+
const path = 'https://res.cloudinary.com/mysite';
184+
const loader = createCloudinaryLoader(path);
185+
expect(
186+
loader({
187+
src: '/img.png',
188+
width: 500,
189+
loaderParams: {transform: {q: 80, e: 'sharpen'}},
190+
}),
191+
).toBe(`${path}/image/upload/f_auto,q_auto,w_500,q_80,e_sharpen/img.png`);
192+
});
154193
});
155194
});
156195

@@ -255,6 +294,54 @@ describe('Built-in image directive loaders', () => {
255294
'https://mysite.com/cdn-cgi/image/format=auto,quality=20/img.png',
256295
);
257296
});
297+
298+
it('should apply custom transformations when transform is provided as a string', () => {
299+
const path = 'https://mysite.com';
300+
const loader = createCloudflareLoader(path);
301+
expect(
302+
loader({src: 'img.png', loaderParams: {transform: 'fit=cover,gravity=face,height=300'}}),
303+
).toBe(
304+
'https://mysite.com/cdn-cgi/image/format=auto,fit=cover,gravity=face,height=300/img.png',
305+
);
306+
});
307+
308+
it('should apply custom transformations when transform is provided as an object', () => {
309+
const path = 'https://mysite.com';
310+
const loader = createCloudflareLoader(path);
311+
expect(
312+
loader({
313+
src: 'img.png',
314+
loaderParams: {transform: {fit: 'cover', gravity: 'auto', height: 300}},
315+
}),
316+
).toBe(
317+
'https://mysite.com/cdn-cgi/image/format=auto,fit=cover,gravity=auto,height=300/img.png',
318+
);
319+
});
320+
321+
it('should combine width and transform parameters', () => {
322+
const path = 'https://mysite.com';
323+
const loader = createCloudflareLoader(path);
324+
expect(
325+
loader({
326+
src: 'img.png',
327+
width: 400,
328+
loaderParams: {transform: {fit: 'crop', gravity: 'face'}},
329+
}),
330+
).toBe(
331+
'https://mysite.com/cdn-cgi/image/format=auto,width=400,fit=crop,gravity=face/img.png',
332+
);
333+
});
334+
335+
it('should support additional transformation options via transform object', () => {
336+
const path = 'https://mysite.com';
337+
const loader = createCloudflareLoader(path);
338+
expect(
339+
loader({
340+
src: 'img.png',
341+
loaderParams: {transform: {blur: 50, rotate: 90, quality: 85}},
342+
}),
343+
).toBe('https://mysite.com/cdn-cgi/image/format=auto,blur=50,rotate=90,quality=85/img.png');
344+
});
258345
});
259346

260347
describe('Netlify loader', () => {

0 commit comments

Comments
 (0)