296 lines
9.7 KiB
JavaScript
296 lines
9.7 KiB
JavaScript
import { isRemotePath, joinPaths } from '@astrojs/internal-helpers/path';
|
|
import { A as AstroError, E as ExpectedImage, L as LocalImageUsedWrongly, M as MissingImageDimension, U as UnsupportedImageFormat, I as InvalidImageService, a as ExpectedImageOptions, b as MissingSharp } from './astro_bfe7ba8a.mjs';
|
|
|
|
const VALID_SUPPORTED_FORMATS = [
|
|
"jpeg",
|
|
"jpg",
|
|
"png",
|
|
"tiff",
|
|
"webp",
|
|
"gif",
|
|
"svg",
|
|
"avif"
|
|
];
|
|
|
|
function isLocalService(service) {
|
|
if (!service) {
|
|
return false;
|
|
}
|
|
return "transform" in service;
|
|
}
|
|
function parseQuality(quality) {
|
|
let result = parseInt(quality);
|
|
if (Number.isNaN(result)) {
|
|
return quality;
|
|
}
|
|
return result;
|
|
}
|
|
const baseService = {
|
|
validateOptions(options) {
|
|
if (!options.src || typeof options.src !== "string" && typeof options.src !== "object") {
|
|
throw new AstroError({
|
|
...ExpectedImage,
|
|
message: ExpectedImage.message(
|
|
JSON.stringify(options.src),
|
|
typeof options.src,
|
|
JSON.stringify(options, (_, v) => v === void 0 ? null : v)
|
|
)
|
|
});
|
|
}
|
|
if (!isESMImportedImage(options.src)) {
|
|
if (options.src.startsWith("/@fs/") || !isRemotePath(options.src) && !options.src.startsWith("/")) {
|
|
throw new AstroError({
|
|
...LocalImageUsedWrongly,
|
|
message: LocalImageUsedWrongly.message(options.src)
|
|
});
|
|
}
|
|
let missingDimension;
|
|
if (!options.width && !options.height) {
|
|
missingDimension = "both";
|
|
} else if (!options.width && options.height) {
|
|
missingDimension = "width";
|
|
} else if (options.width && !options.height) {
|
|
missingDimension = "height";
|
|
}
|
|
if (missingDimension) {
|
|
throw new AstroError({
|
|
...MissingImageDimension,
|
|
message: MissingImageDimension.message(missingDimension, options.src)
|
|
});
|
|
}
|
|
} else {
|
|
if (!VALID_SUPPORTED_FORMATS.includes(options.src.format)) {
|
|
throw new AstroError({
|
|
...UnsupportedImageFormat,
|
|
message: UnsupportedImageFormat.message(
|
|
options.src.format,
|
|
options.src.src,
|
|
VALID_SUPPORTED_FORMATS
|
|
)
|
|
});
|
|
}
|
|
if (options.src.format === "svg") {
|
|
options.format = "svg";
|
|
}
|
|
}
|
|
if (!options.format) {
|
|
options.format = "webp";
|
|
}
|
|
return options;
|
|
},
|
|
getHTMLAttributes(options) {
|
|
let targetWidth = options.width;
|
|
let targetHeight = options.height;
|
|
if (isESMImportedImage(options.src)) {
|
|
const aspectRatio = options.src.width / options.src.height;
|
|
if (targetHeight && !targetWidth) {
|
|
targetWidth = Math.round(targetHeight * aspectRatio);
|
|
} else if (targetWidth && !targetHeight) {
|
|
targetHeight = Math.round(targetWidth / aspectRatio);
|
|
} else if (!targetWidth && !targetHeight) {
|
|
targetWidth = options.src.width;
|
|
targetHeight = options.src.height;
|
|
}
|
|
}
|
|
const { src, width, height, format, quality, ...attributes } = options;
|
|
return {
|
|
...attributes,
|
|
width: targetWidth,
|
|
height: targetHeight,
|
|
loading: attributes.loading ?? "lazy",
|
|
decoding: attributes.decoding ?? "async"
|
|
};
|
|
},
|
|
getURL(options, imageConfig) {
|
|
const searchParams = new URLSearchParams();
|
|
if (isESMImportedImage(options.src)) {
|
|
searchParams.append("href", options.src.src);
|
|
} else if (isRemoteAllowed(options.src, imageConfig)) {
|
|
searchParams.append("href", options.src);
|
|
} else {
|
|
return options.src;
|
|
}
|
|
const params = {
|
|
w: "width",
|
|
h: "height",
|
|
q: "quality",
|
|
f: "format"
|
|
};
|
|
Object.entries(params).forEach(([param, key]) => {
|
|
options[key] && searchParams.append(param, options[key].toString());
|
|
});
|
|
const imageEndpoint = joinPaths("/", "/_image");
|
|
return `${imageEndpoint}?${searchParams}`;
|
|
},
|
|
parseURL(url) {
|
|
const params = url.searchParams;
|
|
if (!params.has("href")) {
|
|
return void 0;
|
|
}
|
|
const transform = {
|
|
src: params.get("href"),
|
|
width: params.has("w") ? parseInt(params.get("w")) : void 0,
|
|
height: params.has("h") ? parseInt(params.get("h")) : void 0,
|
|
format: params.get("f"),
|
|
quality: params.get("q")
|
|
};
|
|
return transform;
|
|
}
|
|
};
|
|
|
|
function matchPattern(url, remotePattern) {
|
|
return matchProtocol(url, remotePattern.protocol) && matchHostname(url, remotePattern.hostname, true) && matchPort(url, remotePattern.port) && matchPathname(url, remotePattern.pathname, true);
|
|
}
|
|
function matchPort(url, port) {
|
|
return !port || port === url.port;
|
|
}
|
|
function matchProtocol(url, protocol) {
|
|
return !protocol || protocol === url.protocol.slice(0, -1);
|
|
}
|
|
function matchHostname(url, hostname, allowWildcard) {
|
|
if (!hostname) {
|
|
return true;
|
|
} else if (!allowWildcard || !hostname.startsWith("*")) {
|
|
return hostname === url.hostname;
|
|
} else if (hostname.startsWith("**.")) {
|
|
const slicedHostname = hostname.slice(2);
|
|
return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname);
|
|
} else if (hostname.startsWith("*.")) {
|
|
const slicedHostname = hostname.slice(1);
|
|
const additionalSubdomains = url.hostname.replace(slicedHostname, "").split(".").filter(Boolean);
|
|
return additionalSubdomains.length === 1;
|
|
}
|
|
return false;
|
|
}
|
|
function matchPathname(url, pathname, allowWildcard) {
|
|
if (!pathname) {
|
|
return true;
|
|
} else if (!allowWildcard || !pathname.endsWith("*")) {
|
|
return pathname === url.pathname;
|
|
} else if (pathname.endsWith("/**")) {
|
|
const slicedPathname = pathname.slice(0, -2);
|
|
return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname);
|
|
} else if (pathname.endsWith("/*")) {
|
|
const slicedPathname = pathname.slice(0, -1);
|
|
const additionalPathChunks = url.pathname.replace(slicedPathname, "").split("/").filter(Boolean);
|
|
return additionalPathChunks.length === 1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isESMImportedImage(src) {
|
|
return typeof src === "object";
|
|
}
|
|
function isRemoteImage(src) {
|
|
return typeof src === "string";
|
|
}
|
|
function isRemoteAllowed(src, {
|
|
domains = [],
|
|
remotePatterns = []
|
|
}) {
|
|
if (!isRemotePath(src))
|
|
return false;
|
|
const url = new URL(src);
|
|
return domains.some((domain) => matchHostname(url, domain)) || remotePatterns.some((remotePattern) => matchPattern(url, remotePattern));
|
|
}
|
|
async function getConfiguredImageService() {
|
|
if (!globalThis?.astroAsset?.imageService) {
|
|
const { default: service } = await Promise.resolve().then(() => sharp$1).catch((e) => {
|
|
const error = new AstroError(InvalidImageService);
|
|
error.cause = e;
|
|
throw error;
|
|
});
|
|
if (!globalThis.astroAsset)
|
|
globalThis.astroAsset = {};
|
|
globalThis.astroAsset.imageService = service;
|
|
return service;
|
|
}
|
|
return globalThis.astroAsset.imageService;
|
|
}
|
|
async function getImage(options, imageConfig) {
|
|
if (!options || typeof options !== "object") {
|
|
throw new AstroError({
|
|
...ExpectedImageOptions,
|
|
message: ExpectedImageOptions.message(JSON.stringify(options))
|
|
});
|
|
}
|
|
const service = await getConfiguredImageService();
|
|
const resolvedOptions = {
|
|
...options,
|
|
src: typeof options.src === "object" && "then" in options.src ? (await options.src).default ?? await options.src : options.src
|
|
};
|
|
const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions, imageConfig) : resolvedOptions;
|
|
let imageURL = await service.getURL(validatedOptions, imageConfig);
|
|
if (isLocalService(service) && globalThis.astroAsset.addStaticImage && // If `getURL` returned the same URL as the user provided, it means the service doesn't need to do anything
|
|
!(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)) {
|
|
imageURL = globalThis.astroAsset.addStaticImage(validatedOptions);
|
|
}
|
|
return {
|
|
rawOptions: resolvedOptions,
|
|
options: validatedOptions,
|
|
src: imageURL,
|
|
attributes: service.getHTMLAttributes !== void 0 ? service.getHTMLAttributes(validatedOptions, imageConfig) : {}
|
|
};
|
|
}
|
|
|
|
let sharp;
|
|
const qualityTable = {
|
|
low: 25,
|
|
mid: 50,
|
|
high: 80,
|
|
max: 100
|
|
};
|
|
async function loadSharp() {
|
|
let sharpImport;
|
|
try {
|
|
sharpImport = (await import('sharp')).default;
|
|
} catch (e) {
|
|
throw new AstroError(MissingSharp);
|
|
}
|
|
return sharpImport;
|
|
}
|
|
const sharpService = {
|
|
validateOptions: baseService.validateOptions,
|
|
getURL: baseService.getURL,
|
|
parseURL: baseService.parseURL,
|
|
getHTMLAttributes: baseService.getHTMLAttributes,
|
|
async transform(inputBuffer, transformOptions) {
|
|
if (!sharp)
|
|
sharp = await loadSharp();
|
|
const transform = transformOptions;
|
|
if (transform.format === "svg")
|
|
return { data: inputBuffer, format: "svg" };
|
|
let result = sharp(inputBuffer, { failOnError: false, pages: -1 });
|
|
result.rotate();
|
|
if (transform.height && !transform.width) {
|
|
result.resize({ height: transform.height });
|
|
} else if (transform.width) {
|
|
result.resize({ width: transform.width });
|
|
}
|
|
if (transform.format) {
|
|
let quality = void 0;
|
|
if (transform.quality) {
|
|
const parsedQuality = parseQuality(transform.quality);
|
|
if (typeof parsedQuality === "number") {
|
|
quality = parsedQuality;
|
|
} else {
|
|
quality = transform.quality in qualityTable ? qualityTable[transform.quality] : void 0;
|
|
}
|
|
}
|
|
result.toFormat(transform.format, { quality });
|
|
}
|
|
const { data, info } = await result.toBuffer({ resolveWithObject: true });
|
|
return {
|
|
data,
|
|
format: info.format
|
|
};
|
|
}
|
|
};
|
|
var sharp_default = sharpService;
|
|
|
|
const sharp$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
|
__proto__: null,
|
|
default: sharp_default
|
|
}, Symbol.toStringTag, { value: 'Module' }));
|
|
|
|
export { getConfiguredImageService as a, getImage as g, isRemoteAllowed as i };
|