Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .changeset/smooth-goats-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
'astro': minor
---

Adds experimental support for generating `srcset` attributes and a new `<Picture />` component.

## `srcset` support

Two new properties have been added to `Image` and `getImage()`: `densities` and `widths`.

These properties can be used to generate a `srcset` attribute, either based on absolute widths in pixels (e.g. [300, 600, 900]) or pixel density descriptors (e.g. `["2x"]` or `[1.5, 2]`).


```astro
---
import { Image } from "astro";
import myImage from "./my-image.jpg";
---

<Image src={myImage} width={myImage.width / 2} densities={[1.5, 2]} alt="My cool image" />
```

```html
<img
src="/_astro/my_image.hash.webp"
srcset="/_astro/my_image.hash.webp 1.5x, /_astro/my_image.hash.webp 2x"
alt="My cool image"
/>
```

## Picture component

The experimental `<Picture />` component can be used to generate a `<picture>` element with multiple `<source>` elements.

The example below uses the `format` property to generate a `<source>` in each of the specified image formats:

```astro
---
import { Picture } from "astro:assets";
import myImage from "./my-image.jpg";
---

<Picture src={myImage} formats={["avif", "webp"]} alt="My super image in multiple formats!" />
```

The above code will generate the following HTML, and allow the browser to determine the best image to display:

```html
<picture>
<source srcset="..." type="image/avif" />
<source srcset="..." type="image/webp" />
<img src="..." alt="My super image in multiple formats!" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will the img here be equivalent to the above image + densities example? a la "show the original file in the root image source?

</picture>
```

The `Picture` component takes all the same props as the `Image` component, including the new `densities` and `widths` properties.
12 changes: 3 additions & 9 deletions packages/astro/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ declare module 'astro:assets' {
imageConfig: import('./dist/@types/astro.js').AstroConfig['image'];
getConfiguredImageService: typeof import('./dist/assets/index.js').getConfiguredImageService;
Image: typeof import('./components/Image.astro').default;
Picture: typeof import('./components/Picture.astro').default;
};

type ImgAttributes = import('./dist/type-utils.js').WithRequired<
Expand All @@ -66,17 +67,10 @@ declare module 'astro:assets' {
export type RemoteImageProps = import('./dist/type-utils.js').Simplify<
import('./dist/assets/types.js').RemoteImageProps<ImgAttributes>
>;
export const { getImage, getConfiguredImageService, imageConfig, Image }: AstroAssets;
export const { getImage, getConfiguredImageService, imageConfig, Image, Picture }: AstroAssets;
}

type InputFormat = import('./dist/assets/types.js').ImageInputFormat;

interface ImageMetadata {
src: string;
width: number;
height: number;
format: InputFormat;
}
type ImageMetadata = import('./dist/assets/types.js').ImageMetadata;

