From e4348a4eb49466579204eb5f7fb8823736f467c0 Mon Sep 17 00:00:00 2001 From: Valentin Bersier Date: Wed, 7 Sep 2022 19:22:11 +0200 Subject: [PATCH] @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 --- .changeset/eleven-baboons-try.md | 5 + packages/integrations/image/README.md | 54 +- .../image/components/Picture.astro | 4 +- .../integrations/image/src/lib/get-image.ts | 10 +- .../integrations/image/src/lib/get-picture.ts | 3 + .../image/src/loaders/colornames.ts | 290 +++ .../integrations/image/src/loaders/index.ts | 32 +- .../integrations/image/src/loaders/sharp.ts | 20 +- .../test/background-color-image-ssg.test.js | 116 ++ .../test/background-color-image-ssr.test.js | 98 + .../background-color-image/astro.config.mjs | 8 + .../background-color-image/package.json | 10 + .../background-color-image/public/favicon.ico | Bin 0 -> 4286 bytes .../background-color-image/server/server.mjs | 44 + .../src/assets/file-icon.png | Bin 0 -> 7746 bytes .../src/pages/index.astro | 21 + .../basic-image/src/pages/index.astro | 2 + .../basic-picture/src/pages/index.astro | 2 + .../fixtures/with-mdx/src/pages/index.mdx | 2 + .../integrations/image/test/image-ssg.test.js | 36 + .../image/test/image-ssr-build.test.js | 24 + .../image/test/image-ssr-dev.test.js | 26 + .../image/test/picture-ssg.test.js | 13 + .../image/test/picture-ssr-build.test.js | 26 + .../image/test/picture-ssr-dev.test.js | 14 + .../integrations/image/test/sharp.test.js | 7 + .../integrations/image/test/with-mdx.test.js | 6 + pnpm-lock.yaml | 1762 +++++++---------- 28 files changed, 1623 insertions(+), 1012 deletions(-) create mode 100644 .changeset/eleven-baboons-try.md create mode 100644 packages/integrations/image/src/loaders/colornames.ts create mode 100644 packages/integrations/image/test/background-color-image-ssg.test.js create mode 100644 packages/integrations/image/test/background-color-image-ssr.test.js create mode 100644 packages/integrations/image/test/fixtures/background-color-image/astro.config.mjs create mode 100644 packages/integrations/image/test/fixtures/background-color-image/package.json create mode 100644 packages/integrations/image/test/fixtures/background-color-image/public/favicon.ico create mode 100644 packages/integrations/image/test/fixtures/background-color-image/server/server.mjs create mode 100644 packages/integrations/image/test/fixtures/background-color-image/src/assets/file-icon.png create mode 100644 packages/integrations/image/test/fixtures/background-color-image/src/pages/index.astro diff --git a/.changeset/eleven-baboons-try.md b/.changeset/eleven-baboons-try.md new file mode 100644 index 0000000000..f953ae10c0 --- /dev/null +++ b/.changeset/eleven-baboons-try.md @@ -0,0 +1,5 @@ +--- +'@astrojs/image': minor +--- + +Added a `background` option to specify a background color to replace transparent pixels (alpha layer). diff --git a/packages/integrations/image/README.md b/packages/integrations/image/README.md index 2ecc813640..fda9727e6b 100644 --- a/packages/integrations/image/README.md +++ b/packages/integrations/image/README.md @@ -24,9 +24,9 @@ This integration provides `` and `` components as well as a ba ### Quick Install - + The `astro add` command-line tool automates the installation for you. Run one of the following commands in a new terminal window. (If you aren't sure which package manager you're using, run the first command.) Then, follow the prompts, and type "y" in the terminal (meaning "yes") for each one. - + ```sh # Using NPM npx astro add image @@ -35,13 +35,13 @@ yarn astro add image # Using PNPM pnpm astro add image ``` - + Finally, in the terminal window running Astro, press `CTRL+C` and then restart the dev server. If you run into any issues, [feel free to report them to us on GitHub](https://github.com/withastro/astro/issues) and try the manual installation steps below. ### Manual Install - + First, install the `@astrojs/image` package using your package manager. If you're using npm or aren't sure, run this in the terminal: ```sh npm install @astrojs/image @@ -57,7 +57,7 @@ export default { // ... integrations: [image()], } -``` +``` Then, restart the dev server. ### Update `env.d.ts` @@ -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 + +

