diff options
author | 2022-07-08 21:37:55 +0000 | |
---|---|---|
committer | 2022-07-08 21:37:55 +0000 | |
commit | 89d76753a0dc50b2967d1fa9d36e34bde2722b83 (patch) | |
tree | cfaf7ecf53999c15bb56646dad8c57e013c68450 /packages/integrations/image/src/get-image.ts | |
parent | ec392589f6d60785e45a49acdb3b9bda29c566df (diff) | |
download | astro-89d76753a0dc50b2967d1fa9d36e34bde2722b83.tar.gz astro-89d76753a0dc50b2967d1fa9d36e34bde2722b83.tar.zst astro-89d76753a0dc50b2967d1fa9d36e34bde2722b83.zip |
Adds a new `<Picture>` component to the image integration (#3866)
* moving all normalization logic out of the Image component
* refactor: only require loaders to provide the image src
* Adding a `<Picture />` component
* fixing types.ts imports
* refactor: moving getImage to it's own file
* updating component types to use astroHTML.JSX
* Revert "updating component types to use astroHTML.JSX"
This reverts commit 6e5f578da8d1d3fd262f3cd9add7549f7580af97.
* going back to letting loaders add extra HTML attributes
* Always use lazy loading and async decoding
* Cleaning up the Picture component
* Adding test coverage for <Picture>
* updating the README
* using JSX types for the Image and Picture elements
* chore: adding changeset
* Update packages/integrations/image/src/get-image.ts
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
* allow users to override loading and async on the <img>
* renaming config to constants, exporting getPicture()
* found the right syntax to import astro-jsx
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
Diffstat (limited to 'packages/integrations/image/src/get-image.ts')
-rw-r--r-- | packages/integrations/image/src/get-image.ts | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/packages/integrations/image/src/get-image.ts b/packages/integrations/image/src/get-image.ts new file mode 100644 index 000000000..ae423c3de --- /dev/null +++ b/packages/integrations/image/src/get-image.ts @@ -0,0 +1,128 @@ +import slash from 'slash'; +import { ROUTE_PATTERN } from './constants.js'; +import { ImageAttributes, ImageMetadata, ImageService, isSSRService, OutputFormat, TransformOptions } from './types.js'; +import { parseAspectRatio } from './utils.js'; + +export interface GetImageTransform extends Omit<TransformOptions, 'src'> { + src: string | ImageMetadata | Promise<{ default: ImageMetadata }>; +} + +function resolveSize(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 = Number.parseInt(width) / Number.parseInt(height); + } + + if (transform.width) { + // only width was provided, calculate height + return { + ...transform, + width: transform.width, + height: Math.round(transform.width / aspectRatio) + } as TransformOptions; + } else if (transform.height) { + // only height was provided, calculate width + return { + ...transform, + width: Math.round(transform.height * aspectRatio), + height: transform.height + }; + } + + return transform; +} + +async function resolveTransform(input: GetImageTransform): Promise<TransformOptions> { + // for remote images, only validate the width and height props + if (typeof input.src === 'string') { + return resolveSize(input as TransformOptions); + } + + // resolve the metadata promise, usually when the ESM import is inlined + const metadata = 'then' in input.src + ? (await input.src).default + : input.src; + + let { width, height, aspectRatio, format = metadata.format, ...rest } = input; + + 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 || Math.round(width / ratio); + } else if (height) { + // one dimension was provided, calculate the other + let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height; + width = width || Math.round(height * ratio); + } + + return { + ...rest, + src: metadata.src, + width, + height, + aspectRatio, + format: format as OutputFormat, + } +} + +/** + * Gets the HTML attributes required to build an `<img />` for the transformed image. + * + * @param loader @type {ImageService} The image service used for transforming images. + * @param transform @type {TransformOptions} The transformations requested for the optimized image. + * @returns @type {ImageAttributes} The HTML attributes to be included on the built `<img />` element. + */ + export async function getImage( + loader: ImageService, + transform: GetImageTransform +): Promise<ImageAttributes> { + (globalThis as any).loader = loader; + + const resolved = await resolveTransform(transform); + const attributes = await loader.getImageAttributes(resolved); + + // For SSR services, build URLs for the injected route + if (isSSRService(loader)) { + const { searchParams } = loader.serializeTransform(resolved); + + // cache all images rendered to HTML + if (globalThis && (globalThis as any).addStaticImage) { + (globalThis as any)?.addStaticImage(resolved); + } + + const src = + globalThis && (globalThis as any).filenameFormat + ? (globalThis as any).filenameFormat(resolved, searchParams) + : `${ROUTE_PATTERN}?${searchParams.toString()}`; + + return { + ...attributes, + src: slash(src), // Windows compat + }; + } + + // For hosted services, return the `<img />` attributes as-is + return attributes; +} |