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
JavaScript
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;