0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-23 21:53:55 -05:00

@astrojs/image: add a background option/prop to replace the alpha layer (#4642)

* Added `background` option and prop.
This optional color specifies which background to use when removing the
alpha channel if the output format doesn't support transparency.

* Modified existing tests

* Fixed wrong dimensions in tests

* Fixing a few instances of jpeg vs jpg

* Added color checking

* working on the tests

* tests are now passing

* Adding tests

* Added tests for background color

* no need to test with subpath

* Added fixture

* Renamed test fixture for background-color

* skipping test until fixed

* Typo

* Working on tests

* tests are passing

* Updated readme and added changeset

* Updated lockfile

* Updated lockfile

* Updated lockfile

Co-authored-by: Tony Sullivan <tony.f.sullivan@outlook.com>
This commit is contained in:
Valentin Bersier 2022-09-07 19:22:11 +02:00 committed by GitHub
parent 93c3aee01c
commit e4348a4eb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1623 additions and 1012 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/image': minor
---
Added a `background` option to specify a background color to replace transparent pixels (alpha layer).

View file

@ -190,6 +190,24 @@ A `string` can be provided in the form of `{width}:{height}`, ex: `16:9` or `3:4
A `number` can also be provided, useful when the aspect ratio is calculated at build time. This can be an inline number such as `1.777` or inlined as a JSX expression like `aspectRatio={16/9}`. A `number` can also be provided, useful when the aspect ratio is calculated at build time. This can be an inline number such as `1.777` or inlined as a JSX expression like `aspectRatio={16/9}`.
#### background
<p>
**Type:** `ColorDefinition`<br>
**Default:** `undefined`
</p>
The background color to use for replacing the alpha channel with `sharp`'s `flatten` method. In case the output format
doesn't support transparency (i.e. `jpeg`), it's advisable to include a background color, otherwise black will be used
as default replacement for transparent pixels.
The parameter accepts a `string` as value.
The parameter can be a [named HTML color](https://www.w3schools.com/tags/ref_colornames.asp), a hexadecimal
color representation with 3 or 6 hexadecimal characters in the form `#123[abc]`, or an RGB definition in the form
`rgb(100,100,100)`.
### `<Picture /`> ### `<Picture /`>
#### src #### src
@ -271,6 +289,24 @@ A `number` can also be provided, useful when the aspect ratio is calculated at b
The output formats to be used in the optimized image. If not provided, `webp` and `avif` will be used in addition to the original image format. The output formats to be used in the optimized image. If not provided, `webp` and `avif` will be used in addition to the original image format.
#### background
<p>
**Type:** `ColorDefinition`<br>
**Default:** `undefined`
</p>
The background color to use for replacing the alpha channel with `sharp`'s `flatten` method. In case the output format
doesn't support transparency (i.e. `jpeg`), it's advisable to include a background color, otherwise black will be used
as default replacement for transparent pixels.
The parameter accepts a `string` as value.
The parameter can be a [named HTML color](https://www.w3schools.com/tags/ref_colornames.asp), a hexadecimal
color representation with 3 or 6 hexadecimal characters in the form `#123[abc]`, or an RGB definition in the form
`rgb(100,100,100)`.
### `getImage` ### `getImage`
This is the helper function used by the `<Image />` component to build `<img />` attributes for the transformed image. This helper can be used directly for more complex use cases that aren't currently supported by the `<Image />` component. This is the helper function used by the `<Image />` component to build `<img />` attributes for the transformed image. This helper can be used directly for more complex use cases that aren't currently supported by the `<Image />` component.

View file

@ -27,6 +27,7 @@ interface RemoteImageProps
widths: number[]; widths: number[];
aspectRatio: TransformOptions['aspectRatio']; aspectRatio: TransformOptions['aspectRatio'];
formats?: OutputFormat[]; formats?: OutputFormat[];
background: TransformOptions['background'];
} }
export type Props = LocalImageProps | RemoteImageProps; export type Props = LocalImageProps | RemoteImageProps;
@ -37,6 +38,7 @@ const {
sizes, sizes,
widths, widths,
aspectRatio, aspectRatio,
background,
formats = ['avif', 'webp'], formats = ['avif', 'webp'],
loading = 'lazy', loading = 'lazy',
decoding = 'async', decoding = 'async',
@ -47,7 +49,7 @@ if (alt === undefined || alt === null) {
warnForMissingAlt(); warnForMissingAlt();
} }
const { image, sources } = await getPicture({ src, widths, formats, aspectRatio }); const { image, sources } = await getPicture({ src, widths, formats, aspectRatio, background });
--- ---
<picture {...attrs}> <picture {...attrs}>

View file

