diff --git a/.changeset/little-carrots-add.md b/.changeset/little-carrots-add.md new file mode 100644 index 000000000000..4336351582d1 --- /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 1cb0d5d4c7e4..d1cc22f2c6fc 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

-**Type:** `'avif' | 'jpeg' | 'png' | 'webp'`
+**Type:** `'avif' | 'jpeg' | 'jpg' | 'png' | 'svg' | 'webp'`
**Default:** `undefined`

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 `` element will get the optimization attributes. + #### quality

diff --git a/packages/integrations/image/client.d.ts b/packages/integrations/image/client.d.ts index cafec4184c65..71842742ab4d 100644 --- a/packages/integrations/image/client.d.ts +++ b/packages/integrations/image/client.d.ts @@ -1,6 +1,16 @@ /// -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 280fb37c28db..225e98cee9cd 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 55ea28645b92..517224289602 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 5d71cdb7fbce..a5be16adb019 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 3eee310e8905..b721578a5992 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): 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 + + + + + + + + + + + + + + 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 ba492576cf96..ed1d79db662d 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);
outside-src
+ logo-svg +
Google
inline diff --git a/packages/integrations/image/test/image-ssg.test.js b/packages/integrations/image/test/image-ssg.test.js index 5bc1c1e0d22c..12b3ffea9c92 100644 --- a/packages/integrations/image/test/image-ssg.test.js +++ b/packages/integrations/image/test/image-ssg.test.js @@ -52,6 +52,12 @@ describe('SSG images - dev', function () { query: { f: 'png', w: '2024', h: '1012' }, 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', @@ -157,6 +163,12 @@ describe('SSG images with subpath - dev', function () { query: { f: 'png', w: '2024', h: '1012' }, 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', @@ -263,6 +275,12 @@ describe('SSG images - build', function () { regex: /^\/_astro\/social.\w{8}_\w{4,10}.png/, 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', @@ -351,6 +369,12 @@ describe('SSG images with subpath - build', function () { regex: /^\/docs\/_astro\/social.\w{8}_\w{4,10}.png/, 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', diff --git a/packages/integrations/image/test/image-ssr-build.test.js b/packages/integrations/image/test/image-ssr-build.test.js index 4b985c0ad9e9..f85373c27cac 100644 --- a/packages/integrations/image/test/image-ssr-build.test.js +++ b/packages/integrations/image/test/image-ssr-build.test.js @@ -28,6 +28,12 @@ describe('SSR images - build', async function () { url: '/_image', 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', @@ -144,6 +150,12 @@ describe('SSR images with subpath - build', function () { href: /^\/docs\/_astro\/introducing astro.\w{8}.jpg/, }, }, + { + 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', diff --git a/packages/integrations/image/test/image-ssr-dev.test.js b/packages/integrations/image/test/image-ssr-dev.test.js index fbaa6f965e17..186100b12da2 100644 --- a/packages/integrations/image/test/image-ssr-dev.test.js +++ b/packages/integrations/image/test/image-ssr-dev.test.js @@ -59,6 +59,13 @@ describe('SSR images - dev', function () { query: { f: 'png', w: '2024', h: '1012' }, 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', @@ -181,6 +188,13 @@ describe('SSR images with subpath - dev', function () { query: { f: 'png', w: '2024', h: '1012' }, 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',