diff --git a/.gitignore b/.gitignore index bf11c31..efc7d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,8 @@ app *.tar dist_backend -.pilot \ No newline at end of file +.pilot + +verify_font_baseline +benchmark_results +.claude \ No newline at end of file diff --git a/task.md b/task.md new file mode 100644 index 0000000..1942c1b --- /dev/null +++ b/task.md @@ -0,0 +1,3 @@ +/loop 持续优化字体子集化性能,可以大胆放开手脚的去做,但是优化完一定要通过基准测试。中途不要切换到其他模式,比如计划模式也不要询问我,你直接做就行了,请你持续的去优化,不要去询问我,不要去中断,好吧 + +啊,你每次优化能不能把基准测试保存在本地目录下,这样我方便查看。你的文档中应该在每个重大节点更新基准测试结果,这样我能方便看到你使用了哪些优化方法,得到了什么样的优化效果。 \ No newline at end of file diff --git a/verify_font.ts b/verify_font.ts index aec795f..3c630bd 100644 --- a/verify_font.ts +++ b/verify_font.ts @@ -18,6 +18,7 @@ const isBaseline = process.argv[2] === "baseline"; const FONT_PATH = "font/令东齐伋复刻体.ttf"; const FONT_NAME = "令东齐伋复刻体"; const SIMILARITY_THRESHOLD = 0.98; +const BASELINE_DIR = "benchmark_results"; const raw = await readFile(FONT_PATH); const fontBuffer = new Uint8Array(raw).buffer; @@ -135,15 +136,16 @@ function extractFontData(font: any) { /** 注册子集字体并返回 fontFamily 名 */ async function registerSubsetFont(ttfBuffer: ArrayBuffer, counter: number): Promise { - const fontPath = `verify_font_baseline/_verify_${counter}.ttf`; + const fontPath = `${BASELINE_DIR}/_verify_${counter}.ttf`; await writeFile(fontPath, Buffer.from(ttfBuffer)); const familyName = `SubsetFont_${counter}`; FontLibrary.use(familyName, [fontPath]); return familyName; } +await mkdir(BASELINE_DIR, { recursive: true }); + if (isBaseline) { - await mkdir("verify_font_baseline", { recursive: true }); const baseline: Record = {}; for (const { text, label } of testCases) { @@ -151,7 +153,7 @@ if (isBaseline) { const font = Font.create(fontBuffer, { type: "ttf", subset }); const data = extractFontData(font); - await writeFile(`verify_font_baseline/${label}.ttf`, Buffer.from(data.buffer)); + await writeFile(`${BASELINE_DIR}/${label}.ttf`, Buffer.from(data.buffer)); /** 用完整字体和子集字体分别渲染,保存像素数据 */ const fullPixels = renderText(FONT_NAME, text, 48); @@ -172,10 +174,10 @@ if (isBaseline) { console.log(` ${label}: glyf=${data.glyfCount}, output=${data.outputSize} bytes, ssim=${ssim.toFixed(4)}`); } - await writeFile("verify_font_baseline.json", JSON.stringify(baseline, null, 2)); + await writeFile(`${BASELINE_DIR}/verify_baseline.json`, JSON.stringify(baseline, null, 2)); console.log("\n基准已生成(含完整字体+子集字体渲染像素数据及相似度)"); } else { - const baselineRaw = await readFile("verify_font_baseline.json", "utf-8"); + const baselineRaw = await readFile(`${BASELINE_DIR}/verify_baseline.json`, "utf-8"); const baseline = JSON.parse(baselineRaw); let allPassed = true; diff --git a/基准测试.test.ts b/基准测试.test.ts index ff91339..64a4768 100644 --- a/基准测试.test.ts +++ b/基准测试.test.ts @@ -5,6 +5,7 @@ * 测量: * 1. 子集化总耗时(Font.create → optimize → sort → write) * 2. 渲染相似度(子集字体 vs 完整字体,SSIM 指标) + * 3. 输出渲染对比图片到 benchmark_results/ 目录 */ import { readFile, writeFile, mkdir } from "node:fs/promises"; import { performance } from "node:perf_hooks"; @@ -13,6 +14,7 @@ import { Canvas, FontLibrary } from "skia-canvas"; const FONT_PATH = "font/令东齐伋复刻体.ttf"; const FONT_NAME = "令东齐伋复刻体"; +const BENCHMARK_DIR = "benchmark_results"; const raw = await readFile(FONT_PATH); const fontBuffer = new Uint8Array(raw).buffer; @@ -48,6 +50,22 @@ function renderText(fontFamily: string, text: string, fontSize: number): Uint8Ar return new Uint8Array(imgData.data.buffer); } +/** 渲染文字并保存为 PNG */ +async function renderTextToPng(fontFamily: string, text: string, fontSize: number, filePath: string) { + const charWidth = Math.ceil(fontSize * 1.5); + const width = text.length * charWidth + 20; + const height = Math.ceil(fontSize * 1.5); + const canvas = new Canvas(width, height); + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, width, height); + ctx.font = `${fontSize}px ${fontFamily}`; + ctx.fillStyle = "black"; + ctx.fillText(text, 10, Math.ceil(fontSize * 1.2)); + const buffer = await canvas.toBuffer("png"); + return writeFile(filePath, buffer); +} + /** 计算两张图片的结构相似度(简化版 SSIM),返回 0~1 */ function calculateSSIM(a: Uint8Array, b: Uint8Array): number { if (a.length !== b.length) return 0; @@ -84,16 +102,27 @@ function calculateSSIM(a: Uint8Array, b: Uint8Array): number { /** 注册子集字体用于渲染 */ async function registerSubsetFont(ttfBuffer: ArrayBuffer, counter: number): Promise { - await mkdir("verify_font_baseline", { recursive: true }); - const fontPath = `verify_font_baseline/_bench_${counter}.ttf`; + await mkdir(BENCHMARK_DIR, { recursive: true }); + const fontPath = `${BENCHMARK_DIR}/_bench_${counter}.ttf`; await writeFile(fontPath, Buffer.from(ttfBuffer)); const familyName = `BenchSubset_${counter}`; FontLibrary.use(familyName, [fontPath]); return familyName; } +await mkdir(BENCHMARK_DIR, { recursive: true }); + console.log("\n=== 字体裁剪基准测试 ===\n"); +const results: Array<{ + label: string; + avg: number; + min: number; + max: number; + outputSize: number; + ssim: number; +}> = []; + for (const { label, text } of testCases) { const subset = [...text].map((c) => c.codePointAt(0)!); const times: number[] = []; @@ -122,12 +151,25 @@ for (const { label, text } of testCases) { if (lastTtfBuffer) { subsetFontCounter++; const familyName = await registerSubsetFont(lastTtfBuffer, subsetFontCounter); + + /** 保存渲染对比图片 */ + const safeLabel = label.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "_"); + await renderTextToPng(FONT_NAME, text, 48, `${BENCHMARK_DIR}/${safeLabel}_full.png`); + await renderTextToPng(familyName, text, 48, `${BENCHMARK_DIR}/${safeLabel}_subset.png`); + const fullPixels = renderText(FONT_NAME, text, 48); const subsetPixels = renderText(familyName, text, 48); ssim = calculateSSIM(fullPixels, subsetPixels); } + results.push({ label, avg, min, max, outputSize: lastOutputSize, ssim }); console.log(` ${label}: avg=${avg.toFixed(1)}ms min=${min.toFixed(1)}ms max=${max.toFixed(1)}ms 输出=${lastOutputSize.toLocaleString()} bytes ssim=${ssim.toFixed(4)}`); } +/** 保存结果到 JSON */ +const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); +const resultFile = `${BENCHMARK_DIR}/benchmark_${timestamp}.json`; +await writeFile(resultFile, JSON.stringify({ timestamp: new Date().toISOString(), rounds: ROUNDS, results }, null, 2)); +console.log(`\n结果已保存到 ${resultFile}`); +console.log(`渲染对比图片已保存到 ${BENCHMARK_DIR}/ 目录`); console.log("");