diff options
author | 2023-02-15 09:21:41 +0100 | |
---|---|---|
committer | 2023-02-15 09:21:41 +0100 | |
commit | ac3649bb589a3ee1deab4ba73c06a36a45e2cee5 (patch) | |
tree | 1554b164878becfabd58c217688a206531959cca | |
parent | 87e0c10365528bf6b328e88d2d56d7ac15c14a2a (diff) | |
download | astro-ac3649bb589a3ee1deab4ba73c06a36a45e2cee5.tar.gz astro-ac3649bb589a3ee1deab4ba73c06a36a45e2cee5.tar.zst astro-ac3649bb589a3ee1deab4ba73c06a36a45e2cee5.zip |
Add support for SVG images to `@astrojs/image` (#6118)
* @astrojs/image: add support for SVG images
* @astrojs/image: add tests for SVG images
* @astrojs/image: update README.md with SVG format info
* Add minor changeset for @astrojs/image
12 files changed, 122 insertions, 7 deletions
diff --git a/.changeset/little-carrots-add.md b/.changeset/little-carrots-add.md new file mode 100644 index 000000000..433635158 --- /dev/null +++ b/.changeset/little-carrots-add.md @@ -0,0 +1,5 @@ +--- +'@astrojs/image': minor +--- + +Add support for SVG images diff --git a/packages/integrations/image/README.md b/packages/integrations/image/README.md index 1cb0d5d4c..d1cc22f2c 100644 --- a/packages/integrations/image/README.md +++ b/packages/integrations/image/README.md @@ -169,13 +169,16 @@ Set to an empty string (`alt=""`) if the image is not a key part of the content <p> -**Type:** `'avif' | 'jpeg' | 'png' | 'webp'`<br> +**Type:** `'avif' | 'jpeg' | 'jpg' | 'png' | 'svg' | 'webp'`<br> **Default:** `undefined` </p> The output format to be used in the optimized image. The original image format will be used if `format` is not provided. This property is required for remote images when using the default image transformer Squoosh, this is because the original format cannot be inferred. + +> When using the `svg` format, the original image must be in SVG format already (raster images cannot be converted to vector images). The SVG image itself won't be transformed but the final `<img />` element will get the optimization attributes. + #### quality <p> diff --git a/packages/integrations/image/client.d.ts b/packages/integrations/image/client.d.ts index cafec4184..71842742a 100644 --- a/packages/integrations/image/client.d.ts +++ b/packages/integrations/image/client.d.ts @@ -1,6 +1,16 @@ /// <reference types="astro/client-base" /> -type InputFormat = 'avif' | 'gif' | 'heic' | 'heif' | 'jpeg' | 'jpg' | 'png' | 'tiff' | 'webp'; +type InputFormat = + | 'avif' + | 'gif' + | 'heic' + | 'heif' + | 'jpeg' + | 'jpg' + | 'png' + | 'tiff' + | 'webp' + | 'svg'; interface ImageMetadata { src: string; @@ -46,3 +56,7 @@ declare module '*.webp' { const metadata: ImageMetadata; export default metadata; } +declare module '*.svg' { + const metadata: ImageMetadata; + export default metadata; +} diff --git a/packages/integrations/image/src/loaders/index.ts b/packages/integrations/image/src/loaders/index.ts index 280fb37c2..225e98cee 100644 --- a/packages/integrations/image/src/loaders/index.ts +++ b/packages/integrations/image/src/loaders/index.ts @@ -10,10 +10,11 @@ export type InputFormat = | 'png' | 'tiff' | 'webp' - | 'gif'; + | 'gif' + | 'svg'; export type OutputFormatSupportsAlpha = 'avif' | 'png' | 'webp'; -export type OutputFormat = OutputFormatSupportsAlpha | 'jpeg' | 'jpg'; +export type OutputFormat = OutputFormatSupportsAlpha | 'jpeg' | 'jpg' | 'svg'; export type ColorDefinition = | NamedColor @@ -49,7 +50,7 @@ export type CropPosition = | 'attention'; export function isOutputFormat(value: string): value is OutputFormat { - return ['avif', 'jpeg', 'jpg', 'png', 'webp'].includes(value); + return ['avif', 'jpeg', 'jpg', 'png', 'webp', 'svg'].includes(value); } export function isOutputFormatSupportsAlpha(value: string): value is OutputFormatSupportsAlpha { diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts index 55ea28645..517224289 100644 --- a/packages/integrations/image/src/loaders/sharp.ts +++ b/packages/integrations/image/src/loaders/sharp.ts @@ -5,6 +5,14 @@ import type { OutputFormat, TransformOptions } from './index.js'; class SharpService extends BaseSSRService { async transform(inputBuffer: Buffer, transform: TransformOptions) { + if (transform.format === 'svg') { + // sharp can't output SVG so we return the input image + return { + data: inputBuffer, + format: transform.format, + }; + } + const sharpImage = sharp(inputBuffer, { failOnError: false, pages: -1 }); // always call rotate to adjust for EXIF data orientation diff --git a/packages/integrations/image/src/loaders/squoosh.ts b/packages/integrations/image/src/loaders/squoosh.ts index 5d71cdb7f..a5be16adb 100644 --- a/packages/integrations/image/src/loaders/squoosh.ts +++ b/packages/integrations/image/src/loaders/squoosh.ts @@ -82,6 +82,14 @@ class SquooshService extends BaseSSRService { } async transform(inputBuffer: Buffer, transform: TransformOptions) { + if (transform.format === 'svg') { + // squoosh can't output SVG so we return the input image + return { + data: inputBuffer, + format: transform.format, + }; + } + const operations: Operation[] = []; if (!isRemoteImage(transform.src)) { diff --git a/packages/integrations/image/src/vite-plugin-astro-image.ts b/packages/integrations/image/src/vite-plugin-astro-image.ts index 3eee310e8..b721578a5 100644 --- a/packages/integrations/image/src/vite-plugin-astro-image.ts +++ b/packages/integrations/image/src/vite-plugin-astro-image.ts @@ -1,5 +1,6 @@ import type { AstroConfig } from 'astro'; import MagicString from 'magic-string'; +import mime from 'mime'; import fs from 'node:fs/promises'; import { basename, extname } from 'node:path'; import { Readable } from 'node:stream'; @@ -18,7 +19,7 @@ export interface ImageMetadata { export function createPlugin(config: AstroConfig, options: Required<IntegrationOptions>): Plugin { const filter = (id: string) => - /^(?!\/_image?).*.(heic|heif|avif|jpeg|jpg|png|tiff|webp|gif)$/.test(id); + /^(?!\/_image?).*.(heic|heif|avif|jpeg|jpg|png|tiff|webp|gif|svg)$/.test(id); const virtualModuleId = 'virtual:image-loader'; @@ -97,7 +98,7 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO format = result.format; } - res.setHeader('Content-Type', `image/${format}`); + res.setHeader('Content-Type', mime.getType(format) || ''); res.setHeader('Cache-Control', 'max-age=360000'); const stream = Readable.from(data); diff --git a/packages/integrations/image/test/fixtures/basic-image/src/assets/logo.svg b/packages/integrations/image/test/fixtures/basic-image/src/assets/logo.svg new file mode 100644 index 000000000..a1388d931 --- /dev/null +++ b/packages/integrations/image/test/fixtures/basic-image/src/assets/logo.svg @@ -0,0 +1,22 @@ +<svg width="192" height="256" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" + d="M131.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.258 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" + fill="url(#paint0_linear)" /> + <path fill-rule="evenodd" clip-rule="evenodd" + d="M136.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.35-11.349 10.742 0 10.73 9.373 10.72 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" + fill="#FF5D01" /> + <path fill-rule="evenodd" clip-rule="evenodd" + d="M136.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.35-11.349 10.742 0 10.73 9.373 10.72 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" + fill="url(#paint1_linear)" /> + <defs> + <linearGradient id="paint0_linear" x1="144.599" y1="5.423" x2="95.791" y2="173.38" gradientUnits="userSpaceOnUse"> + <stop stop-color="#000014" /> + <stop offset="1" stop-color="#150426" /> + </linearGradient> + <linearGradient id="paint1_linear" x1="168.336" y1="130.49" x2="126.065" y2="218.982" + gradientUnits="userSpaceOnUse"> + <stop stop-color="#FF1639" /> + <stop offset="1" stop-color="#FF1639" stop-opacity="0" /> + </linearGradient> + </defs> +</svg> diff --git a/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro b/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro index ba492576c..ed1d79db6 100644 --- a/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro +++ b/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro @@ -1,5 +1,6 @@ --- import socialJpg from '../assets/social.jpg'; +import logoSvg from '../assets/logo.svg'; import introJpg from '../assets/blog/introducing astro.jpg'; import outsideSrc from '../../social.png'; import { Image } from '@astrojs/image/components'; @@ -21,6 +22,8 @@ const publicImage = new URL('./hero.jpg', Astro.url); <br /> <Image id="outside-src" src={outsideSrc} alt="outside-src" /> <br /> + <Image id="logo-svg" src={logoSvg} alt="logo-svg" /> + <br /> <Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" alt="Google" /> <br /> <Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" /> diff --git a/packages/integrations/image/test/image-ssg.test.js b/packages/integrations/image/test/image-ssg.test.js index 5bc1c1e0d..12b3ffea9 100644 --- a/packages/integrations/image/test/image-ssg.test.js +++ b/packages/integrations/image/test/image-ssg.test.js @@ -53,6 +53,12 @@ describe('SSG images - dev', function () { contentType: 'image/png', }, { + title: 'SVG image', + id: '#logo-svg', + url: toAstroImage('src/assets/logo.svg'), + query: { f: 'svg', w: '192', h: '256' }, + }, + { title: 'Inline imports', id: '#inline', url: toAstroImage('src/assets/social.jpg'), @@ -158,6 +164,12 @@ describe('SSG images with subpath - dev', function () { contentType: 'image/png', }, { + title: 'SVG image', + id: '#logo-svg', + url: toAstroImage('src/assets/logo.svg'), + query: { f: 'svg', w: '192', h: '256' }, + }, + { title: 'Inline imports', id: '#inline', url: toAstroImage('src/assets/social.jpg'), @@ -264,6 +276,12 @@ describe('SSG images - build', function () { size: { type: 'png', width: 2024, height: 1012 }, }, { + title: 'SVG image', + id: '#logo-svg', + regex: /^\/_astro\/logo.\w{8}_\w{4,10}.svg/, + size: { width: 192, height: 256, type: 'svg' }, + }, + { title: 'Inline imports', id: '#inline', regex: /^\/_astro\/social.\w{8}_\w{4,10}.jpg/, @@ -352,6 +370,12 @@ describe('SSG images with subpath - build', function () { size: { type: 'png', width: 2024, height: 1012 }, }, { + title: 'SVG image', + id: '#logo-svg', + regex: /^\/docs\/_astro\/logo.\w{8}_\w{4,10}.svg/, + size: { width: 192, height: 256, type: 'svg' }, + }, + { title: 'Inline imports', id: '#inline', regex: /^\/docs\/_astro\/social.\w{8}_\w{4,10}.jpg/, diff --git a/packages/integrations/image/test/image-ssr-build.test.js b/packages/integrations/image/test/image-ssr-build.test.js index 4b985c0ad..f85373c27 100644 --- a/packages/integrations/image/test/image-ssr-build.test.js +++ b/packages/integrations/image/test/image-ssr-build.test.js @@ -29,6 +29,12 @@ describe('SSR images - build', async function () { query: { f: 'webp', w: '768', h: '414', href: /^\/_astro\/introducing astro.\w{8}.jpg/ }, }, { + title: 'SVG image', + id: '#logo-svg', + url: '/_image', + query: { f: 'svg', w: '192', h: '256', href: /^\/_astro\/logo.\w{8}.svg/ }, + }, + { title: 'Inline imports', id: '#inline', url: '/_image', @@ -145,6 +151,12 @@ describe('SSR images with subpath - build', function () { }, }, { + title: 'SVG image', + id: '#logo-svg', + url: '/_image', + query: { f: 'svg', w: '192', h: '256', href: /^\/docs\/_astro\/logo.\w{8}.svg/ }, + }, + { title: 'Inline imports', id: '#inline', url: '/_image', diff --git a/packages/integrations/image/test/image-ssr-dev.test.js b/packages/integrations/image/test/image-ssr-dev.test.js index fbaa6f965..186100b12 100644 --- a/packages/integrations/image/test/image-ssr-dev.test.js +++ b/packages/integrations/image/test/image-ssr-dev.test.js @@ -60,6 +60,13 @@ describe('SSR images - dev', function () { contentType: 'image/png', }, { + title: 'SVG image', + id: '#logo-svg', + url: toAstroImage('src/assets/logo.svg'), + query: { f: 'svg', w: '192', h: '256' }, + contentType: 'image/svg+xml', + }, + { title: 'Inline imports', id: '#inline', url: toAstroImage('src/assets/social.jpg'), @@ -182,6 +189,13 @@ describe('SSR images with subpath - dev', function () { contentType: 'image/png', }, { + title: 'SVG image', + id: '#logo-svg', + url: toAstroImage('src/assets/logo.svg'), + query: { f: 'svg', w: '192', h: '256' }, + contentType: 'image/svg+xml', + }, + { title: 'Inline imports', id: '#inline', url: toAstroImage('src/assets/social.jpg'), |