diff --git a/packages/astro/components/Image.astro b/packages/astro/components/Image.astro index a00f8c990..a11efd4f9 100644 --- a/packages/astro/components/Image.astro +++ b/packages/astro/components/Image.astro @@ -23,10 +23,11 @@ if (typeof props.height === 'string') { } const image = await getImage(props); + const additionalAttributes: Record = {}; -if (image.srcSet.length > 0) { - additionalAttributes.srcset = image.srcSetValue; +if (image.srcSet.values.length > 0) { + additionalAttributes.srcset = image.srcSet.attribute; } --- diff --git a/packages/astro/components/Picture.astro b/packages/astro/components/Picture.astro index 506f72ea0..bc53cd8cc 100644 --- a/packages/astro/components/Picture.astro +++ b/packages/astro/components/Picture.astro @@ -6,17 +6,22 @@ import { AstroError, AstroErrorData } from '../dist/core/errors/index.js'; import { HTMLAttributes } from '../types'; type Props = (LocalImageProps | RemoteImageProps) & { - formats: ImageOutputFormat[]; - fallbackFormat: ImageOutputFormat; - pictureAttributes: HTMLAttributes<'picture'>; + formats?: ImageOutputFormat[]; + fallbackFormat?: ImageOutputFormat; + pictureAttributes?: HTMLAttributes<'picture'>; }; -const props = Astro.props; -const optimizedImages: Record< - ImageOutputFormat, - GetImageResult -> = await Promise.all( - props.formats.map(async (format) => await getImage({ ...props, format: format, widths: props.widths, densities: props.densities })) +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 = @@ -26,19 +31,24 @@ const fallbackFormat = : 'png' : 'png'; -const originalImage = await getImage({ ...props, format: fallbackFormat, widths: props.widths, densities: props.densities }); +const fallbackImage = await getImage({ + ...props, + format: fallbackFormat, + widths: props.widths, + densities: props.densities, +}); -if (props.alt === undefined || props.alt === null) { - throw new AstroError(AstroErrorData.ImageMissingAlt); +const additionalAttributes: Record = {}; +if (fallbackImage.srcSet.values.length > 0) { + additionalAttributes.srcset = fallbackImage.srcSet.attribute; } --- - - {Object.entries(optimizedImages).map(([format, image]) => ( - - ))} - + + { + Object.entries(optimizedImages).map(([_, image]) => ( + + )) + } + diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts index 3b453c21e..ee9fde51f 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -124,8 +124,10 @@ export async function getImage( rawOptions: resolvedOptions, options: validatedOptions, src: imageURL, - srcSet: srcSets, - srcSetValue: srcSets.map((srcSet) => `${srcSet.url} ${srcSet.descriptor}`).join(', '), + srcSet: { + values: srcSets, + attribute: srcSets.map((srcSet) => `${srcSet.url} ${srcSet.descriptor}`).join(', '), + }, attributes: service.getHTMLAttributes !== undefined ? service.getHTMLAttributes(validatedOptions, imageConfig) diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts index 8405a328c..e24dc78f9 100644 --- a/packages/astro/src/assets/services/service.ts +++ b/packages/astro/src/assets/services/service.ts @@ -188,8 +188,7 @@ export const baseService: Omit = { } if (options.widths && options.densities) { - console.warn('Cannot use `widths` and `densities` at the same time. Using `densities`.'); - options.widths = undefined; + throw new AstroError(AstroErrorData.IncompatibleDescriptorOptions); } // We currently do not support processing SVGs, so whenever the input format is a SVG, force the output to also be one @@ -226,6 +225,9 @@ export const baseService: Omit = { const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT; const aspectRatio = targetWidth / targetHeight; + const imageWidth = isESMImportedImage(options.src) ? options.src.width : options.width; + const maxWidth = options.width ?? imageWidth ?? Infinity; + if (densities) { const densityValues = densities.map((density) => { if (typeof density === 'number') { @@ -235,14 +237,17 @@ export const baseService: Omit = { } }); - const densityWidths = densityValues.map((density) => Math.round(targetWidth * density)); + const densityWidths = densityValues + .sort() + .map((density) => Math.round(targetWidth * density)); densityWidths.forEach((width, index) => { + const maxTargetWidth = Math.min(width, maxWidth); srcSet.push({ transform: { ...options, - width, - height: Math.round(width / aspectRatio), + width: maxTargetWidth, + height: Math.round(maxTargetWidth / aspectRatio), format: targetFormat, }, descriptor: `${densityValues[index]}x`, @@ -253,11 +258,12 @@ export const baseService: Omit = { }); } else if (widths) { widths.forEach((width) => { + const maxTargetWidth = Math.min(width, maxWidth); srcSet.push({ transform: { ...options, width, - height: Math.round(width / aspectRatio), + height: Math.round(maxTargetWidth / aspectRatio), format: targetFormat, }, descriptor: `${width}w`, diff --git a/packages/astro/src/assets/types.ts b/packages/astro/src/assets/types.ts index 92a66044d..f8ee91cd8 100644 --- a/packages/astro/src/assets/types.ts +++ b/packages/astro/src/assets/types.ts @@ -59,8 +59,10 @@ export interface GetImageResult { rawOptions: ImageTransform; options: ImageTransform; src: string; - srcSet: SrcSetValue[]; - srcSetValue: string; + srcSet: { + values: SrcSetValue[]; + attribute: string; + }; attributes: Record; } diff --git a/packages/astro/src/assets/utils/transformToPath.ts b/packages/astro/src/assets/utils/transformToPath.ts index d5535137b..c998140fa 100644 --- a/packages/astro/src/assets/utils/transformToPath.ts +++ b/packages/astro/src/assets/utils/transformToPath.ts @@ -16,8 +16,8 @@ export function propsToFilename(transform: ImageTransform, hash: string) { } export function hashTransform(transform: ImageTransform, imageService: string) { - // take everything from transform except alt, which is not used in the hash - const { alt, ...rest } = transform; + // Extract the fields we want to hash + const { alt, class: className, style, ...rest } = transform; const hashFields = { ...rest, imageService }; return shorthash(JSON.stringify(hashFields)); } diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index e4fe35540..0d8626ffc 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -621,6 +621,21 @@ export const ExpectedImageOptions = { `Expected getImage() parameter to be an object. Received \`${options}\`.`, } satisfies ErrorData; +/** + * @docs + * @see + * - [Images](https://docs.astro.build/en/guides/images/) + * @description + * Only one of `densities` or `widths` can be specified. Those attributes are used to construct a `srcset` attribute, which cannot have both `x` and `w` descriptors. + */ +export const IncompatibleDescriptorOptions = { + name: 'IncompatibleDescriptorOptions', + title: 'Cannot set both `densities` and `widths`', + message: + "Only one of `densities` or `widths` can be specified. In most cases, you'll probably want to use only `widths` if you require specific widths.", + hint: 'Those attributes are used to construct a `srcset` attribute, which cannot have both `x` and `w` descriptors.', +} satisfies ErrorData; + /** * @docs * @see