@ -1,5 +1,10 @@
/// <reference types="astro/astro-jsx" /> /// <reference types="astro/astro-jsx" />
import type { ImageService, OutputFormat, TransformOptions } from '../loaders/index.js'; import type {
ColorDefinition,
ImageService,
OutputFormat,
TransformOptions,
} from '../loaders/index.js';
import { isSSRService, parseAspectRatio } from '../loaders/index.js'; import { isSSRService, parseAspectRatio } from '../loaders/index.js';
import sharp from '../loaders/sharp.js'; import sharp from '../loaders/sharp.js';
import { isRemoteImage } from '../utils/paths.js'; import { isRemoteImage } from '../utils/paths.js';
@ -63,7 +68,7 @@ async function resolveTransform(input: GetImageTransform): Promise<TransformOpti
// resolve the metadata promise, usually when the ESM import is inlined // resolve the metadata promise, usually when the ESM import is inlined
const metadata = 'then' in input.src ? (await input.src).default : input.src; const metadata = 'then' in input.src ? (await input.src).default : input.src;
let { width, height, aspectRatio, format = metadata.format, ...rest } = input; let { width, height, aspectRatio, background, format = metadata.format, ...rest } = input;
if (!width && !height) { if (!width && !height) {
// neither dimension was provided, use the file metadata // neither dimension was provided, use the file metadata
@ -86,6 +91,7 @@ async function resolveTransform(input: GetImageTransform): Promise<TransformOpti
height, height,
aspectRatio, aspectRatio,
format: format as OutputFormat, format: format as OutputFormat,
background: background as ColorDefinition | undefined,
}; };
} }

View file

@ -10,6 +10,7 @@ export interface GetPictureParams {
widths: number[]; widths: number[];
formats: OutputFormat[]; formats: OutputFormat[];
aspectRatio?: TransformOptions['aspectRatio']; aspectRatio?: TransformOptions['aspectRatio'];
background?: TransformOptions['background'];
} }
export interface GetPictureResult { export interface GetPictureResult {
@ -64,6 +65,7 @@ export async function getPicture(params: GetPictureParams): Promise<GetPictureRe
format, format,
width, width,
height: Math.round(width / aspectRatio!), height: Math.round(width / aspectRatio!),
background: params.background,
}); });
return `${img.src} ${width}w`; return `${img.src} ${width}w`;
}) })
@ -83,6 +85,7 @@ export async function getPicture(params: GetPictureParams): Promise<GetPictureRe
width: Math.max(...widths), width: Math.max(...widths),
aspectRatio, aspectRatio,
format: allFormats[allFormats.length - 1], format: allFormats[allFormats.length - 1],
background: params.background,
}); });
const sources = await Promise.all(allFormats.map((format) => getSource(format))); const sources = await Promise.all(allFormats.map((format) => getSource(format)));

View file

@ -0,0 +1,290 @@
export type NamedColor =
| 'aliceblue'
| 'antiquewhite'
| 'aqua'
| 'aquamarine'
| 'azure'
| 'beige'
| 'bisque'
| 'black'
| 'blanchedalmond'
| 'blue'
| 'blueviolet'
| 'brown'
| 'burlywood'
| 'cadetblue'
| 'chartreuse'
| 'chocolate'
| 'coral'
| 'cornflowerblue'
| 'cornsilk'
| 'crimson'
| 'cyan'
| 'darkblue'
| 'darkcyan'
| 'darkgoldenrod'
| 'darkgray'
| 'darkgreen'
| 'darkkhaki'
| 'darkmagenta'
| 'darkolivegreen'
| 'darkorange'
| 'darkorchid'
| 'darkred'
| 'darksalmon'
| 'darkseagreen'
| 'darkslateblue'
| 'darkslategray'
| 'darkturquoise'
| 'darkviolet'
| 'deeppink'
| 'deepskyblue'
| 'dimgray'
| 'dodgerblue'
| 'firebrick'
| 'floralwhite'
| 'forestgreen'
| 'fuchsia'
| 'gainsboro'
| 'ghostwhite'
| 'gold'
| 'goldenrod'
| 'gray'
| 'green'
| 'greenyellow'
| 'honeydew'
| 'hotpink'
| 'indianred'
| 'indigo'
| 'ivory'
| 'khaki'
| 'lavender'
| 'lavenderblush'
| 'lawngreen'
| 'lemonchiffon'
| 'lightblue'
| 'lightcoral'
| 'lightcyan'
| 'lightgoldenrodyellow'
| 'lightgray'
| 'lightgreen'
| 'lightpink'
| 'lightsalmon'
| 'lightsalmon'
| 'lightseagreen'
| 'lightskyblue'
| 'lightslategray'
| 'lightsteelblue'
| 'lightyellow'
| 'lime'
| 'limegreen'
| 'linen'
| 'magenta'
| 'maroon'
| 'mediumaquamarine'
| 'mediumblue'
| 'mediumorchid'
| 'mediumpurple'
| 'mediumseagreen'
| 'mediumslateblue'
| 'mediumslateblue'
| 'mediumspringgreen'
| 'mediumturquoise'
| 'mediumvioletred'
| 'midnightblue'
| 'mintcream'
| 'mistyrose'
| 'moccasin'
| 'navajowhite'
| 'navy'
| 'oldlace'
| 'olive'
| 'olivedrab'
| 'orange'
| 'orangered'
| 'orchid'
| 'palegoldenrod'
| 'palegreen'
| 'paleturquoise'
| 'palevioletred'
| 'papayawhip'
| 'peachpuff'
| 'peru'
| 'pink'
| 'plum'
| 'powderblue'
| 'purple'
| 'rebeccapurple'
| 'red'
| 'rosybrown'
| 'royalblue'
| 'saddlebrown'
| 'salmon'
| 'sandybrown'
| 'seagreen'
| 'seashell'
| 'sienna'
| 'silver'
| 'skyblue'
| 'slateblue'
| 'slategray'
| 'snow'
| 'springgreen'
| 'steelblue'
| 'tan'
| 'teal'
| 'thistle'
| 'tomato'
| 'turquoise'
| 'violet'
| 'wheat'
| 'white'
| 'whitesmoke'
| 'yellow'
| 'yellowgreen';
export const htmlColorNames: NamedColor[] = [
'aliceblue',
'antiquewhite',
'aqua',
'aquamarine',
'azure',
'beige',
'bisque',
'black',
'blanchedalmond',
'blue',
'blueviolet',
'brown',
'burlywood',
'cadetblue',
'chartreuse',
'chocolate',
'coral',
'cornflowerblue',
'cornsilk',
'crimson',
'cyan',
'darkblue',
'darkcyan',
'darkgoldenrod',
'darkgray',
'darkgreen',
'darkkhaki',
'darkmagenta',
'darkolivegreen',
'darkorange',
'darkorchid',
'darkred',
'darksalmon',
'darkseagreen',
'darkslateblue',
'darkslategray',
'darkturquoise',
'darkviolet',
'deeppink',
'deepskyblue',
'dimgray',
'dodgerblue',
'firebrick',
'floralwhite',
'forestgreen',
'fuchsia',
'gainsboro',
'ghostwhite',
'gold',
'goldenrod',
'gray',
'green',
'greenyellow',
'honeydew',
'hotpink',
'indianred',
'indigo',
'ivory',
'khaki',
'lavender',
'lavenderblush',
'lawngreen',
'lemonchiffon',
'lightblue',
'lightcoral',
'lightcyan',
'lightgoldenrodyellow',
'lightgray',
'lightgreen',
'lightpink',
'lightsalmon',
'lightsalmon',
'lightseagreen',
'lightskyblue',
'lightslategray',
'lightsteelblue',
'lightyellow',
'lime',
'limegreen',
'linen',
'magenta',
'maroon',
'mediumaquamarine',
'mediumblue',
'mediumorchid',
'mediumpurple',
'mediumseagreen',
'mediumslateblue',
'mediumslateblue',
'mediumspringgreen',
'mediumturquoise',
'mediumvioletred',
'midnightblue',
'mintcream',
'mistyrose',
'moccasin',
'navajowhite',
'navy',
'oldlace',
'olive',
'olivedrab',
'orange',
'orangered',
'orchid',
'palegoldenrod',
'palegreen',
'paleturquoise',
'palevioletred',
'papayawhip',
'peachpuff',
'peru',
'pink',
'plum',
'powderblue',
'purple',
'rebeccapurple',
'red',
'rosybrown',
'royalblue',
'saddlebrown',
'salmon',
'sandybrown',
'seagreen',
'seashell',
'sienna',
'silver',
'skyblue',
'slateblue',
'slategray',
'snow',
'springgreen',
'steelblue',
'tan',
'teal',
'thistle',
'tomato',
'turquoise',
'violet',
'wheat',
'white',
'whitesmoke',
'yellow',
'yellowgreen',
];

View file

@ -1,3 +1,5 @@
import { type NamedColor, htmlColorNames } from './colornames.js';
/// <reference types="astro/astro-jsx" /> /// <reference types="astro/astro-jsx" />
export type InputFormat = export type InputFormat =
| 'heic' | 'heic'
@ -10,16 +12,35 @@ export type InputFormat =
| 'webp' | 'webp'
| 'gif'; | 'gif';
export type OutputFormat = 'avif' | 'jpeg' | 'png' | 'webp'; export type OutputFormatSupportsAlpha = 'avif' | 'png' | 'webp';
export type OutputFormat = OutputFormatSupportsAlpha | 'jpeg';
export type ColorDefinition =
| NamedColor
| `#${string}`
| `rgb(${number}, ${number}, ${number})`
| `rgb(${number},${number},${number})`;
export function isOutputFormat(value: string): value is OutputFormat { export function isOutputFormat(value: string): value is OutputFormat {
return ['avif', 'jpeg', 'png', 'webp'].includes(value); return ['avif', 'jpeg', 'png', 'webp'].includes(value);
} }
export function isOutputFormatSupportsAlpha(value: string): value is OutputFormatSupportsAlpha {
return ['avif', 'png', 'webp'].includes(value);
}
export function isAspectRatioString(value: string): value is `${number}:${number}` { export function isAspectRatioString(value: string): value is `${number}:${number}` {
return /^\d*:\d*$/.test(value); return /^\d*:\d*$/.test(value);
} }
export function isColor(value: string): value is ColorDefinition {
return (
(htmlColorNames as string[]).includes(value.toLowerCase()) ||
/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(value) ||
/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(value)
);
}
export function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) { export function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) {
if (!aspectRatio) { if (!aspectRatio) {
return undefined; return undefined;
@ -75,6 +96,15 @@ export interface TransformOptions {
* @example "16:9" - strings can be used in the format of `{ratioWidth}:{ratioHeight}`. * @example "16:9" - strings can be used in the format of `{ratioWidth}:{ratioHeight}`.
*/ */
aspectRatio?: number | `${number}:${number}`; aspectRatio?: number | `${number}:${number}`;
/**
* The background color to use when converting from a transparent image format to a
* non-transparent format. This is useful for converting PNGs to JPEGs.
*
* @example "white" - a named color
* @example "#ffffff" - a hex color
* @example "rgb(255, 255, 255)" - an rgb color
*/
background?: ColorDefinition;
} }
export interface HostedImageService<T extends TransformOptions = TransformOptions> { export interface HostedImageService<T extends TransformOptions = TransformOptions> {

View file

@ -1,11 +1,11 @@
import sharp from 'sharp'; import sharp from 'sharp';
import { isAspectRatioString, isOutputFormat } from '../loaders/index.js'; import { isAspectRatioString, isColor, isOutputFormat } from '../loaders/index.js';
import type { OutputFormat, SSRImageService, TransformOptions } from './index.js'; import type { OutputFormat, SSRImageService, TransformOptions } from './index.js';
class SharpService implements SSRImageService { class SharpService implements SSRImageService {
async getImageAttributes(transform: TransformOptions) { async getImageAttributes(transform: TransformOptions) {
// strip off the known attributes // strip off the known attributes
const { width, height, src, format, quality, aspectRatio, ...rest } = transform; const { width, height, src, format, quality, aspectRatio, background, ...rest } = transform;
return { return {
...rest, ...rest,
@ -37,6 +37,10 @@ class SharpService implements SSRImageService {
searchParams.append('ar', transform.aspectRatio.toString()); searchParams.append('ar', transform.aspectRatio.toString());
} }
if (transform.background) {
searchParams.append('bg', transform.background);
}
return { searchParams }; return { searchParams };
} }
@ -72,6 +76,13 @@ class SharpService implements SSRImageService {
} }
} }
if (searchParams.has('bg')) {
const background = searchParams.get('bg')!;
if (isColor(background)) {
transform.background = background;
}
}
return transform; return transform;
} }
@ -87,6 +98,11 @@ class SharpService implements SSRImageService {
sharpImage.resize(width, height); sharpImage.resize(width, height);
} }
// remove alpha channel and replace with background color if requested
if (transform.background) {
sharpImage.flatten({ background: transform.background });
}
if (transform.format) { if (transform.format) {
sharpImage.toFormat(transform.format, { quality: transform.quality }); sharpImage.toFormat(transform.format, { quality: transform.quality });
} }

View file

@ -0,0 +1,116 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import sharp from 'sharp';
import { fileURLToPath } from 'url';
import { loadFixture } from './test-utils.js';
describe('SSG image with background - dev', function () {
let fixture;
let devServer;
let $;
before(async () => {
fixture = await loadFixture({ root: './fixtures/background-color-image/' });
devServer = await fixture.startDevServer();
const html = await fixture.fetch('/').then((res) => res.text());
$ = cheerio.load(html);
});
after(async () => {
await devServer.stop();
});
[
{
title: 'Named color',
id: '#named',
bg: 'dimgray',
},
{
title: 'Hex color',
id: '#hex',
bg: '#696969',
},
{
title: 'Hex color short',
id: '#hex-short',
bg: '#666',
},
{
title: 'RGB color',
id: '#rgb',
bg: 'rgb(105,105,105)',
},
{
title: 'RGB color with spaces',
id: '#rgb-spaced',
bg: 'rgb(105, 105, 105)',
},
].forEach(({ title, id, bg }) => {
it(title, async () => {
const image = $(id);
const src = image.attr('src');
const [_, params] = src.split('?');
const searchParams = new URLSearchParams(params);
expect(searchParams.get('bg')).to.equal(bg);
});
});
});
describe('SSG image with background - build', function () {
let fixture;
let $;
let html;
before(async () => {
fixture = await loadFixture({ root: './fixtures/background-color-image/' });
await fixture.build();
html = await fixture.readFile('/index.html');
$ = cheerio.load(html);
});
async function verifyImage(pathname, expectedBg) {
const url = new URL('./fixtures/background-color-image/dist/' + pathname, import.meta.url);
const dist = fileURLToPath(url);
const data = await sharp(dist).raw().toBuffer();
// check that the first RGB pixel indeed has the requested background color
expect(data[0]).to.equal(expectedBg[0]);
expect(data[1]).to.equal(expectedBg[1]);
expect(data[2]).to.equal(expectedBg[2]);
}
[
{
title: 'Named color',
id: '#named',
bg: [105, 105, 105],
},
{
title: 'Hex color',
id: '#hex',
bg: [105, 105, 105],
},
{
title: 'Hex color short',
id: '#hex-short',
bg: [102, 102, 102],
},
{
title: 'RGB color',
id: '#rgb',
bg: [105, 105, 105],
},
{
title: 'RGB color with spaces',
id: '#rgb-spaced',
bg: [105, 105, 105],
},
].forEach(({ title, id, bg }) => {
it(title, async () => {
const image = $(id);
const src = image.attr('src');
await verifyImage(src, bg);
});
});
});

View file

@ -0,0 +1,98 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
import testAdapter from '../../../astro/test/test-adapter.js';
let fixture;
describe('SSR image with background', function () {
before(async () => {
fixture = await loadFixture({
root: './fixtures/background-color-image/',
adapter: testAdapter({ streaming: false }),
output: 'server',
});
await fixture.build();
});
[
{
title: 'Named color',
id: '#named',
query: {
f: 'jpeg',
w: '256',
h: '256',
href: /^\/assets\/file-icon.\w{8}.png/,
bg: 'dimgray',
},
},
{
title: 'Hex color',
id: '#hex',
query: {
f: 'avif',
w: '256',
h: '256',
href: /^\/assets\/file-icon.\w{8}.png/,
bg: '#696969',
},
},
{
title: 'Hex color short',
id: '#hex-short',
query: {
f: 'png',
w: '256',
h: '256',
href: /^\/assets\/file-icon.\w{8}.png/,
bg: '#666',
},
},
{
title: 'RGB color',
id: '#rgb',
query: {
f: 'webp',
w: '256',
h: '256',
href: /^\/assets\/file-icon.\w{8}.png/,
bg: 'rgb(105,105,105)',
},
},
{
title: 'RGB color with spaces',
id: '#rgb-spaced',
query: {
f: 'jpeg',
w: '256',
h: '256',
href: /^\/assets\/file-icon.\w{8}.png/,
bg: 'rgb(105, 105, 105)',
},
},
].forEach(({ title, id, query }) => {
it(title, async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
const html = await response.text();
const $ = cheerio.load(html);
const image = $(id);
const src = image.attr('src');
const [_, params] = src.split('?');
const searchParams = new URLSearchParams(params);
for (const [key, value] of Object.entries(query)) {
if (typeof value === 'string') {
expect(searchParams.get(key)).to.equal(value);
} else {
expect(searchParams.get(key)).to.match(value);
}
}
});
});
});

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
// https://astro.build/config
export default defineConfig({
site: 'http://localhost:3000',
integrations: [image({ logLevel: 'silent' })]
});

