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
This commit is contained in:
Gérald Gounot 2023-02-15 09:21:41 +01:00 committed by GitHub
parent 87e0c10365
commit ac3649bb58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 122 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/image': minor
---
Add support for SVG images

View file

@ -169,13 +169,16 @@ Set to an empty string (`alt=""`) if the image is not a key part of the content
<p> <p>
**Type:** `'avif' | 'jpeg' | 'png' | 'webp'`<br> **Type:** `'avif' | 'jpeg' | 'jpg' | 'png' | 'svg' | 'webp'`<br>
**Default:** `undefined` **Default:** `undefined`
</p> </p>
The output format to be used in the optimized image. The original image format will be used if `format` is not provided. 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. 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 #### quality
<p> <p>

View file

@ -1,6 +1,16 @@
/// <reference types="astro/client-base" /> /// <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 { interface ImageMetadata {
src: string; src: string;
@ -46,3 +56,7 @@ declare module '*.webp' {
const metadata: ImageMetadata; const metadata: ImageMetadata;
export default metadata; export default metadata;
} }
declare module '*.svg' {
const metadata: ImageMetadata;
export default metadata;
}

View file

@ -10,10 +10,11 @@ export type InputFormat =
| 'png' | 'png'
| 'tiff' | 'tiff'
| 'webp' | 'webp'
| 'gif'; | 'gif'
| 'svg';
export type OutputFormatSupportsAlpha = 'avif' | 'png' | 'webp'; export type OutputFormatSupportsAlpha = 'avif' | 'png' | 'webp';
export type OutputFormat = OutputFormatSupportsAlpha | 'jpeg' | 'jpg'; export type OutputFormat = OutputFormatSupportsAlpha | 'jpeg' | 'jpg' | 'svg';
export type ColorDefinition = export type ColorDefinition =
| NamedColor | NamedColor
@ -49,7 +50,7 @@ export type CropPosition =
| 'attention'; | 'attention';
export function isOutputFormat(value: string): value is OutputFormat { 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 { export function isOutputFormatSupportsAlpha(value: string): value is OutputFormatSupportsAlpha {

View file

@ -5,6 +5,14 @@ import type { OutputFormat, TransformOptions } from './index.js';
class SharpService extends BaseSSRService { class SharpService extends BaseSSRService {
async transform(inputBuffer: Buffer, transform: TransformOptions) { 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 }); const sharpImage = sharp(inputBuffer, { failOnError: false, pages: -1 });
// always call rotate to adjust for EXIF data orientation // always call rotate to adjust for EXIF data orientation

View file

@ -82,6 +82,14 @@ class SquooshService extends BaseSSRService {
} }
async transform(inputBuffer: Buffer, transform: TransformOptions) { 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[] = []; const operations: Operation[] = [];
if (!isRemoteImage(transform.src)) { if (!isRemoteImage(transform.src)) {

View file

@ -1,5 +1,6 @@
import type { AstroConfig } from 'astro'; import type { AstroConfig } from 'astro';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import mime from 'mime';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import { basename, extname } from 'node:path'; import { basename, extname } from 'node:path';
import { Readable } from 'node:stream'; import { Readable } from 'node:stream';
@ -18,7 +19,7 @@ export interface ImageMetadata {
export function createPlugin(config: AstroConfig, options: Required<IntegrationOptions>): Plugin { export function createPlugin(config: AstroConfig, options: Required<IntegrationOptions>): Plugin {
const filter = (id: string) => 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'; const virtualModuleId = 'virtual:image-loader';
@ -97,7 +98,7 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO
format = result.format; format = result.format;
} }
res.setHeader('Content-Type', `image/${format}`); res.setHeader('Content-Type', mime.getType(format) || '');
res.setHeader('Cache-Control', 'max-age=360000'); res.setHeader('Cache-Control', 'max-age=360000');
const stream = Readable.from(data); const stream = Readable.from(data);

View file

@ -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>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,5 +1,6 @@
--- ---
import socialJpg from '../assets/social.jpg'; import socialJpg from '../assets/social.jpg';
import logoSvg from '../assets/logo.svg';
import introJpg from '../assets/blog/introducing astro.jpg'; import introJpg from '../assets/blog/introducing astro.jpg';
import outsideSrc from '../../social.png'; import outsideSrc from '../../social.png';
import { Image } from '@astrojs/image/components'; import { Image } from '@astrojs/image/components';
@ -21,6 +22,8 @@ const publicImage = new URL('./hero.jpg', Astro.url);
<br /> <br />
<Image id="outside-src" src={outsideSrc} alt="outside-src" /> <Image id="outside-src" src={outsideSrc} alt="outside-src" />
<br /> <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" /> <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 /> <br />
<Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" /> <Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" />

View file

@ -52,6 +52,12 @@ describe('SSG images - dev', function () {
query: { f: 'png', w: '2024', h: '1012' }, query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png', 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', title: 'Inline imports',
id: '#inline', id: '#inline',
@ -157,6 +163,12 @@ describe('SSG images with subpath - dev', function () {
query: { f: 'png', w: '2024', h: '1012' }, query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png', 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', title: 'Inline imports',
id: '#inline', id: '#inline',
@ -263,6 +275,12 @@ describe('SSG images - build', function () {
regex: /^\/_astro\/social.\w{8}_\w{4,10}.png/, regex: /^\/_astro\/social.\w{8}_\w{4,10}.png/,
size: { type: 'png', width: 2024, height: 1012 }, 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', title: 'Inline imports',
id: '#inline', id: '#inline',
@ -351,6 +369,12 @@ describe('SSG images with subpath - build', function () {
regex: /^\/docs\/_astro\/social.\w{8}_\w{4,10}.png/, regex: /^\/docs\/_astro\/social.\w{8}_\w{4,10}.png/,
size: { type: 'png', width: 2024, height: 1012 }, 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', title: 'Inline imports',
id: '#inline', id: '#inline',

View file

@ -28,6 +28,12 @@ describe('SSR images - build', async function () {
url: '/_image', url: '/_image',
query: { f: 'webp', w: '768', h: '414', href: /^\/_astro\/introducing astro.\w{8}.jpg/ }, 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', title: 'Inline imports',
id: '#inline', id: '#inline',
@ -144,6 +150,12 @@ describe('SSR images with subpath - build', function () {
href: /^\/docs\/_astro\/introducing astro.\w{8}.jpg/, 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', title: 'Inline imports',
id: '#inline', id: '#inline',

View file

@ -59,6 +59,13 @@ describe('SSR images - dev', function () {
query: { f: 'png', w: '2024', h: '1012' }, query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png', 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', title: 'Inline imports',
id: '#inline', id: '#inline',
@ -181,6 +188,13 @@ describe('SSR images with subpath - dev', function () {
query: { f: 'png', w: '2024', h: '1012' }, query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png', 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', title: 'Inline imports',
id: '#inline', id: '#inline',