145 lines
3.8 KiB
TypeScript
145 lines
3.8 KiB
TypeScript
const IMAGE_PROXY_PATH = "/api/image-proxy";
|
|
|
|
type ImageProxyFormat = "webp" | "avif" | "jpeg" | "png";
|
|
|
|
export type ImageProxyOptions = {
|
|
width?: number;
|
|
quality?: number;
|
|
format?: ImageProxyFormat;
|
|
delivery?: "display" | "proxy";
|
|
};
|
|
|
|
function clamp(value: number, min: number, max: number): number {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
function normalizeWidth(width?: number): number | undefined {
|
|
if (!Number.isFinite(width) || !width) return undefined;
|
|
return clamp(Math.round(width), 64, 4096);
|
|
}
|
|
|
|
function normalizeQuality(quality?: number): number | undefined {
|
|
if (!Number.isFinite(quality) || !quality) return undefined;
|
|
return clamp(Math.round(quality), 40, 100);
|
|
}
|
|
|
|
function isAbsoluteHttpUrl(url: string): boolean {
|
|
return /^https?:\/\//i.test(url);
|
|
}
|
|
|
|
function isBypassUrl(url: string): boolean {
|
|
return (
|
|
!url ||
|
|
url.startsWith("data:") ||
|
|
url.startsWith("blob:") ||
|
|
url.startsWith(IMAGE_PROXY_PATH) ||
|
|
!isAbsoluteHttpUrl(url)
|
|
);
|
|
}
|
|
|
|
function isAliyunOssImageHost(hostname: string): boolean {
|
|
const normalized = hostname.trim().toLowerCase();
|
|
return normalized.endsWith(".aliyuncs.com") && normalized.includes(".oss-");
|
|
}
|
|
|
|
function isTrustedDirectStorageImageHost(hostname: string): boolean {
|
|
const normalized = hostname.trim().toLowerCase();
|
|
return (
|
|
normalized === "yybb.top" ||
|
|
normalized.endsWith(".qiniucs.com") ||
|
|
normalized.endsWith(".qiniudn.com") ||
|
|
normalized.endsWith(".clouddn.com") ||
|
|
normalized.endsWith(".qbox.me")
|
|
);
|
|
}
|
|
|
|
function resolveTrustedDirectStorageDisplayUrl(url: string): string | null {
|
|
try {
|
|
const target = new URL(url);
|
|
return isTrustedDirectStorageImageHost(target.hostname)
|
|
? target.toString()
|
|
: null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function resolveAliyunOssDisplayUrl(
|
|
url: string,
|
|
options: Pick<ImageProxyOptions, "width" | "quality" | "format">
|
|
): string | null {
|
|
let target: URL;
|
|
try {
|
|
target = new URL(url);
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
if (!isAliyunOssImageHost(target.hostname)) {
|
|
return null;
|
|
}
|
|
|
|
if (target.searchParams.has("x-oss-process")) {
|
|
return target.toString();
|
|
}
|
|
|
|
const width = normalizeWidth(options.width);
|
|
const quality = normalizeQuality(options.quality);
|
|
const format = options.format;
|
|
|
|
if (!width && !quality && !format) {
|
|
return target.toString();
|
|
}
|
|
|
|
const processSteps = ["image"];
|
|
if (width) {
|
|
processSteps.push(`resize,w_${width}`);
|
|
}
|
|
if (quality) {
|
|
processSteps.push(`quality,q_${quality}`);
|
|
}
|
|
if (format) {
|
|
const ossFormat = format === "jpeg" ? "jpg" : format;
|
|
if (ossFormat !== "webp" && ossFormat !== "jpg" && ossFormat !== "png") {
|
|
return null;
|
|
}
|
|
processSteps.push(`format,${ossFormat}`);
|
|
}
|
|
|
|
target.searchParams.set("x-oss-process", processSteps.join("/"));
|
|
return target.toString();
|
|
}
|
|
|
|
export function buildImageProxyUrl(url: string, options: ImageProxyOptions = {}): string {
|
|
const source = String(url || "").trim();
|
|
if (isBypassUrl(source)) {
|
|
return source;
|
|
}
|
|
|
|
const trustedDirectStorageUrl = resolveTrustedDirectStorageDisplayUrl(source);
|
|
if (trustedDirectStorageUrl) {
|
|
return trustedDirectStorageUrl;
|
|
}
|
|
|
|
if (options.delivery !== "proxy") {
|
|
const directDisplayUrl = resolveAliyunOssDisplayUrl(source, options);
|
|
if (directDisplayUrl) {
|
|
return directDisplayUrl;
|
|
}
|
|
return source;
|
|
}
|
|
|
|
const width = normalizeWidth(options.width);
|
|
const quality = normalizeQuality(options.quality);
|
|
const format = options.format;
|
|
|
|
const params = new URLSearchParams();
|
|
params.set("url", source);
|
|
if (options.delivery === "proxy") params.set("proxy", "1");
|
|
if (width) params.set("w", String(width));
|
|
if (quality) params.set("q", String(quality));
|
|
if (format) params.set("f", format);
|
|
|
|
return `${IMAGE_PROXY_PATH}?${params.toString()}`;
|
|
}
|