Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(common): request low quality placeholder images #54899

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {PLACEHOLDER_QUALITY} from './constants';
import {createImageLoader, ImageLoaderConfig} from './image_loader';

/**
Expand All @@ -29,6 +30,12 @@ function createCloudflareUrl(path: string, config: ImageLoaderConfig) {
if (config.width) {
params += `,width=${config.width}`;
}

// When requesting a placeholder image we ask for a low quality image to reduce the load time.
if (config.isPlaceholder) {
params += `,quality=${PLACEHOLDER_QUALITY}`;
}

// Cloudflare image URLs format:
// https://developers.cloudflare.com/images/image-resizing/url-format/
return `${path}/cdn-cgi/image/${params}/${config.src}`;
Expand Down
Expand Up @@ -52,9 +52,15 @@ function createCloudinaryUrl(path: string, config: ImageLoaderConfig) {
// https://cloudinary.com/documentation/image_transformations#transformation_url_structure
// Example of a Cloudinary image URL:
// https://res.cloudinary.com/mysite/image/upload/c_scale,f_auto,q_auto,w_600/marketing/tile-topics-m.png
let params = `f_auto,q_auto`; // sets image format and quality to "auto"

// For a placeholder image, we use the lowest image setting available to reduce the load time
// else we use the auto size
const quality = config.isPlaceholder ? 'q_auto:low' : 'q_auto';

let params = `f_auto,${quality}`;
if (config.width) {
params += `,w_${config.width}`;
}

return `${path}/image/upload/${params}/${config.src}`;
}
@@ -0,0 +1,12 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/**
* Value (out of 100) of the requested quality for placeholder images.
*/
export const PLACEHOLDER_QUALITY = '20';
AndrewKushnir marked this conversation as resolved.
Show resolved Hide resolved
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {PLACEHOLDER_QUALITY} from './constants';
import {createImageLoader, ImageLoaderConfig, ImageLoaderInfo} from './image_loader';

/**
Expand Down Expand Up @@ -53,5 +54,11 @@ export function createImagekitUrl(path: string, config: ImageLoaderConfig): stri
urlSegments = [path, src];
}

return urlSegments.join('/');
const url = new URL(urlSegments.join('/'));

// When requesting a placeholder image we ask for a low quality image to reduce the load time.
if (config.isPlaceholder) {
url.searchParams.set('q', PLACEHOLDER_QUALITY);
}
return url.href;
}
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {PLACEHOLDER_QUALITY} from './constants';
import {createImageLoader, ImageLoaderConfig, ImageLoaderInfo} from './image_loader';

/**
Expand Down Expand Up @@ -45,5 +46,10 @@ function createImgixUrl(path: string, config: ImageLoaderConfig) {
if (config.width) {
url.searchParams.set('w', config.width.toString());
}

// When requesting a placeholder image we ask a low quality image to reduce the load time.
if (config.isPlaceholder) {
url.searchParams.set('q', PLACEHOLDER_QUALITY);
}
return url.href;
}
Expand Up @@ -16,6 +16,7 @@ import {RuntimeErrorCode} from '../../../errors';
import {isAbsoluteUrl, isValidPath} from '../url';

import {IMAGE_LOADER, ImageLoaderConfig, ImageLoaderInfo} from './image_loader';
import {PLACEHOLDER_QUALITY} from './constants';

/**
* Name and URL tester for Netlify.
Expand Down Expand Up @@ -90,6 +91,13 @@ function createNetlifyUrl(config: ImageLoaderConfig, path?: string) {
url.searchParams.set('w', config.width.toString());
}

// When requesting a placeholder image we ask for a low quality image to reduce the load time.
// If the quality is specified in the loader config - always use provided value.
const configQuality = config.loaderParams?.['quality'] ?? config.loaderParams?.['q'];
if (config.isPlaceholder && !configQuality) {
url.searchParams.set('q', PLACEHOLDER_QUALITY);
}

for (const [param, value] of Object.entries(config.loaderParams ?? {})) {
if (validParams.has(param)) {
url.searchParams.set(validParams.get(param)!, value.toString());
Expand Down
44 changes: 44 additions & 0 deletions packages/common/test/image_loaders/image_loader_spec.ts
Expand Up @@ -75,6 +75,13 @@ describe('Built-in image directive loaders', () => {
const loader = createImgixLoader(path);
expect(() => loader({src})).toThrowError(absoluteUrlError(src, path));
});

it('should load a low quality image when a placeholder is requested', () => {
const path = 'https://somesite.imgix.net';
const loader = createImgixLoader(path);
const config = {src: 'img.png', isPlaceholder: true};
expect(loader(config)).toBe(`${path}/img.png?auto=format&q=20`);
});
});

describe('Cloudinary loader', () => {
Expand All @@ -97,6 +104,13 @@ describe('Built-in image directive loaders', () => {
).toBe(`${path}/image/upload/f_auto,q_auto/marketing/img-2.png`);
});

it('should load a low quality image when a placeholder is requested', () => {
const path = 'https://res.cloudinary.com/mysite';
const loader = createCloudinaryLoader(path);
const config = {src: 'img.png', isPlaceholder: true};
expect(loader(config)).toBe(`${path}/image/upload/f_auto,q_auto:low/img.png`);
});

describe('input validation', () => {
it('should throw if an absolute URL is provided as a loader input', () => {
const path = 'https://res.cloudinary.com/mysite';
Expand Down Expand Up @@ -154,6 +168,13 @@ describe('Built-in image directive loaders', () => {
);
});

it('should load a low quality image when a placeholder is requested', () => {
const path = 'https://ik.imageengine.io/imagetest';
const loader = createImageKitLoader(path);
const config = {src: 'img.png', isPlaceholder: true};
expect(loader(config)).toBe(`${path}/img.png?q=20`);
});

describe('input validation', () => {
it('should throw if an absolute URL is provided as a loader input', () => {
const path = 'https://ik.imageengine.io/imagetest';
Expand Down Expand Up @@ -210,6 +231,15 @@ describe('Built-in image directive loaders', () => {
const loader = createCloudflareLoader(path);
expect(() => loader({src})).toThrowError(absoluteUrlError(src, path));
});

it('should load a low quality image when a placeholder is requested', () => {
const path = 'https://mysite.com';
const loader = createCloudflareLoader(path);
const config = {src: 'img.png', isPlaceholder: true};
expect(loader(config)).toBe(
'https://mysite.com/cdn-cgi/image/format=auto,quality=20/img.png',
);
});
});

describe('Netlify loader', () => {
Expand Down Expand Up @@ -261,6 +291,20 @@ describe('Built-in image directive loaders', () => {
`NG0${RuntimeErrorCode.INVALID_LOADER_ARGUMENTS}: The Netlify image loader has detected an \`<img>\` tag with the unsupported attribute "\`unknown\`".`,
);
});

it('should load a low quality image when a placeholder is requested', () => {
const path = 'https://mysite.com';
const loader = createNetlifyLoader(path);
const config = {src: 'img.png', isPlaceholder: true};
expect(loader(config)).toBe('https://mysite.com/.netlify/images?url=%2Fimg.png&q=20');
});

it('should not load a low quality image when a placeholder is requested with a quality param', () => {
const path = 'https://mysite.com';
const loader = createNetlifyLoader(path);
const config = {src: 'img.png', isPlaceholder: true, loaderParams: {quality: 50}};
expect(loader(config)).toBe('https://mysite.com/.netlify/images?url=%2Fimg.png&q=50');
});
});

describe('loader utils', () => {
Expand Down