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:
parent
87e0c10365
commit
ac3649bb58
12 changed files with 122 additions and 7 deletions
5
.changeset/little-carrots-add.md
Normal file
5
.changeset/little-carrots-add.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/image': minor
|
||||
---
|
||||
|
||||
Add support for SVG images
|
|
@ -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>
|
||||
|
|
16
packages/integrations/image/client.d.ts
vendored
16
packages/integrations/image/client.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
|
22
packages/integrations/image/test/fixtures/basic-image/src/assets/logo.svg
vendored
Normal file
22
packages/integrations/image/test/fixtures/basic-image/src/assets/logo.svg
vendored
Normal 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 |
|
@ -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" />
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue