UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

187 lines (186 loc) • 6.54 kB
import { AstroError, AstroErrorData } from "../../core/errors/index.js"; import { resolveDefaultOutputFormat } from "../utils/inferSourceFormat.js"; import { detector } from "../utils/vendor/image-size/detector.js"; import { baseService, parseQuality } from "./service.js"; let sharp; const qualityTable = { low: 25, mid: 50, high: 80, max: 100 }; function resolveSharpQuality(quality) { if (!quality) return undefined; const parsedQuality = parseQuality(quality); if (typeof parsedQuality === "number") { return parsedQuality; } return quality in qualityTable ? qualityTable[quality] : undefined; } export function resolveSharpEncoderOptions(transform, inputFormat, serviceConfig = {}) { const quality = resolveSharpQuality(transform.quality); if (transform.format === undefined) { return quality === undefined ? undefined : { quality }; } switch (transform.format) { case "jpg": case "jpeg": return { ...serviceConfig.jpeg, ...quality === undefined ? {} : { quality } }; case "png": return { ...serviceConfig.png, ...quality === undefined ? {} : { quality } }; case "webp": { const webpOptions = { ...serviceConfig.webp, ...quality === undefined ? {} : { quality } }; if (inputFormat === "gif") { webpOptions.loop ??= 0; } return webpOptions; } case "avif": return { ...serviceConfig.avif, ...quality === undefined ? {} : { quality } }; default: return quality === undefined ? undefined : { quality }; } } async function loadSharp() { let sharpImport; try { sharpImport = (await import("sharp")).default; } catch { throw new AstroError(AstroErrorData.MissingSharp); } // Disable the `sharp` `libvips` cache as it errors when the file is too small and operations are happening too fast (runs into a race condition) https://github.com/lovell/sharp/issues/3935#issuecomment-1881866341 sharpImport.cache(false); return sharpImport; } const fitMap = { fill: "fill", contain: "inside", cover: "cover", none: "outside", "scale-down": "inside", outside: "outside", inside: "inside" }; const sharpService = { validateOptions: baseService.validateOptions, getURL: baseService.getURL, parseURL: baseService.parseURL, getHTMLAttributes: baseService.getHTMLAttributes, getSrcSet: baseService.getSrcSet, getRemoteSize: baseService.getRemoteSize, async transform(inputBuffer, transformOptions, config, logger) { if (!sharp) sharp = await loadSharp(); const transform = transformOptions; const kernel = config.service.config.kernel; const bufferFormat = detector(inputBuffer); // Resolve the output format from the buffer when validateOptions deferred (ambiguous remote URL hit SSR, manually formed URLs etc) const outputFormat = transform.format ?? resolveDefaultOutputFormat(bufferFormat); // TODO: Sharp has some support for SVGs, we could probably support this once Sharp is the default and only service. if (outputFormat === "svg") { if (bufferFormat && bufferFormat !== "svg") { logger.warn(`Astro expected an SVG for "${transform.src}" but the source is ${bufferFormat}. Passing it through as ${bufferFormat} instead.`); return { data: inputBuffer, format: bufferFormat }; } return { data: inputBuffer, format: "svg" }; } // If we couldn't figure out the format, it's probably something weird we shouldn't try to process. if (!bufferFormat) { throw new AstroError({ ...AstroErrorData.NoImageMetadata, message: AstroErrorData.NoImageMetadata.message(transform.src) }); } if (bufferFormat === "svg" && !config.dangerouslyProcessSVG) { throw new AstroError({ ...AstroErrorData.UnsupportedImageFormat, message: `SVG image processing is disabled, but the source for "${transform.src}" is an SVG. Pass it through unchanged by setting \`format="svg"\` on the component, or set \`image.dangerouslyProcessSVG: true\` to rasterize SVG sources.` }); } const result = sharp(inputBuffer, { failOn: "none", pages: -1, limitInputPixels: config.service.config.limitInputPixels }); // always call rotate to adjust for EXIF data orientation result.rotate(); if (transform.width && transform.height) { const fit = transform.fit ? fitMap[transform.fit] ?? "inside" : undefined; result.resize({ width: Math.round(transform.width), height: Math.round(transform.height), kernel, fit, position: transform.position, withoutEnlargement: true }); } else if (transform.height && !transform.width) { result.resize({ height: Math.round(transform.height), withoutEnlargement: true, kernel }); } else if (transform.width) { result.resize({ width: Math.round(transform.width), withoutEnlargement: true, kernel }); } // If background is set, flatten the image with the specified background. // We do this after resize to ensure the background covers the entire image // even if its size has expanded. if (transform.background) { result.flatten({ background: transform.background }); } const encoderOptions = resolveSharpEncoderOptions({ format: outputFormat, quality: transform.quality }, bufferFormat, config.service.config); if (outputFormat === "webp") { result.webp(encoderOptions); } else if (outputFormat === "png") { result.png(encoderOptions); } else if (outputFormat === "avif") { result.avif(encoderOptions); } else if (outputFormat === "jpeg" || outputFormat === "jpg") { result.jpeg(encoderOptions); } else { result.toFormat(outputFormat, encoderOptions); } let data; try { data = await result.toBuffer(); } catch { // Sharp cannot decode this image (e.g. animated AVIF sequences). // Pass it through unmodified rather than crashing the build. When Sharp adds support for these // formats, the image will be optimized automatically without code changes. logger.warn(`Astro could not optimize image "${transform.src}". Sharp doesn't support this format. The image will be used unoptimized. Consider converting to WebP or placing in the public/ folder.`); return { data: inputBuffer, format: bufferFormat }; } // Sharp can sometimes return a SharedArrayBuffer when using WebAssembly. // SharedArrayBuffers need to be copied into an ArrayBuffer in order to be manipulated. const needsCopy = "buffer" in data && data.buffer instanceof SharedArrayBuffer; return { data: needsCopy ? new Uint8Array(data) : data, format: outputFormat }; } }; export default sharpService;