From e8ae79a58c678026c62caa4758af08c1a54544ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Fri, 24 Jul 2026 23:24:18 +0800 Subject: [PATCH] =?UTF-8?q?perf(cff):=20Local=20Subr=20INDEX=20=E6=94=B9?= =?UTF-8?q?=20indexByteRange=20=E4=B8=8D=E5=85=A8=E9=87=8F=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=20offset=EF=BC=8CsubsetCFF=20-46%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit subsetCFF 0.313ms 中 0.202ms 集中在 fdaParse 段:readIndex(b, privOrigOff+subrRel) 全量解析 Local Subr INDEX 的 count+1 个 offset 数组,但后续只用 b.subarray(start,end) 取整体字节切片透传——offset 数组从未被消费。思源等 CID 字体 Local Subr 可达数千 subr, 全量解析是 subsetCFF 主要耗时。 新增 indexByteRange:仅读 count/offSize + 第 count 个 offset(位于 offset 数组末尾)算 end,跳过中间全部 offset 解析。Local Subr 透传改用它。 顺带合并 FDSelect 两处 lookupFDSelect 遍历为单次(首次遍历同时记录 gidOrigFds 供 newGidToFd 复用,消除第二次对 newSubsetGids 的二分查找)。 subsetOTF(思源8字) keepGSUB=true 0.704→0.504ms(-28%)、keepGSUB=false 0.428→0.232ms(-46%); fdaParse 0.202→0.003ms;字节级零回归(cmp 输出逐字节一致); 基准测试全字体 SSIM 与优化前完全一致。 Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/font_util/cff-subset.ts | 40 ++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/backend/font_util/cff-subset.ts b/backend/font_util/cff-subset.ts index fdccfa8..b61f31e 100644 --- a/backend/font_util/cff-subset.ts +++ b/backend/font_util/cff-subset.ts @@ -59,6 +59,23 @@ function readIndex(b: Uint8Array, pos: number): CffIndex { function getIndexBytes(b: Uint8Array, idx: CffIndex): Uint8Array { return b.subarray(idx.start, idx.end); } +/** 只取 INDEX 的字节范围 [start, end),不解析中间 offset 数组。 + * Local/Global Subr INDEX 透传时只需整体字节切片,全量解析 count+1 个 offset 是纯浪费 + * (思源等大字体的 Local Subr 可达数千 subr,readIndex 全量解析占 subsetCFF 主要耗时)。 + * end = dataStart + offsets[count] - 1,仅读第 count 个 offset(位于 pos+3+count*offSize)即可。 */ +function indexByteRange(b: Uint8Array, pos: number): { start: number; end: number } { + const count = (b[pos] << 8) | b[pos + 1]; + /** count=0 的 INDEX 仅 2 字节 */ + if (count === 0) return { start: pos, end: pos + 2 }; + const offSize = b[pos + 2]; + /** dataStart = pos + 3(count+offSize 头)+ (count+1)*offSize(offset 数组) */ + const dataStart = pos + 3 + (count + 1) * offSize; + /** 第 count 个 offset 位于 offset 数组末尾(pos+3 + count*offSize),读 offSize 字节大端 */ + let lastOff = 0; + const lp = pos + 3 + count * offSize; + for (let j = 0; j < offSize; j++) lastOff = (lastOff << 8) | b[lp + j]; + return { start: pos, end: dataStart + lastOff - 1 }; +} /** CFF Top/Private DICT 解析结果:操作码键 → 操作数数组。双字节操作码 12,n 存为 (12<<8)|n */ type CffDict = Map; @@ -358,13 +375,16 @@ export function subsetCFF(cffBytes: Uint8Array, subsetGids: number[]): Uint8Arra } const newCharset = encodeCharsetFormat0(newCharsetBody); - /** FDSelect:按需查每个 subsetGid 的原 FD(lookupFDSelect 二分 range),不全量展开 numGlyphs。 */ - - /** 收集命中的 FD(保持首次出现顺序),构建 原FD→新FD 映射 */ + /** FDSelect:按需查每个 subsetGid 的原 FD(lookupFDSelect 二分 range),不全量展开 numGlyphs。 + * 单次遍历同时构建:原FD→新FD 映射(fdRemap/usedFds)+ 每 gid 的原 FD 数组(gidOrigFds), + * 后者供 newGidToFd 直接复用,避免对 newSubsetGids 第二次 lookupFDSelect 遍历。 */ const fdRemap = new Map(); const usedFds: number[] = []; - for (const gid of newSubsetGids) { + const gidOrigFds: number[] = new Array(newSubsetNumGlyphs); + for (let i = 0; i < newSubsetNumGlyphs; i++) { + const gid = newSubsetGids[i]; const fd = lookupFDSelect(b, fdSelectOff, gid); + gidOrigFds[i] = fd; if (!fdRemap.has(fd)) { fdRemap.set(fd, usedFds.length); usedFds.push(fd); @@ -411,8 +431,9 @@ export function subsetCFF(cffBytes: Uint8Array, subsetGids: number[]): Uint8Arra let localSubr: Uint8Array | null = null; if (subrRel !== undefined) { /** Local Subr INDEX 紧接 Private DICT 字节之后(绝对偏移 = privOrigOff + subrRel) */ - const subrIdx = readIndex(b, privOrigOff + subrRel); - localSubr = b.subarray(subrIdx.start, subrIdx.end); + /** Local Subr INDEX 仅需整体字节切片透传,不全量解析 offset(思源 Local Subr 可达数千 subr) */ + const subrRange = indexByteRange(b, privOrigOff + subrRel); + localSubr = b.subarray(subrRange.start, subrRange.end); } info = { origOff: privOrigOff, len: privLen, localSubr }; privSegCache.set(privOrigOff, info); @@ -420,12 +441,11 @@ export function subsetCFF(cffBytes: Uint8Array, subsetGids: number[]): Uint8Arra fdInfos.push({ dictBytes, priv: info }); } - /** 新 FDSelect:每个新 gid → 新 FD 编号。单 FD 用格式 0(最省,1+numGlyphs 字节); - * 多 FD 用格式 3 ranges(与原始 CID 字体一致,兼容 OTS 严格校验)。 */ + /** 新 FDSelect:每个新 gid → 新 FD 编号(复用首次遍历的 gidOrigFds,无需第二次 lookupFDSelect)。 + * 单 FD 用格式 0(最省,1+numGlyphs 字节);多 FD 用格式 3 ranges(与原始 CID 字体一致,兼容 OTS 严格校验)。 */ const newGidToFd: number[] = new Array(newSubsetNumGlyphs); for (let i = 0; i < newSubsetNumGlyphs; i++) { - const origFd = lookupFDSelect(b, fdSelectOff, newSubsetGids[i]); - newGidToFd[i] = fdRemap.get(origFd) ?? 0; + newGidToFd[i] = fdRemap.get(gidOrigFds[i]) ?? 0; } const newFdSelectBody = encodeFDSelect(newGidToFd);