84 lines
2.7 KiB
Plaintext
84 lines
2.7 KiB
Plaintext
---
|
|
import { getImage, type LocalImageProps, type RemoteImageProps } from 'astro:assets';
|
|
import type { GetImageResult, ImageOutputFormat } from '../dist/@types/astro';
|
|
import { isESMImportedImage } from '../dist/assets/utils/imageKind';
|
|
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 defaultFormats = ['webp'] as const;
|
|
const defaultFallbackFormat = 'png' as const;
|
|
|
|
// Certain formats don't want PNG fallbacks:
|
|
// - GIF will typically want to stay as a gif, either for animation or for the lower amount of colors
|
|
// - SVGs can't be converted to raster formats in most cases
|
|
// - JPEGs compress photographs and high-noise images better than PNG in most cases
|
|
// For those, we'll use the original format as the fallback instead.
|
|
const specialFormatsFallback = ['gif', 'svg', 'jpg', 'jpeg'] as const;
|
|
|
|
const { formats = defaultFormats, pictureAttributes = {}, fallbackFormat, ...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 })
|
|
)
|
|
);
|
|
|
|
let resultFallbackFormat = fallbackFormat ?? defaultFallbackFormat;
|
|
if (
|
|
!fallbackFormat &&
|
|
isESMImportedImage(props.src) &&
|
|
specialFormatsFallback.includes(props.src.format)
|
|
) {
|
|
resultFallbackFormat = props.src.format;
|
|
}
|
|
|
|
const fallbackImage = await getImage({
|
|
...props,
|
|
format: resultFallbackFormat,
|
|
widths: props.widths,
|
|
densities: props.densities,
|
|
});
|
|
|
|
const imgAdditionalAttributes: HTMLAttributes<'img'> = {};
|
|
const sourceAdditionaAttributes: HTMLAttributes<'source'> = {};
|
|
|
|
// Propagate the `sizes` attribute to the `source` elements
|
|
if (props.sizes) {
|
|
sourceAdditionaAttributes.sizes = props.sizes;
|
|
}
|
|
|
|
if (fallbackImage.srcSet.values.length > 0) {
|
|
imgAdditionalAttributes.srcset = fallbackImage.srcSet.attribute;
|
|
}
|
|
---
|
|
|
|
<picture {...pictureAttributes}>
|
|
{
|
|
Object.entries(optimizedImages).map(([_, image]) => {
|
|
const srcsetAttribute =
|
|
props.densities || (!props.densities && !props.widths)
|
|
? `${image.src}${image.srcSet.values.length > 0 ? ', ' + image.srcSet.attribute : ''}`
|
|
: image.srcSet.attribute;
|
|
return (
|
|
<source
|
|
srcset={srcsetAttribute}
|
|
type={'image/' + image.options.format}
|
|
{...sourceAdditionaAttributes}
|
|
/>
|
|
);
|
|
})
|
|
}
|
|
<img src={fallbackImage.src} {...imgAdditionalAttributes} {...fallbackImage.attributes} />
|
|
</picture>
|