fix: 修复 OTF→TTF 子集字体浏览器渲染空白 + 改进基准测试

- API 默认 outType 改为 ttf(兼容性最好)
- Cache-Control 从 immutable 改为 24h 缓存
- 基准测试改为 DOM 渲染 + puppeteer 截图 + pngjs 解码(更贴近真实浏览器)
- 增加 maxp 表验证(maxPoints/maxContours 为 0 直接报错)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-04-10 13:55:27 +08:00
parent af0ab38cec
commit 1e543d58ab
4 changed files with 26 additions and 23 deletions

View File

@ -314,12 +314,12 @@ async function handleFontSubset(req: Request, res: Response) {
};
}
/** LLRT 不支持 wasm默认 ttfNode.js 默认 woff2体积更小 */
/** 默认 ttf兼容性最好LLRT 不支持 wasm 只能用 ttf */
const isLlrt = release_name === "llrt";
const outTypeParam = params.get("outType") || "";
const outType = (outTypeParam === "woff2" || outTypeParam === "ttf")
? (isLlrt && outTypeParam === "woff2" ? "ttf" : outTypeParam)
: (isLlrt ? "ttf" : "woff2");
: "ttf";
const newFont = await fontSubset(oldFontBuffer, text, {
outType: outType,
@ -337,7 +337,7 @@ async function handleFontSubset(req: Request, res: Response) {
status: 200,
headers: {
"Content-Type": contentTypes[outType] || "font/ttf",
"Cache-Control": "public, max-age=31536000, immutable",
"Cache-Control": "public, max-age=86400",
},
}),
};

View File

@ -22,6 +22,7 @@
"@types/node": "^25.5.2",
"@xmldom/xmldom": "^0.9.9",
"jsdom": "^29.0.2",
"pngjs": "^7.0.0",
"puppeteer": "^24.40.0",
"skia-canvas": "^3.0.8",
"tsup": "^8.5.1",

9
pnpm-lock.yaml generated
View File

@ -24,6 +24,9 @@ importers:
jsdom:
specifier: ^29.0.2
version: 29.0.2
pngjs:
specifier: ^7.0.0
version: 7.0.0
puppeteer:
specifier: ^24.40.0
version: 24.40.0(typescript@6.0.2)
@ -1279,6 +1282,10 @@ packages:
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
pngjs@7.0.0:
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
engines: {node: '>=14.19.0'}
postcss-load-config@6.0.1:
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
engines: {node: '>= 18'}
@ -2790,6 +2797,8 @@ snapshots:
mlly: 1.8.2
pathe: 2.0.3
pngjs@7.0.0: {}
postcss-load-config@6.0.1(postcss@8.5.9):
dependencies:
lilconfig: 3.1.3

View File

@ -19,6 +19,7 @@ import { createServer, type Server, type IncomingMessage, type ServerResponse }
import { performance } from "node:perf_hooks";
import puppeteer, { type Page } from "puppeteer";
import { fontSubset } from "./backend/font_util/font.js";
import { PNG } from "pngjs";
const BENCHMARK_DIR = "benchmark_results";
const ROUNDS = 10;
@ -46,7 +47,7 @@ function createFontServer(): Promise<{ server: Server; port: number }> {
<style>
@font-face { font-family: "${fontFamily}"; src: url("/fonts/${fontFamily}") format("${fontFormat}"); }
body { margin: 0; background: white; }
#text { font-family: "${fontFamily}", sans-serif; font-size: ${fontSize}px; line-height: 1.2; color: black; padding: ${Math.ceil(fontSize * 0.1)}px 10px; display: inline-block; }
#text { font-family: "${fontFamily}", sans-serif; font-size: ${fontSize}px; line-height: 1.2; color: black; padding: ${Math.ceil(fontSize * 0.1)}px 10px; display: inline-block; white-space: nowrap; }
</style></head><body>
<div id="text">${text.replace(/</g, "&lt;")}</div>
<script>
@ -119,31 +120,23 @@ async function renderTextViaBrowser(
}
/**
* 使 DOM + canvas
* maxp
* DOM + puppeteer canvas
* pngjs SSIM
*/
const screenshot = await page.screenshot({ type: "png" });
const pixelData = await page.evaluate(() => {
const el = document.getElementById("text")!;
const rect = el.getBoundingClientRect();
/** 用 canvas 从 DOM 元素提取像素 */
const canvas = document.createElement("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(document.documentElement as any, 0, 0);
const imgData = ctx.getImageData(Math.round(rect.left), Math.round(rect.top), Math.round(rect.width), Math.round(rect.height));
let ink = 0;
for (let i = 0; i < imgData.data.length; i += 4) { if (imgData.data[i] < 128) ink++; }
return { pixels: Array.from(imgData.data), ink };
});
const screenshot = await page.screenshot({ type: "png", clip: { x: 0, y: 0, width, height } });
const png = PNG.sync.read(screenshot);
const pixels = new Uint8Array(png.data);
let inkPixels = 0;
for (let i = 0; i < pixels.length; i += 4) {
if (pixels[i] < 128) inkPixels++;
}
const inkPixels = pixelData.ink;
if (inkPixels === 0) {
throw new Error(`字体渲染无墨水像素 (${fontFamily}),字体可能未正确加载`);
}
return { pixels: new Uint8Array(pixelData.pixels), screenshot: Buffer.from(screenshot), inkPixels };
return { pixels, screenshot: Buffer.from(screenshot), inkPixels };
}
/** 计算两张图片的结构相似度(简化版 SSIM返回 0~1 */