+ +**Type:** `ColorDefinition`
+**Default:** `undefined` +

+ +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)`. + ### ` #### 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 + +

+ +**Type:** `ColorDefinition`
+**Default:** `undefined` +

+ +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 `` component to build `` attributes for the transformed image. This helper can be used directly for more complex use cases that aren't currently supported by the `` component. @@ -307,7 +343,7 @@ The integration can be configured to run with a different image service, either ### config.serviceEntryPoint - + The `serviceEntryPoint` should resolve to the image service installed from NPM. The default entry point is `@astrojs/image/sharp`, which resolves to the entry point exported from this integration's `package.json`. ```js @@ -342,7 +378,7 @@ export default { ## Examples ### Local images - + Image files in your project's `src` directory can be imported in frontmatter and passed directly to the `` component. All other properties are optional and will default to the original image file's properties if not provided. ```astro @@ -371,7 +407,7 @@ import heroImage from '../assets/hero.png'; Files in the `/public` directory are always served or copied as-is, with no processing. We recommend that local images are always kept in `src/` so that Astro can transform, optimize and bundle them. But if you absolutely must keep an image in `public/`, use its relative URL path as the image's `src=` attribute. It will be treated as a remote image, which requires an `aspectRatio` attribute. -Alternatively, you can import an image from your `public/` directory in your frontmatter and use a variable in your `src=` attribute. You cannot, however, import this directly inside the component as its `src` value. +Alternatively, you can import an image from your `public/` directory in your frontmatter and use a variable in your `src=` attribute. You cannot, however, import this directly inside the component as its `src` value. For example, use an image located at `public/social.png` in either static or SSR builds like so: @@ -386,7 +422,7 @@ import socialImage from '/social.png'; ``` ### Remote images - + Remote images can be transformed with the `` component. The `` component needs to know the final dimensions for the `` element to avoid content layout shifts. For remote images, this means you must either provide `width` and `height`, or one of the dimensions plus the required `aspectRatio`. ```astro diff --git a/packages/integrations/image/components/Picture.astro b/packages/integrations/image/components/Picture.astro index 7fe43d9dbe..e28f5bf409 100644 --- a/packages/integrations/image/components/Picture.astro +++ b/packages/integrations/image/components/Picture.astro @@ -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 }); --- diff --git a/packages/integrations/image/src/lib/get-image.ts b/packages/integrations/image/src/lib/get-image.ts index 15a0d91dbc..856f9f8c6d 100644 --- a/packages/integrations/image/src/lib/get-image.ts +++ b/packages/integrations/image/src/lib/get-image.ts @@ -1,5 +1,10 @@ /// -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 getSource(format))); diff --git a/packages/integrations/image/src/loaders/colornames.ts b/packages/integrations/image/src/loaders/colornames.ts new file mode 100644 index 0000000000..806e55f31d --- /dev/null +++ b/packages/integrations/image/src/loaders/colornames.ts @@ -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', +]; diff --git a/packages/integrations/image/src/loaders/index.ts b/packages/integrations/image/src/loaders/index.ts index 58a9924a89..c3ca5a59a6 100644 --- a/packages/integrations/image/src/loaders/index.ts +++ b/packages/integrations/image/src/loaders/index.ts @@ -1,3 +1,5 @@ +import { type NamedColor, htmlColorNames } from './colornames.js'; + /// 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 { diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts index 4e7b3f104a..dbb082dba1 100644 --- a/packages/integrations/image/src/loaders/sharp.ts +++ b/packages/integrations/image/src/loaders/sharp.ts @@ -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 }); } diff --git a/packages/integrations/image/test/background-color-image-ssg.test.js b/packages/integrations/image/test/background-color-image-ssg.test.js new file mode 100644 index 0000000000..3c488a3ffc --- /dev/null +++ b/packages/integrations/image/test/background-color-image-ssg.test.js @@ -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); + }); + }); +}); diff --git a/packages/integrations/image/test/background-color-image-ssr.test.js b/packages/integrations/image/test/background-color-image-ssr.test.js new file mode 100644 index 0000000000..ff4c208f8f --- /dev/null +++ b/packages/integrations/image/test/background-color-image-ssr.test.js @@ -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); + } + } + }); + }); +}); diff --git a/packages/integrations/image/test/fixtures/background-color-image/astro.config.mjs b/packages/integrations/image/test/fixtures/background-color-image/astro.config.mjs new file mode 100644 index 0000000000..7dafac3b65 --- /dev/null +++ b/packages/integrations/image/test/fixtures/background-color-image/astro.config.mjs @@ -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' })] +}); diff --git a/packages/integrations/image/test/fixtures/background-color-image/package.json b/packages/integrations/image/test/fixtures/background-color-image/package.json new file mode 100644 index 0000000000..bca4ff1783 --- /dev/null +++ b/packages/integrations/image/test/fixtures/background-color-image/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/background-color-image", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/image": "workspace:*", + "@astrojs/node": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/image/test/fixtures/background-color-image/public/favicon.ico b/packages/integrations/image/test/fixtures/background-color-image/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..578ad458b8906c08fbed84f42b045fea04db89d1 GIT binary patch literal 4286 zcmchZF=!M)6ox0}Fc8GdTHG!cdIY>nA!3n2f|wxIl0rn}Hl#=uf>?-!2r&jMEF^_k zh**lGut*gwBmoNv7AaB&2~nbzULg{WBhPQ{ZVzvF_HL8Cb&hv$_s#qN|IO^o>?+mA zuTW6tU%k~z<&{z+7$G%*nRsTcEO|90xy<-G5&JTt%CgZZCDT4%R?+{Vd^wh>P8_)} z`+dF$HQb9!>1o`Ivn;GInlCw{9T@Rt%q+d^T3Ke%cxkk;$v`{s^zCB9nHAv6w$Vbn z8fb<+eQTNM`;rf9#obfGnV#3+OQEUv4gU;{oA@zol%keY9-e>4W>p7AHmH~&!P7f7!Uj` zwgFeQ=<3G4O;mwWO`L!=R-=y3_~-DPjH3W^3f&jjCfC$o#|oGaahSL`_=f?$&Aa+W z2h8oZ+@?NUcjGW|aWJfbM*ZzxzmCPY`b~RobNrrj=rd`=)8-j`iSW64@0_b6?;GYk zNB+-fzOxlqZ?`y{OA$WigtZXa8)#p#=DPYxH=VeC_Q5q9Cv`mvW6*zU&Gnp1;oPM6 zaK_B3j(l^FyJgYeE9RrmDyhE7W2}}nW%ic#0v@i1E!yTey$W)U>fyd+!@2hWQ!Wa==NAtKoj`f3tp4y$Al`e;?)76?AjdaRR>|?&r)~3Git> zb1)a?uiv|R0_{m#A9c;7)eZ1y6l@yQ#oE*>(Z2fG-&&smPa2QTW>m*^K65^~`coP$ z8y5Y?iS<4Gz{Zg##$1mk)u-0;X|!xu^FCr;ce~X<&UWE&pBgqfYmEJTzpK9I%vr%b z3Ksd6qlPJLI%HFfeXK_^|BXiKZC>Ocu(Kk6hD3G-8usLzVG^q00Qh gz)s7ge@$ApxGu7=(6IGIk+uG&HTev01^#CH3$(Wk5&!@I literal 0 HcmV?d00001 diff --git a/packages/integrations/image/test/fixtures/background-color-image/server/server.mjs b/packages/integrations/image/test/fixtures/background-color-image/server/server.mjs new file mode 100644 index 0000000000..d7a0a7a40f --- /dev/null +++ b/packages/integrations/image/test/fixtures/background-color-image/server/server.mjs @@ -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