Files
dusuniot/src/utils/image-variant.ts
T

97 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 模块 Elementor 图片 WP 尺寸变体解析(仅服务端)
* 原站 image widget 的 image_size=medium/thumbnail/... 时 WP 渲染 -300x272 这类裁剪/缩放
* 变体文件;dump 的 image.url 只存原图。这里在服务端按 uploads 目录里实际存在的
* `<base>-<w>x<h>.<ext>` 变体择优改写 image.url(选宽度 ≤ 目标里最大的;无变体保持原图)。
*/
import { readdirSync } from "node:fs";
import { join, dirname } from "node:path";
/** # 常量 WP 尺寸名 → 目标宽度(Elementor 常用档位) **/
const SIZE_TARGET: Record<string, number> = {
thumbnail: 150,
medium: 300,
medium_large: 768,
large: 1024,
};
/** # 成员变量 目录列表缓存(同一目录只扫一次盘) **/
const dirCache = new Map<string, string[]>();
/** # 成员变量 变体解析结果缓存(url+size → 最终 url) **/
const resultCache = new Map<string, string>();
/** # 函数 对外:媒体 URL 的 WP 尺寸变体解析(服务端;无变体返回原 URL)
* 参数 url 原图 URL;targetWidth WP 档位目标宽(150/300/600/768/1024)
*/
export function resolveMediaVariantUrl(url: string, targetWidth: number): string {
if (typeof window !== "undefined" || !url?.includes("/uploads/")) return url;
return findVariant(url, targetWidth) ?? url;
}
/** # 函数 查找某原图在给定目标宽度下的最佳变体 URL(无则返回 null)
* 参数 url 原图 URL(本地 /uploads/... 或原站绝对地址);targetWidth WP 档位目标宽
*/
function findVariant(url: string, targetWidth: number): string | null {
// 归一化到本地 /uploads/ 路径(dump 里可能存原站绝对地址)
const up = url.indexOf("/uploads/");
if (up === -1) return null;
const localPath = url.slice(up); // /uploads/2022/06/Group-201.png
const m = localPath.match(/^(.*\/)([^/]+)\.(\w+)$/);
if (!m) return null;
const [, dir, base, ext] = m;
const fsDir = join("public", dir);
let files = dirCache.get(fsDir);
if (!files) {
try {
files = readdirSync(fsDir);
} catch {
files = [];
}
dirCache.set(fsDir, files);
}
// 候选:<base>-<w>x<h>.<ext>;取宽度 ≤ 目标里最大的(同宽取更矮的,WP medium 缩放优先于裁剪)
const prefix = `${base}-`;
let best: { w: number; h: number; file: string } | null = null;
for (const f of files) {
const fm = f.match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\d+)x(\\d+)\\.${ext}$`));
if (!fm) continue;
const w = Number(fm[1]);
const h = Number(fm[2]);
if (w > targetWidth) continue;
if (!best || w > best.w || (w === best.w && h < best.h)) best = { w, h, file: f };
}
return best ? `${dir}${best.file}` : null;
}
/** # 函数 就地解析文档内 image widget 的尺寸变体(服务端 loader 调用;浏览器端空操作)
* 参数 doc elementor 文档(数组或 {elements} 包装),image.url 被改写为变体地址
*/
export function resolveImageVariants(doc: unknown): void {
if (typeof window !== "undefined") return;
const walk = (el: unknown) => {
if (Array.isArray(el)) {
for (const x of el) walk(x);
return;
}
if (!el || typeof el !== "object") return;
const e = el as { id?: unknown; widgetType?: unknown; settings?: Record<string, unknown>; elements?: unknown[] };
if (e.widgetType === "image") {
const st = e.settings ?? {};
const target = SIZE_TARGET[String(st.image_size ?? "")];
const img = st.image as { url?: string } | undefined;
if (target && img?.url && img.url.includes("/uploads/")) {
const key = `${img.url}|${target}`;
let resolved = resultCache.get(key);
if (resolved === undefined) {
resolved = findVariant(img.url, target) ?? "";
resultCache.set(key, resolved);
}
if (resolved) img.url = resolved;
}
}
for (const c of Array.isArray(e.elements) ? e.elements : []) walk(c);
};
walk(doc);
}