Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
51 changes: 51 additions & 0 deletions .changeset/smooth-goats-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
'astro': minor
---

Add support for generating multiple widths when using the Image component and a Picture component for supporting multiple formats.
Comment thread
Princesseuh marked this conversation as resolved.
Outdated

## `srcset` support

The current usage is as follow:

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

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

Alternatively to `densities`, `widths` can be used for specific widths. In both cases, according images and the following code will be generated:

```html
<img src="..." srcset="... 2x, ... 3x" alt="My cool image" />
```

(if `widths` is used the descriptor will be `w` instead of `x`)

## Picture component

The `Picture` component can be used to generate a `<picture>` element with multiple sources. It can be used as follow:
Comment thread
Princesseuh marked this conversation as resolved.
Outdated

```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:
Comment thread
Princesseuh marked this conversation as resolved.
Outdated

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

Copy link
Copy Markdown
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 `densities` and `widths`.
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
Copy Markdown
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
Copy Markdown
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[];
Comment thread
Princesseuh marked this conversation as resolved.
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
Copy Markdown
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
Copy Markdown
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.attribute}
type={image.srcSet.values[0].attributes?.type}
/>
))
}
<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;
Comment thread
Princesseuh marked this conversation as resolved.
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
? service.getHTMLAttributes(validatedOptions, imageConfig)
Expand Down
Loading