-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 6e5f578. * 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 <[email protected]> * 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 <[email protected]>
- Loading branch information
1 parent
ec39258
commit 89d7675
Showing
28 changed files
with
1,052 additions
and
164 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@astrojs/image': minor | ||
--- | ||
|
||
The new `<Picture />` component adds art direction support for building responsive images with multiple sizes and file types :tada: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
--- | ||
// @ts-ignore | ||
import loader from 'virtual:image-loader'; | ||
import { getPicture } from '../src/get-picture.js'; | ||
import type { ImageAttributes, ImageMetadata, OutputFormat, PictureAttributes, TransformOptions } from '../src/types.js'; | ||
export interface LocalImageProps extends Omit<PictureAttributes, 'src' | 'width' | 'height'>, Omit<TransformOptions, 'src'>, Omit<ImageAttributes, 'src' | 'width' | 'height'> { | ||
src: ImageMetadata | Promise<{ default: ImageMetadata }>; | ||
sizes: HTMLImageElement['sizes']; | ||
widths: number[]; | ||
formats?: OutputFormat[]; | ||
} | ||
export interface RemoteImageProps extends Omit<PictureAttributes, 'src' | 'width' | 'height'>, TransformOptions, Omit<ImageAttributes, 'src' | 'width' | 'height'> { | ||
src: string; | ||
sizes: HTMLImageElement['sizes']; | ||
widths: number[]; | ||
aspectRatio: TransformOptions['aspectRatio']; | ||
formats?: OutputFormat[]; | ||
} | ||
export type Props = LocalImageProps | RemoteImageProps; | ||
const { src, sizes, widths, aspectRatio, formats = ['avif', 'webp'], loading = 'lazy', decoding = 'eager', ...attrs } = Astro.props as Props; | ||
const { image, sources } = await getPicture({ loader, src, widths, formats, aspectRatio }); | ||
--- | ||
|
||
<picture {...attrs}> | ||
{sources.map(attrs => ( | ||
<source {...attrs} {sizes}>))} | ||
<img {...image} {loading} {decoding} /> | ||
</picture> | ||
|
||
<style> | ||
img { | ||
content-visibility: auto; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export { default as Image } from './Image.astro'; | ||
export { default as Picture } from './Picture.astro'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const PKG_NAME = '@astrojs/image'; | ||
export const ROUTE_PATTERN = '/_image'; | ||
export const OUTPUT_DIR = '/_image'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Oops, something went wrong.