astro/packages/integrations/image/components/Image.astro

126 lines
3.4 KiB
Text
Raw Normal View History

Adds an `@astrojs/image` integration for optimizing images (#3694) * initial commit * WIP: starting to define interfaces for images and transformers * WIP: basic sharp service to test out the API setup * adding a few tests for sharp.toImageSrc * Adding tests for sharp.parseImageSrc * hooking up basic SSR support * updating image services to return width/height * simplifying config setup for v1 * hooking up basic SSR + SSG support (dev & build) * refactor: a bit of code cleanup and commenting * WIP: migrating local files to ESM + vite plugin * WIP: starting to hook up user-provided loaderEntryPoints * chore: update lock file * chore: update merged lockfile * refactor: code cleanup and type docs * pulling over the README template for first-party integrations * moving metadata out to the loader * updating the test for the refactored import * revert: remove unrelated webapi formatting * revert: remove unrelated change * fixing up the existing sharp tests * fix: vite plugin wasn't dynamically loading the image service properly * refactor: minor API renaming, removing last hard-coded use of sharp loader * don't manipulate src for hosted image services * Adding support for automatically calculating dimensions by aspect ratio, if needed * a few bug fixes + renaming the aspect ratio search param to "ar" * Adding ETag support, removing need for loaders to parse file metadata * using the battle tested `etag` package * Adding support for dynamically calculating partial sizes * refactor: moving to the packages/integrations dir, Astro Labs TBD later * refactor: renaming parse/serialize functions * Adding tests for SSG image optimizations * refactor: clean up outdated names related to ImageProps * nit: reusing cached SSG filename * chore: update pnpm lock file * handling file URLs when resolving local image imports * updating image file resolution to use file URLs * increasing test timeout for image build tests * fixing eslint error in sharp test * adding slash for windows compat in src URLs * chore: update lockfile after merge * Adding README content * adding a readme call to action for configuration options * review: A few of the quick updates from the PR review * hack: adds a one-off check to allow query params for the _image route * Adds support for src={import("...")}, and named component exports * adding SSR tests * nit: adding a bit more comments * limiting the query params in SSG dev to the images integration
2022-07-01 15:47:48 +00:00
---
// @ts-ignore
import loader from 'virtual:image-loader';
import { getImage } from '../src';
import type { ImageAttributes, ImageMetadata, TransformOptions, OutputFormat } from '../src/types';
export interface LocalImageProps extends Omit<TransformOptions, 'src'>, Omit<ImageAttributes, 'src'> {
src: ImageMetadata | Promise<{ default: ImageMetadata }>;
}
export interface RemoteImageProps extends TransformOptions, ImageAttributes {
src: string;
format: OutputFormat;
width: number;
height: number;
}
export type Props = LocalImageProps | RemoteImageProps;
function isLocalImage(props: Props): props is LocalImageProps {
// vite-plugin-astro-image resolves ESM imported images
// to a metadata object
return typeof props.src !== 'string';
}
function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) {
if (!aspectRatio) {
return undefined;
}
// parse aspect ratio strings, if required (ex: "16:9")
if (typeof aspectRatio === 'number') {
aspectRatio = aspectRatio;
} else {
const [width, height] = aspectRatio.split(':');
aspectRatio = parseInt(width) / parseInt(height);
}
}
async function resolveProps(props: Props): Promise<TransformOptions> {
// For remote images, just check the width/height provided
if (!isLocalImage(props)) {
return calculateSize(props);
}
let { width, height, aspectRatio, format, ...rest } = props;
// if a Promise<ImageMetadata> was provided, unwrap it first
const { src, ...metadata } = 'then' in props.src ? (await props.src).default : props.src;
if (!width && !height) {
// neither dimension was provided, use the file metadata
width = metadata.width;
height = metadata.height;
} else if (width) {
// one dimension was provided, calculate the other
let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height;
height = height || width / ratio;
} else if (height) {
// one dimension was provided, calculate the other
let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height;
width = width || height * ratio;
}
return {
...rest,
width,
height,
aspectRatio,
src,
format: format || metadata.format as OutputFormat,
}
}
function calculateSize(transform: TransformOptions): TransformOptions {
// keep width & height as provided
if (transform.width && transform.height) {
return transform;
}
if (!transform.width && !transform.height) {
throw new Error(`"width" and "height" cannot both be undefined`);
}
if (!transform.aspectRatio) {
throw new Error(`"aspectRatio" must be included if only "${transform.width ? "width": "height"}" is provided`)
}
let aspectRatio: number;
// parse aspect ratio strings, if required (ex: "16:9")
if (typeof transform.aspectRatio === 'number') {
aspectRatio = transform.aspectRatio;
} else {
const [width, height] = transform.aspectRatio.split(':');
aspectRatio = parseInt(width) / parseInt(height);
}
if (transform.width) {
// only width was provided, calculate height
return {
...transform,
width: transform.width,
height: transform.width / aspectRatio
};
} else if (transform.height) {
// only height was provided, calculate width
return {
...transform,
width: transform.height * aspectRatio,
height: transform.height
}
}
return transform;
}
const props = Astro.props as Props;
const imageProps = await resolveProps(props);
const attrs = await getImage(loader, imageProps);
---
<img {...attrs} />