View file

@ -0,0 +1,10 @@
{
"name": "@test/background-color-image",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/image": "workspace:*",
"@astrojs/node": "workspace:*",
"astro": "workspace:*"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,44 @@
import { createServer } from 'http';
import fs from 'fs';
import mime from 'mime';
import { handler as ssrHandler } from '../dist/server/entry.mjs';
const clientRoot = new URL('../dist/client/', import.meta.url);
async function handle(req, res) {
ssrHandler(req, res, async (err) => {
if (err) {
res.writeHead(500);
res.end(err.stack);
return;
}
let local = new URL('.' + req.url, clientRoot);
try {
const data = await fs.promises.readFile(local);
res.writeHead(200, {
'Content-Type': mime.getType(req.url),
});
res.end(data);
} catch {
res.writeHead(404);
res.end();
}
});
}
const server = createServer((req, res) => {
handle(req, res).catch((err) => {
console.error(err);
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end(err.toString());
});
});
server.listen(8085);
console.log('Serving at http://localhost:8085');
// Silence weird <time> warning
console.error = () => {};

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,21 @@
---
import { Image } from '@astrojs/image/components';
---
<html>
<head>
<!-- Head Stuff -->
</head>
<body>
<Image id="named" src={import('../assets/file-icon.png')} width={256} format="jpeg" background="dimgray" alt="named" />
<br />
<Image id="hex" src={import('../assets/file-icon.png')} width={256} format="avif" background="#696969" alt="hex" />
<br />
<Image id="hex-short" src={import('../assets/file-icon.png')} width={256} background="#666" alt="hex-short" />
<br />
<Image id="rgb" src={import('../assets/file-icon.png')} width={256} format="webp" background="rgb(105,105,105)" alt="rgb" />
<br />
<Image id="rgb-spaced" src={import('../assets/file-icon.png')} width={256} format="jpeg" background="rgb(105, 105, 105)" alt="rgb-spaced" />
<br />
</body>
</html>

View file

@ -21,6 +21,8 @@ import { Image } from '@astrojs/image/components';
<br /> <br />
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" alt="query" /> <Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" alt="query" />
<br /> <br />
<Image id="bg-color" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="jpeg" alt="Google" background="#333333" />
<br />
<Image id="ipsum" src="https://picsum.photos/200/300" width={200} height={300} alt="ipsum" format="jpeg" /> <Image id="ipsum" src="https://picsum.photos/200/300" width={200} height={300} alt="ipsum" format="jpeg" />
</body> </body>
</html> </html>

View file

@ -15,6 +15,8 @@ import { Picture } from '@astrojs/image/components';
<Picture id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" sizes="(min-width: 640px) 50vw, 100vw" widths={[272, 544]} aspectRatio={544/184} alt="Google logo" formats={["avif", "webp", "png"]} /> <Picture id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" sizes="(min-width: 640px) 50vw, 100vw" widths={[272, 544]} aspectRatio={544/184} alt="Google logo" formats={["avif", "webp", "png"]} />
<br /> <br />
<Picture id='inline' src={import('../assets/social.jpg')} sizes="(min-width: 640px) 50vw, 100vw" widths={[253, 506]} alt="Inline social image" /> <Picture id='inline' src={import('../assets/social.jpg')} sizes="(min-width: 640px) 50vw, 100vw" widths={[253, 506]} alt="Inline social image" />
<br />
<Picture id="bg-color" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" sizes="(min-width: 640px) 50vw, 100vw" widths={[272, 544]} aspectRatio={544/184} alt="Google logo" background="rgb(51, 51, 51)" formats={['avif', 'jpeg']} />
<br /> <br />
<Picture id="ipsum" src="https://picsum.photos/200/300" sizes="100vw" widths={[100, 200]} aspectRatio={2/3} formats={["avif", "webp", "jpg"]} alt="ipsum" /> <Picture id="ipsum" src="https://picsum.photos/200/300" sizes="100vw" widths={[100, 200]} aspectRatio={2/3} formats={["avif", "webp", "jpg"]} alt="ipsum" />
</body> </body>

View file

@ -15,3 +15,5 @@ import { Image } from '@astrojs/image/components';
<Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" /> <Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" />
<br /> <br />
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" alt="query" /> <Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" alt="query" />
<br />
<Image id="bg-color" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="jpeg" alt="Google" background="#333333" />

View file

@ -66,6 +66,18 @@ describe('SSG images - dev', function () {
url: '/_image', url: '/_image',
query: { f: 'webp', w: '768', h: '414', href: '/hero.jpg' }, query: { f: 'webp', w: '768', h: '414', href: '/hero.jpg' },
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'jpeg',
w: '544',
h: '184',
bg: '#333333',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
},
].forEach(({ title, id, url, query }) => { ].forEach(({ title, id, url, query }) => {
it(title, () => { it(title, () => {
const image = $(id); const image = $(id);
@ -147,6 +159,18 @@ describe('SSG images with subpath - dev', function () {
url: '/_image', url: '/_image',
query: { f: 'webp', w: '768', h: '414', href: '/hero.jpg' }, query: { f: 'webp', w: '768', h: '414', href: '/hero.jpg' },
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'jpeg',
w: '544',
h: '184',
bg: '#333333',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
},
].forEach(({ title, id, url, query }) => { ].forEach(({ title, id, url, query }) => {
it(title, () => { it(title, () => {
const image = $(id); const image = $(id);
@ -222,6 +246,12 @@ describe('SSG images - build', function () {
regex: /^\/hero_\w{4,10}.webp/, regex: /^\/hero_\w{4,10}.webp/,
size: { width: 768, height: 414, type: 'webp' }, size: { width: 768, height: 414, type: 'webp' },
}, },
{
title: 'Remote images',
id: '#bg-color',
regex: /^\/googlelogo_color_272x92dp_\w{4,10}.jpeg/,
size: { width: 544, height: 184, type: 'jpg' },
},
].forEach(({ title, id, regex, size }) => { ].forEach(({ title, id, regex, size }) => {
it(title, () => { it(title, () => {
const image = $(id); const image = $(id);
@ -292,6 +322,12 @@ describe('SSG images with subpath - build', function () {
regex: /^\/docs\/hero_\w{4,10}.webp/, regex: /^\/docs\/hero_\w{4,10}.webp/,
size: { width: 768, height: 414, type: 'webp' }, size: { width: 768, height: 414, type: 'webp' },
}, },
{
title: 'Remote images',
id: '#bg-color',
regex: /^\/docs\/googlelogo_color_272x92dp_\w{4,10}.jpeg/,
size: { width: 544, height: 184, type: 'jpg' },
},
].forEach(({ title, id, regex, size }) => { ].forEach(({ title, id, regex, size }) => {
it(title, () => { it(title, () => {
const image = $(id); const image = $(id);

View file

@ -72,6 +72,18 @@ describe('SSR images - build', async function () {
url: '/_image', url: '/_image',
query: { f: 'webp', w: '768', h: '414', href: '/hero.jpg' }, query: { f: 'webp', w: '768', h: '414', href: '/hero.jpg' },
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'jpeg',
w: '544',
h: '184',
bg: '#333333',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
},
].forEach(({ title, id, url, query }) => { ].forEach(({ title, id, url, query }) => {
it(title, async () => { it(title, async () => {
const app = await fixture.loadTestAdapterApp(); const app = await fixture.loadTestAdapterApp();
@ -176,6 +188,18 @@ describe('SSR images with subpath - build', function () {
url: '/_image', url: '/_image',
query: { f: 'webp', w: '768', h: '414', href: '/hero.jpg' }, query: { f: 'webp', w: '768', h: '414', href: '/hero.jpg' },
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'jpeg',
w: '544',
h: '184',
bg: '#333333',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
},
].forEach(({ title, id, url, query }) => { ].forEach(({ title, id, url, query }) => {
it(title, async () => { it(title, async () => {
const app = await fixture.loadTestAdapterApp(); const app = await fixture.loadTestAdapterApp();

View file

@ -81,6 +81,19 @@ describe('SSR images - dev', function () {
}, },
contentType: 'image/webp', contentType: 'image/webp',
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'jpeg',
w: '544',
h: '184',
bg: '#333333',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
contentType: 'image/jpeg',
},
].forEach(({ title, id, url, query, contentType }) => { ].forEach(({ title, id, url, query, contentType }) => {
it(title, async () => { it(title, async () => {
const image = $(id); const image = $(id);
@ -183,6 +196,19 @@ describe('SSR images with subpath - dev', function () {
}, },
contentType: 'image/webp', contentType: 'image/webp',
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'jpeg',
w: '544',
h: '184',
bg: '#333333',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
contentType: 'image/jpeg',
},
].forEach(({ title, id, url, query, contentType }) => { ].forEach(({ title, id, url, query, contentType }) => {
it(title, async () => { it(title, async () => {
const image = $(id); const image = $(id);

View file

@ -56,6 +56,19 @@ describe('SSG pictures - dev', function () {
query: { f: 'jpg', w: '768', h: '414', href: '/hero.jpg' }, query: { f: 'jpg', w: '768', h: '414', href: '/hero.jpg' },
alt: 'Hero image', alt: 'Hero image',
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'png',
w: '544',
h: '184',
bg: 'rgb(51, 51, 51)',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
alt: 'Google logo',
},
].forEach(({ title, id, url, query, alt }) => { ].forEach(({ title, id, url, query, alt }) => {
it(title, () => { it(title, () => {
const sources = $(`${id} source`); const sources = $(`${id} source`);

View file

@ -60,6 +60,19 @@ describe('SSR pictures - build', function () {
query: { f: 'jpg', w: '768', h: '414', href: '/hero.jpg' }, query: { f: 'jpg', w: '768', h: '414', href: '/hero.jpg' },
alt: 'Hero image', alt: 'Hero image',
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'png',
w: '544',
h: '184',
bg: 'rgb(51, 51, 51)',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
alt: 'Google logo',
},
].forEach(({ title, id, url, query }) => { ].forEach(({ title, id, url, query }) => {
it(title, async () => { it(title, async () => {
const app = await fixture.loadTestAdapterApp(); const app = await fixture.loadTestAdapterApp();
@ -151,6 +164,19 @@ describe('SSR pictures with subpath - build', function () {
query: { f: 'jpg', w: '768', h: '414', href: '/hero.jpg' }, query: { f: 'jpg', w: '768', h: '414', href: '/hero.jpg' },
alt: 'Hero image', alt: 'Hero image',
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'png',
w: '544',
h: '184',
bg: 'rgb(51, 51, 51)',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
alt: 'Google logo',
},
].forEach(({ title, id, url, query }) => { ].forEach(({ title, id, url, query }) => {
it(title, async () => { it(title, async () => {
const app = await fixture.loadTestAdapterApp(); const app = await fixture.loadTestAdapterApp();

View file

@ -80,6 +80,20 @@ describe('SSR pictures - dev', function () {
contentType: 'image/jpeg', contentType: 'image/jpeg',
alt: 'Hero image', alt: 'Hero image',
}, },
{
title: 'Background color',
id: '#bg-color',
url: '/_image',
query: {
f: 'png',
w: '544',
h: '184',
bg: 'rgb(51, 51, 51)',
href: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
},
contentType: 'image/png',
alt: 'Google logo',
},
].forEach(({ title, id, url, query, alt, contentType }) => { ].forEach(({ title, id, url, query, alt, contentType }) => {
it(title, async () => { it(title, async () => {
const sources = $(`${id} source`); const sources = $(`${id} source`);

View file

@ -14,6 +14,7 @@ describe('Sharp service', () => {
['width & height', { src, height: 400, width: 200 }], ['width & height', { src, height: 400, width: 200 }],
['aspect ratio string', { src, aspectRatio: '16:9' }], ['aspect ratio string', { src, aspectRatio: '16:9' }],
['aspect ratio float', { src, aspectRatio: 1.7 }], ['aspect ratio float', { src, aspectRatio: 1.7 }],
['background color', { src, format: 'jpeg', background: '#333333' }],
].forEach(([description, props]) => { ].forEach(([description, props]) => {
it(description, async () => { it(description, async () => {
const { searchParams } = await sharp.serializeTransform(props); const { searchParams } = await sharp.serializeTransform(props);
@ -31,6 +32,7 @@ describe('Sharp service', () => {
verifyProp(props.width, 'w'); verifyProp(props.width, 'w');
verifyProp(props.height, 'h'); verifyProp(props.height, 'h');
verifyProp(props.aspectRatio, 'ar'); verifyProp(props.aspectRatio, 'ar');
verifyProp(props.background, 'bg');
}); });
}); });
}); });
@ -48,6 +50,11 @@ describe('Sharp service', () => {
['width & height', `w=200&h=400&href=${href}`, { src, height: 400, width: 200 }], ['width & height', `w=200&h=400&href=${href}`, { src, height: 400, width: 200 }],
['aspect ratio string', `ar=16:9&href=${href}`, { src, aspectRatio: '16:9' }], ['aspect ratio string', `ar=16:9&href=${href}`, { src, aspectRatio: '16:9' }],
['aspect ratio float', `ar=1.7&href=${href}`, { src, aspectRatio: 1.7 }], ['aspect ratio float', `ar=1.7&href=${href}`, { src, aspectRatio: 1.7 }],
[
'background color',
`f=jpeg&bg=%23333333&href=${href}`,
{ src, format: 'jpeg', background: '#333333' },
],
].forEach(([description, params, expected]) => { ].forEach(([description, params, expected]) => {
it(description, async () => { it(description, async () => {
const searchParams = new URLSearchParams(params); const searchParams = new URLSearchParams(params);

View file

@ -49,6 +49,12 @@ describe('Images in MDX - build', function () {
regex: /^\/hero_\w{4,10}.webp/, regex: /^\/hero_\w{4,10}.webp/,
size: { width: 768, height: 414, type: 'webp' }, size: { width: 768, height: 414, type: 'webp' },
}, },
{
title: 'Background color',
id: '#bg-color',
regex: /^\/googlelogo_color_272x92dp_\w{4,10}.jpeg/,
size: { width: 544, height: 184, type: 'jpg' },
},
].forEach(({ title, id, regex, size }) => { ].forEach(({ title, id, regex, size }) => {
it(title, () => { it(title, () => {
const image = $(id); const image = $(id);

File diff suppressed because it is too large Load diff