feat: update with RFC feedback
This commit is contained in:
parent
3809a452ae
commit
7398d63331
7 changed files with 70 additions and 34 deletions
|
@ -23,10 +23,11 @@ if (typeof props.height === 'string') {
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = await getImage(props);
|
const image = await getImage(props);
|
||||||
|
|
||||||
const additionalAttributes: Record<string, any> = {};
|
const additionalAttributes: Record<string, any> = {};
|
||||||
|
|
||||||
if (image.srcSet.length > 0) {
|
if (image.srcSet.values.length > 0) {
|
||||||
additionalAttributes.srcset = image.srcSetValue;
|
additionalAttributes.srcset = image.srcSet.attribute;
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -6,17 +6,22 @@ import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
|
||||||
import { HTMLAttributes } from '../types';
|
import { HTMLAttributes } from '../types';
|
||||||
|
|
||||||
type Props = (LocalImageProps | RemoteImageProps) & {
|
type Props = (LocalImageProps | RemoteImageProps) & {
|
||||||
formats: ImageOutputFormat[];
|
formats?: ImageOutputFormat[];
|
||||||
fallbackFormat: ImageOutputFormat;
|
fallbackFormat?: ImageOutputFormat;
|
||||||
pictureAttributes: HTMLAttributes<'picture'>;
|
pictureAttributes?: HTMLAttributes<'picture'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = Astro.props;
|
const {formats = ["webp"], pictureAttributes = {}, ...props} = Astro.props;
|
||||||
const optimizedImages: Record<
|
|
||||||
ImageOutputFormat,
|
if (props.alt === undefined || props.alt === null) {
|
||||||
GetImageResult
|
throw new AstroError(AstroErrorData.ImageMissingAlt);
|
||||||
> = await Promise.all(
|
}
|
||||||
props.formats.map(async (format) => await getImage({ ...props, format: format, widths: props.widths, densities: props.densities }))
|
|
||||||
|
const optimizedImages: GetImageResult[] = await Promise.all(
|
||||||
|
formats.map(
|
||||||
|
async (format) =>
|
||||||
|
await getImage({ ...props, format: format, widths: props.widths, densities: props.densities })
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const fallbackFormat =
|
const fallbackFormat =
|
||||||
|
@ -26,19 +31,24 @@ const fallbackFormat =
|
||||||
: 'png'
|
: 'png'
|
||||||
: '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) {
|
const additionalAttributes: Record<string, any> = {};
|
||||||
throw new AstroError(AstroErrorData.ImageMissingAlt);
|
if (fallbackImage.srcSet.values.length > 0) {
|
||||||
|
additionalAttributes.srcset = fallbackImage.srcSet.attribute;
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<picture {...props.pictureAttributes}>
|
<picture {...pictureAttributes}>
|
||||||
{Object.entries(optimizedImages).map(([format, image]) => (
|
{
|
||||||
<source
|
Object.entries(optimizedImages).map(([_, image]) => (
|
||||||
srcset={image.srcSetValue}
|
<source srcset={`${image.src}, ` + image.srcSet.attribute} type={image.srcSet.values[0].attributes?.type} />
|
||||||
type={image.srcSet[0].attributes?.type}
|
))
|
||||||
/>
|
}
|
||||||
))}
|
<img src={fallbackImage.src} {...additionalAttributes} {...fallbackImage.attributes} />
|
||||||
<img src={originalImage.src} {...originalImage.attributes} />
|
|
||||||
</picture>
|
</picture>
|
||||||
|
|
|
@ -124,8 +124,10 @@ export async function getImage(
|
||||||
rawOptions: resolvedOptions,
|
rawOptions: resolvedOptions,
|
||||||
options: validatedOptions,
|
options: validatedOptions,
|
||||||
src: imageURL,
|
src: imageURL,
|
||||||
srcSet: srcSets,
|
srcSet: {
|
||||||
srcSetValue: srcSets.map((srcSet) => `${srcSet.url} ${srcSet.descriptor}`).join(', '),
|
values: srcSets,
|
||||||
|
attribute: srcSets.map((srcSet) => `${srcSet.url} ${srcSet.descriptor}`).join(', '),
|
||||||
|
},
|
||||||
attributes:
|
attributes:
|
||||||
service.getHTMLAttributes !== undefined
|
service.getHTMLAttributes !== undefined
|
||||||
? service.getHTMLAttributes(validatedOptions, imageConfig)
|
? service.getHTMLAttributes(validatedOptions, imageConfig)
|
||||||
|
|
|
@ -188,8 +188,7 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.widths && options.densities) {
|
if (options.widths && options.densities) {
|
||||||
console.warn('Cannot use `widths` and `densities` at the same time. Using `densities`.');
|
throw new AstroError(AstroErrorData.IncompatibleDescriptorOptions);
|
||||||
options.widths = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We currently do not support processing SVGs, so whenever the input format is a SVG, force the output to also be one
|
// 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<LocalImageService, 'transform'> = {
|
||||||
const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT;
|
const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT;
|
||||||
|
|
||||||
const aspectRatio = targetWidth / targetHeight;
|
const aspectRatio = targetWidth / targetHeight;
|
||||||
|
const imageWidth = isESMImportedImage(options.src) ? options.src.width : options.width;
|
||||||
|
const maxWidth = options.width ?? imageWidth ?? Infinity;
|
||||||
|
|
||||||
if (densities) {
|
if (densities) {
|
||||||
const densityValues = densities.map((density) => {
|
const densityValues = densities.map((density) => {
|
||||||
if (typeof density === 'number') {
|
if (typeof density === 'number') {
|
||||||
|
@ -235,14 +237,17 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const densityWidths = densityValues.map((density) => Math.round(targetWidth * density));
|
const densityWidths = densityValues
|
||||||
|
.sort()
|
||||||
|
.map((density) => Math.round(targetWidth * density));
|
||||||
|
|
||||||
densityWidths.forEach((width, index) => {
|
densityWidths.forEach((width, index) => {
|
||||||
|
const maxTargetWidth = Math.min(width, maxWidth);
|
||||||
srcSet.push({
|
srcSet.push({
|
||||||
transform: {
|
transform: {
|
||||||
...options,
|
...options,
|
||||||
width,
|
width: maxTargetWidth,
|
||||||
height: Math.round(width / aspectRatio),
|
height: Math.round(maxTargetWidth / aspectRatio),
|
||||||
format: targetFormat,
|
format: targetFormat,
|
||||||
},
|
},
|
||||||
descriptor: `${densityValues[index]}x`,
|
descriptor: `${densityValues[index]}x`,
|
||||||
|
@ -253,11 +258,12 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
|
||||||
});
|
});
|
||||||
} else if (widths) {
|
} else if (widths) {
|
||||||
widths.forEach((width) => {
|
widths.forEach((width) => {
|
||||||
|
const maxTargetWidth = Math.min(width, maxWidth);
|
||||||
srcSet.push({
|
srcSet.push({
|
||||||
transform: {
|
transform: {
|
||||||
...options,
|
...options,
|
||||||
width,
|
width,
|
||||||
height: Math.round(width / aspectRatio),
|
height: Math.round(maxTargetWidth / aspectRatio),
|
||||||
format: targetFormat,
|
format: targetFormat,
|
||||||
},
|
},
|
||||||
descriptor: `${width}w`,
|
descriptor: `${width}w`,
|
||||||
|
|
|
@ -59,8 +59,10 @@ export interface GetImageResult {
|
||||||
rawOptions: ImageTransform;
|
rawOptions: ImageTransform;
|
||||||
options: ImageTransform;
|
options: ImageTransform;
|
||||||
src: string;
|
src: string;
|
||||||
srcSet: SrcSetValue[];
|
srcSet: {
|
||||||
srcSetValue: string;
|
values: SrcSetValue[];
|
||||||
|
attribute: string;
|
||||||
|
};
|
||||||
attributes: Record<string, any>;
|
attributes: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ export function propsToFilename(transform: ImageTransform, hash: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hashTransform(transform: ImageTransform, imageService: string) {
|
export function hashTransform(transform: ImageTransform, imageService: string) {
|
||||||
// take everything from transform except alt, which is not used in the hash
|
// Extract the fields we want to hash
|
||||||
const { alt, ...rest } = transform;
|
const { alt, class: className, style, ...rest } = transform;
|
||||||
const hashFields = { ...rest, imageService };
|
const hashFields = { ...rest, imageService };
|
||||||
return shorthash(JSON.stringify(hashFields));
|
return shorthash(JSON.stringify(hashFields));
|
||||||
}
|
}
|
||||||
|
|
|
@ -621,6 +621,21 @@ export const ExpectedImageOptions = {
|
||||||
`Expected getImage() parameter to be an object. Received \`${options}\`.`,
|
`Expected getImage() parameter to be an object. Received \`${options}\`.`,
|
||||||
} satisfies ErrorData;
|
} 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
|
* @docs
|
||||||
* @see
|
* @see
|
||||||
|
|
Loading…
Reference in a new issue