--- import { getImage, imageConfig, type LocalImageProps, type RemoteImageProps } from 'astro:assets'; import * as mime from 'mrmime'; import { isESMImportedImage, resolveSrc } from '../dist/assets/utils/imageKind.js'; import { AstroError, AstroErrorData } from '../dist/core/errors/index.js'; import type { GetImageResult, ImageOutputFormat, UnresolvedImageTransform, } from '../dist/types/public/index.js'; import type { HTMLAttributes } from '../types'; export 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); } // Picture attribute inherit scoped styles from class and attributes const scopedStyleClass = props.class?.match(/\bastro-\w{8}\b/)?.[0]; if (scopedStyleClass) { if (pictureAttributes.class) { pictureAttributes.class = `${pictureAttributes.class} ${scopedStyleClass}`; } else { pictureAttributes.class = scopedStyleClass; } } const layout = props.layout ?? imageConfig.layout ?? 'none'; const useResponsive = layout !== 'none'; if (useResponsive) { // Apply defaults from imageConfig if not provided props.layout ??= imageConfig.layout; props.fit ??= imageConfig.objectFit ?? 'cover'; props.position ??= imageConfig.objectPosition ?? 'center'; } for (const key in props) { if (key.startsWith('data-astro-cid')) { // @ts-expect-error This is for island props so they're not properly typed pictureAttributes[key] = props[key]; } } const originalSrc = await resolveSrc(props.src); const optimizedImages: GetImageResult[] = await Promise.all( formats.map( async (format) => await getImage({ ...props, src: originalSrc, format: format, widths: props.widths, densities: props.densities, } as UnresolvedImageTransform), ), ); let resultFallbackFormat = fallbackFormat ?? defaultFallbackFormat; if ( !fallbackFormat && isESMImportedImage(originalSrc) && (specialFormatsFallback as ReadonlyArray).includes(originalSrc.format) ) { resultFallbackFormat = originalSrc.format; } const fallbackImage = await getImage({ ...props, format: resultFallbackFormat, widths: props.widths, densities: props.densities, } as UnresolvedImageTransform); const imgAdditionalAttributes: HTMLAttributes<'img'> = {}; const sourceAdditionalAttributes: HTMLAttributes<'source'> = {}; // Propagate the `sizes` attribute to the `source` elements if (props.sizes) { sourceAdditionalAttributes.sizes = props.sizes; } if (fallbackImage.srcSet.values.length > 0) { imgAdditionalAttributes.srcset = fallbackImage.srcSet.attribute; } if (import.meta.env.DEV) { imgAdditionalAttributes['data-image-component'] = 'true'; } const { class: className, ...attributes } = { ...imgAdditionalAttributes, ...fallbackImage.attributes, }; --- { Object.entries(optimizedImages).map(([_, image]) => { const srcsetAttribute = props.densities || (!props.densities && !props.widths && !useResponsive) ? `${image.src}${image.srcSet.values.length > 0 ? ', ' + image.srcSet.attribute : ''}` : image.srcSet.attribute; return ( ); }) } {/* Applying class outside of the spread prevents it from applying unnecessary astro-* classes */}