declare module '*.gif' {
const metadata: ImageMetadata;
Expand Down
8 changes: 7 additions & 1 deletion packages/astro/components/Image.astro
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ if (typeof props.height === 'string') {
}

const image = await getImage(props);

const additionalAttributes: Record<string, any> = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not unknown?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly so we don't have to check the attributes before using them, since it doesn't really matter (it's classic Astro serializing, so it supports everything)


if (image.srcSet.values.length > 0) {
additionalAttributes.srcset = image.srcSet.attribute;
}
---

<img src={image.src} {...image.attributes} />
<img src={image.src} {...additionalAttributes} {...image.attributes} />
57 changes: 57 additions & 0 deletions packages/astro/components/Picture.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
import { getImage, type LocalImageProps, type RemoteImageProps } from 'astro:assets';
import type { GetImageResult, ImageOutputFormat } from '../dist/@types/astro';
import { isESMImportedImage } from '../dist/assets/internal';
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
import type { HTMLAttributes } from '../types';

type Props = (LocalImageProps | RemoteImageProps) & {
formats?: ImageOutputFormat[];
fallbackFormat?: ImageOutputFormat;
pictureAttributes?: HTMLAttributes<'picture'>;
};

const { formats = ['webp'], pictureAttributes = {}, ...props } = Astro.props;

if (props.alt === undefined || props.alt === null) {
throw new AstroError(AstroErrorData.ImageMissingAlt);
}

const optimizedImages: GetImageResult[] = await Promise.all(
formats.map(
async (format) =>
await getImage({ ...props, format: format, widths: props.widths, densities: props.densities })
)
);

const fallbackFormat =
props.fallbackFormat ?? isESMImportedImage(props.src)
? ['svg', 'gif'].includes(props.src.format)
? props.src.format
: 'png'
: 'png';

const fallbackImage = await getImage({
...props,
format: fallbackFormat,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, so the answer is yes!

widths: props.widths,
densities: props.densities,
});

const additionalAttributes: Record<string, any> = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const additionalAttributes: Record<string, any> = {};
const additionalAttributes: Record<string, unknown> = {};

perhaps?

if (fallbackImage.srcSet.values.length > 0) {
additionalAttributes.srcset = fallbackImage.srcSet.attribute;
}
---

<picture {...pictureAttributes}>
{
Object.entries(optimizedImages).map(([_, image]) => (
<source
srcset={`${image.src}${image.srcSet.values.length > 0 ? ' , ' + image.srcSet.attribute : ''}`}
type={"image/" + image.options.format}
/>
))
}
<img src={fallbackImage.src} {...additionalAttributes} {...fallbackImage.attributes} />
</picture>
1 change: 1 addition & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
"mocha": "^10.2.0",
"network-information-types": "^0.1.1",
"node-mocks-http": "^1.13.0",
"parse-srcset": "^1.0.2",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
"rehype-toc": "^3.0.2",
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/assets/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export const VALID_SUPPORTED_FORMATS = [
'svg',
'avif',
] as const;
export const DEFAULT_OUTPUT_FORMAT = 'webp' as const;
export const VALID_OUTPUT_FORMATS = ['avif', 'png', 'webp', 'jpeg', 'jpg', 'svg'] as const;
24 changes: 22 additions & 2 deletions packages/astro/src/assets/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
GetImageResult,
ImageMetadata,
ImageTransform,
SrcSetValue,
UnresolvedImageTransform,
} from './types.js';
import { matchHostname, matchPattern } from './utils/remotePattern.js';
Expand Down Expand Up @@ -93,22 +94,41 @@ export async function getImage(
? await service.validateOptions(resolvedOptions, imageConfig)
: resolvedOptions;

// Get all the options for the different srcSets
const srcSetTransforms = service.getSrcSet
? await service.getSrcSet(validatedOptions, imageConfig)
: [];

let imageURL = await service.getURL(validatedOptions, imageConfig);
let srcSets: SrcSetValue[] = await Promise.all(
srcSetTransforms.map(async (srcSet) => ({
url: await service.getURL(srcSet.transform, imageConfig),
descriptor: srcSet.descriptor,
attributes: srcSet.attributes,
}))
);

// In build and for local services, we need to collect the requested parameters so we can generate the final images
if (
isLocalService(service) &&
globalThis.astroAsset.addStaticImage &&
// If `getURL` returned the same URL as the user provided, it means the service doesn't need to do anything
!(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)
) {
imageURL = globalThis.astroAsset.addStaticImage(validatedOptions);
srcSets = srcSetTransforms.map((srcSet) => ({
url: globalThis.astroAsset.addStaticImage!(srcSet.transform),
descriptor: srcSet.descriptor,
attributes: srcSet.attributes,
}));
}

return {
rawOptions: resolvedOptions,
options: validatedOptions,
src: imageURL,
srcSet: {
values: srcSets,
attribute: srcSets.map((srcSet) => `${srcSet.url} ${srcSet.descriptor}`).join(', '),
},
attributes:
service.getHTMLAttributes !== undefined
? await service.getHTMLAttributes(validatedOptions, imageConfig)
Expand Down
Loading