web-font/backend/routes/subset.ts
崮生(子虚) 168fc66544 feat(v1.9.0): OTF 子集化支持 + 法字笔画修复 + subsetCache 版本指纹
性能(vs v1.7.0 基线,基准测试全用例通过):
- 千字文 woff2 346ms → 9.8ms(35x)
- 8汉字 woff2 39.7ms → 2.8ms(14x)
- 千字文 ttf 6.0ms → 4.4ms
- 8汉字 ttf 2.7ms → 2.1ms

OTF 正确性:
- 修复 CFF.js readCFFIndexObject 漏读 off[idx+1] 导致子程序读成 0 字节,
  思源黑体「法」字三点水丢笔画的问题
- 修复非 subset 模式 cmap format12 崩溃(空数组误判为 subset)

缓存健壮性:
- subsetCache key 加版本指纹(package version + process.uptime),
  杜绝代码已修但缓存返回旧错误结果

基准测试:
- 新增白狐/思源 OTF 用例(含法海波等复杂笔画字)守护 OTF→TTF 转换
- 修复 calculateSSIM 的 width 推断 bug(sqrt 假设正方形)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:01:19 +08:00

101 lines
3.2 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.

import { createRequire } from "node:module";
import { fontSubset } from "../font_util/font";
import type { FontEditor } from "../../vendor/fonteditor-core/lib/ttf/font.js";
import { parseUrl, jsonResponse, stats, subsetCache, findFontPath, readFontBuffer } from "../shared";
/**
* 子集化版本指纹
*
* 纳入 subsetCache 的 key让旧缓存条目在以下两种场景自动失效无需手动清缓存
* - 生产:发版 bump package.json 的 version旧缓存自然过期语义版本失效
* - 开发pnpm dev 重启进程时进程启动时间戳变化,内存缓存整体重置
*
* 杜绝「子集化代码已修但缓存返回旧错误结果」的陷阱。
*/
const packageVersion: string = createRequire(import.meta.url)("../../package.json").version;
const SUBSET_CACHE_KEY = `${packageVersion}:${process.uptime()}`;
/** GET /api?font=...&text=... — 字体裁剪 */
export async function handleFontSubset(req: Request, res: Response) {
const url = parseUrl(req);
const params = new URLSearchParams(url.search);
const font = params.get("font") || "";
const text = params.get("text") || "";
if (text.length === 0) {
return { req, res };
}
const fontPath = await findFontPath(font);
if (!fontPath) {
return {
req,
res: new Response(`Font not found: ${font}`, {
status: 404,
headers: { "Content-Type": "text/plain; charset=utf-8" },
}),
};
}
/** 默认 ttf兼容性最好 */
const outTypeParam = params.get("outType") || "";
const outType = (outTypeParam === "woff2" || outTypeParam === "ttf") ? outTypeParam : "ttf";
/** 查询裁剪结果缓存 */
/** 版本指纹纳入 key代码变更后旧缓存自动失效 */
const cacheKey = `${SUBSET_CACHE_KEY}:${fontPath}:${outType}:${text}`;
stats.subsetRequests++;
stats.totalChars += text.length;
const cached = subsetCache.get(cacheKey);
if (cached) {
stats.subsetCacheHits++;
const contentTypes: Record<string, string> = { ttf: "font/ttf", woff2: "font/woff2" };
return {
req,
res: new Response(cached, {
status: 200,
headers: {
"Content-Type": contentTypes[outType] || "font/ttf",
"Cache-Control": "public, max-age=86400",
"X-Cache": "HIT",
},
}),
};
}
const fontType = fontPath.split(".").pop() as FontEditor.FontType;
let oldFontBuffer: ArrayBuffer;
try {
oldFontBuffer = await readFontBuffer(fontPath);
} catch {
return {
req,
res: new Response(`Font read error: ${font}`, {
status: 500,
headers: { "Content-Type": "text/plain; charset=utf-8" },
}),
};
}
const newFont = await fontSubset(oldFontBuffer, text, {
outType: outType,
sourceType: fontType,
});
/** 写入裁剪结果缓存 */
subsetCache.set(cacheKey, newFont as ArrayBuffer);
const contentTypes: Record<string, string> = { ttf: "font/ttf", woff2: "font/woff2" };
return {
req,
res: new Response(newFont, {
status: 200,
headers: {
"Content-Type": contentTypes[outType] || "font/ttf",
"Cache-Control": "public, max-age=86400",
"X-Cache": "MISS",
},
}),
};
}