astro/packages/integrations/image/components/Image.astro
Tony Sullivan e8593e7ead
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

125 lines
3.4 KiB
Text

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