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

View file

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

View file

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

View file

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

View file

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

View file

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

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 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" />

View file

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

View file

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

View file

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