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:
parent
93c3aee01c
commit
e4348a4eb4
28 changed files with 1623 additions and 1012 deletions
5
.changeset/eleven-baboons-try.md
Normal file
5
.changeset/eleven-baboons-try.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/image': minor
|
||||
---
|
||||
|
||||
Added a `background` option to specify a background color to replace transparent pixels (alpha layer).
|
|
@ -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}`.
|
||||
|
||||
#### 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 /`>
|
||||
|
||||
#### 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.
|
||||
|
||||
#### 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`
|
||||
|
||||
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.
|
||||
|
|
|
@ -27,6 +27,7 @@ interface RemoteImageProps
|
|||
widths: number[];
|
||||
aspectRatio: TransformOptions['aspectRatio'];
|
||||
formats?: OutputFormat[];
|
||||
background: TransformOptions['background'];
|
||||
}
|
||||
|
||||
export type Props = LocalImageProps | RemoteImageProps;
|
||||
|
@ -37,6 +38,7 @@ const {
|
|||
sizes,
|
||||
widths,
|
||||
aspectRatio,
|
||||
background,
|
||||
formats = ['avif', 'webp'],
|
||||
loading = 'lazy',
|
||||
decoding = 'async',
|
||||
|
@ -47,7 +49,7 @@ if (alt === undefined || alt === null) {
|
|||
warnForMissingAlt();
|
||||
}
|
||||
|
||||
const { image, sources } = await getPicture({ src, widths, formats, aspectRatio });
|
||||
const { image, sources } = await getPicture({ src, widths, formats, aspectRatio, background });
|
||||
---
|
||||
|
||||
<picture {...attrs}>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
/// <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 sharp from '../loaders/sharp.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
|
||||
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) {
|
||||
// neither dimension was provided, use the file metadata
|
||||
|
@ -86,6 +91,7 @@ async function resolveTransform(input: GetImageTransform): Promise<TransformOpti
|
|||
height,
|
||||
aspectRatio,
|
||||
format: format as OutputFormat,
|
||||
background: background as ColorDefinition | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface GetPictureParams {
|
|||
widths: number[];
|
||||
formats: OutputFormat[];
|
||||
aspectRatio?: TransformOptions['aspectRatio'];
|
||||
background?: TransformOptions['background'];
|
||||
}
|
||||
|
||||
export interface GetPictureResult {
|
||||
|
@ -64,6 +65,7 @@ export async function getPicture(params: GetPictureParams): Promise<GetPictureRe
|
|||
format,
|
||||
width,
|
||||
height: Math.round(width / aspectRatio!),
|
||||
background: params.background,
|
||||
});
|
||||
return `${img.src} ${width}w`;
|
||||
})
|
||||
|
@ -83,6 +85,7 @@ export async function getPicture(params: GetPictureParams): Promise<GetPictureRe
|
|||
width: Math.max(...widths),
|
||||
aspectRatio,
|
||||
format: allFormats[allFormats.length - 1],
|
||||
background: params.background,
|
||||
});
|
||||
|
||||
const sources = await Promise.all(allFormats.map((format) => getSource(format)));
|
||||
|
|
290
packages/integrations/image/src/loaders/colornames.ts
Normal file
290
packages/integrations/image/src/loaders/colornames.ts
Normal 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',
|
||||
];
|
|
@ -1,3 +1,5 @@
|
|||
import { type NamedColor, htmlColorNames } from './colornames.js';
|
||||
|
||||
/// <reference types="astro/astro-jsx" />
|
||||
export type InputFormat =
|
||||
| 'heic'
|
||||
|
@ -10,16 +12,35 @@ export type InputFormat =
|
|||
| 'webp'
|
||||
| '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 {
|
||||
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}` {
|
||||
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']) {
|
||||
if (!aspectRatio) {
|
||||
return undefined;
|
||||
|
@ -75,6 +96,15 @@ export interface TransformOptions {
|
|||
* @example "16:9" - strings can be used in the format of `{ratioWidth}:{ratioHeight}`.
|
||||
*/
|
||||
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> {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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';
|
||||
|
||||
class SharpService implements SSRImageService {
|
||||
async getImageAttributes(transform: TransformOptions) {
|
||||
// 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 {
|
||||
...rest,
|
||||
|
@ -37,6 +37,10 @@ class SharpService implements SSRImageService {
|
|||
searchParams.append('ar', transform.aspectRatio.toString());
|
||||
}
|
||||
|
||||
if (transform.background) {
|
||||
searchParams.append('bg', transform.background);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -87,6 +98,11 @@ class SharpService implements SSRImageService {
|
|||
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) {
|
||||
sharpImage.toFormat(transform.format, { quality: transform.quality });
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
8
packages/integrations/image/test/fixtures/background-color-image/astro.config.mjs
vendored
Normal file
8
packages/integrations/image/test/fixtures/background-color-image/astro.config.mjs
vendored
Normal 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' })]
|
||||
});
|
10
packages/integrations/image/test/fixtures/background-color-image/package.json
vendored
Normal file
10
packages/integrations/image/test/fixtures/background-color-image/package.json
vendored
Normal 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:*"
|
||||
}
|
||||
}
|
BIN
packages/integrations/image/test/fixtures/background-color-image/public/favicon.ico
vendored
Normal file
BIN
packages/integrations/image/test/fixtures/background-color-image/public/favicon.ico
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
44
packages/integrations/image/test/fixtures/background-color-image/server/server.mjs
vendored
Normal file
44
packages/integrations/image/test/fixtures/background-color-image/server/server.mjs
vendored
Normal 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 = () => {};
|
BIN
packages/integrations/image/test/fixtures/background-color-image/src/assets/file-icon.png
vendored
Normal file
BIN
packages/integrations/image/test/fixtures/background-color-image/src/assets/file-icon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
21
packages/integrations/image/test/fixtures/background-color-image/src/pages/index.astro
vendored
Normal file
21
packages/integrations/image/test/fixtures/background-color-image/src/pages/index.astro
vendored
Normal 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>
|
|
@ -21,6 +21,8 @@ import { Image } from '@astrojs/image/components';
|
|||
<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" />
|
||||
<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" />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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"]} />
|
||||
<br />
|
||||
<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 />
|
||||
<Picture id="ipsum" src="https://picsum.photos/200/300" sizes="100vw" widths={[100, 200]} aspectRatio={2/3} formats={["avif", "webp", "jpg"]} alt="ipsum" />
|
||||
</body>
|
||||
|
|
|
@ -15,3 +15,5 @@ import { Image } from '@astrojs/image/components';
|
|||
<Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" />
|
||||
<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" />
|
||||
<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" />
|
||||
|
|
|
@ -66,6 +66,18 @@ describe('SSG images - dev', function () {
|
|||
url: '/_image',
|
||||
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 }) => {
|
||||
it(title, () => {
|
||||
const image = $(id);
|
||||
|
@ -147,6 +159,18 @@ describe('SSG images with subpath - dev', function () {
|
|||
url: '/_image',
|
||||
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 }) => {
|
||||
it(title, () => {
|
||||
const image = $(id);
|
||||
|
@ -222,6 +246,12 @@ describe('SSG images - build', function () {
|
|||
regex: /^\/hero_\w{4,10}.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 }) => {
|
||||
it(title, () => {
|
||||
const image = $(id);
|
||||
|
@ -292,6 +322,12 @@ describe('SSG images with subpath - build', function () {
|
|||
regex: /^\/docs\/hero_\w{4,10}.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 }) => {
|
||||
it(title, () => {
|
||||
const image = $(id);
|
||||
|
|
|
@ -72,6 +72,18 @@ describe('SSR images - build', async function () {
|
|||
url: '/_image',
|
||||
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 }) => {
|
||||
it(title, async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
@ -176,6 +188,18 @@ describe('SSR images with subpath - build', function () {
|
|||
url: '/_image',
|
||||
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 }) => {
|
||||
it(title, async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
|
|
@ -81,6 +81,19 @@ describe('SSR images - dev', function () {
|
|||
},
|
||||
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 }) => {
|
||||
it(title, async () => {
|
||||
const image = $(id);
|
||||
|
@ -183,6 +196,19 @@ describe('SSR images with subpath - dev', function () {
|
|||
},
|
||||
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 }) => {
|
||||
it(title, async () => {
|
||||
const image = $(id);
|
||||
|
|
|
@ -56,6 +56,19 @@ describe('SSG pictures - dev', function () {
|
|||
query: { f: 'jpg', w: '768', h: '414', href: '/hero.jpg' },
|
||||
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 }) => {
|
||||
it(title, () => {
|
||||
const sources = $(`${id} source`);
|
||||
|
|
|
@ -60,6 +60,19 @@ describe('SSR pictures - build', function () {
|
|||
query: { f: 'jpg', w: '768', h: '414', href: '/hero.jpg' },
|
||||
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 }) => {
|
||||
it(title, async () => {
|
||||
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' },
|
||||
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 }) => {
|
||||
it(title, async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
|
|
@ -80,6 +80,20 @@ describe('SSR pictures - dev', function () {
|
|||
contentType: 'image/jpeg',
|
||||
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 }) => {
|
||||
it(title, async () => {
|
||||
const sources = $(`${id} source`);
|
||||
|
|
|
@ -14,6 +14,7 @@ describe('Sharp service', () => {
|
|||
['width & height', { src, height: 400, width: 200 }],
|
||||
['aspect ratio string', { src, aspectRatio: '16:9' }],
|
||||
['aspect ratio float', { src, aspectRatio: 1.7 }],
|
||||
['background color', { src, format: 'jpeg', background: '#333333' }],
|
||||
].forEach(([description, props]) => {
|
||||
it(description, async () => {
|
||||
const { searchParams } = await sharp.serializeTransform(props);
|
||||
|
@ -31,6 +32,7 @@ describe('Sharp service', () => {
|
|||
verifyProp(props.width, 'w');
|
||||
verifyProp(props.height, 'h');
|
||||
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 }],
|
||||
['aspect ratio string', `ar=16:9&href=${href}`, { src, aspectRatio: '16:9' }],
|
||||
['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]) => {
|
||||
it(description, async () => {
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
|
|
@ -49,6 +49,12 @@ describe('Images in MDX - build', function () {
|
|||
regex: /^\/hero_\w{4,10}.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 }) => {
|
||||
it(title, () => {
|
||||
const image = $(id);
|
||||
|
|
1762
pnpm-lock.yaml
1762
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue