perf(font): 轻量 GSUB probe 跳过 glyf 解析,CJK 子集化提速 17%~56%

fontSubset 原先对 probe 阶段也调用完整 Font.create(subset 模式),
它会解析子集 glyph 的 glyf 轮廓(parseSimpleGlyf)。但 probe 只需要
GSUB(原始字节透传)和 cmap(codepoint→gid),根本不需要轮廓。
实测 CJK 字体(思源黑体/令东齐伋)两次 Font.create 占总耗时 36%~63%。

新增 gsub-probe.ts:直接从字体字节解析表目录,提取 GSUB 字节切片 +
cmap 的 codepoint→gid 查找(format4 BMP + format12 补充平面二分,
与 fonteditor-core readWindowsAllCodes 优先级一致),完全跳过 glyf。

三种路径(与原 Font.create probe 行为严格等价):
- otf / ttf 无 GSUB:直接判无 reachable(otf 的 fonteditor probe 本就
  origGSUB=undefined),连 probe Font.create 也跳过
- ttf 有 GSUB 且 cmap 含 fmt4/12:轻量 probe 算 reachable
- ttf 有 GSUB 但 cmap 无 fmt4/12(极罕见):回退 Font.create probe

验证:
- seedGids / GSUB字节 / reachable 与原 probe 逐项等价(FiraCode 87 target 等)
- 5 个代表字体 woff2 输出与优化前逐字节一致(cmp 通过)
- pnpx tsx 基准测试.test.ts 全部 SSIM 无回归:
  思源黑体-千字文中段 7.8→4.5ms(-42%)、千字文前段 11.9→8.9ms(-25%)、
  otf-思源 2.9→1.9ms(-34%)、霞鹜文楷-汉字标点 5.0→2.2ms(-56%)、
  FiraCode 18.5→17.2ms,SSIM 全部不变

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-23 17:41:16 +08:00
parent 2da35e4257
commit 2c56d2cbb2
2 changed files with 262 additions and 19 deletions

View File

@ -3,6 +3,7 @@ import type { FontEditor } from "../../vendor/fonteditor-core/lib/ttf/font.js";
import { subsetGPOS } from "./gpos-subset.js";
import { subsetGSUB } from "./gsub-subset.js";
import { collectReachableGsubTargets } from "./gsub-reachable.js";
import { probeGsubAndCmap } from "./gsub-probe.js";
/** 优化291: TextEncoder 模块级单例 */
const textEncoder = new TextEncoder();
@ -128,30 +129,49 @@ export const fontSubset = (
): Uint8Array => {
const codePoints = textToCodePoints(subString);
/** GSUB target glyph GSUB subset codepoint
/** GSUB target glyph GSUB probe codepoint
* GSUB target glyph unicode FiraCode greater_equal.liga
* extraSubsetGids 使 target
* probe subset cmap codepoint
* GSUB GSUB reachable extraSubsetGids undefined */
*
* probe GSUB + cmap codepointgid
* Font.create glyf probe GSUB/cmapCJK Font.create
* 36%~63% probe GSUB ttf / otffonteditor probe
* origGSUB=undefined reachable probe Font.create GSUB cmap
* format4/12 subtable退 Font.create probe */
let extraSubsetGids: number[] | undefined;
const probeFont = Font.create(fontBuffer, {
type: option.sourceType,
subset: codePoints,
kerning: true,
});
const probeTtf = (probeFont as any).get();
const origGSUB = probeTtf.GSUB;
const origCmap = probeTtf.cmap;
if (origGSUB && origCmap) {
const gsubBytes = origGSUB instanceof Uint8Array ? origGSUB : new Uint8Array(origGSUB);
/** seed = 子集 codepoint 经 cmap 映射的原始 gid */
const seedGids = new Set<number>();
seedGids.add(0); /** .notdef */
const probe = probeGsubAndCmap(fontBuffer, codePoints, option.sourceType);
let probedSeedGids: Set<number> | undefined;
let probedGsubBytes: Uint8Array | undefined;
if (probe.ok) {
probedGsubBytes = probe.gsubBytes;
probedSeedGids = new Set<number>();
probedSeedGids.add(0); /** .notdef */
for (const cp of codePoints) {
const gid = origCmap[cp];
if (gid !== undefined) seedGids.add(gid);
const gid = probe.lookup.get(cp);
if (gid !== undefined) probedSeedGids.add(gid);
}
const reachable = collectReachableGsubTargets(gsubBytes, seedGids);
} else if (probe.needsFallback) {
/** 有 GSUB 但 cmap 无 format4/12回退 Font.create probe与原路径一致 */
const probeFont = Font.create(fontBuffer, {
type: option.sourceType,
subset: codePoints,
kerning: true,
});
const probeTtf = (probeFont as any).get();
const origGSUB = probeTtf.GSUB;
const origCmap = probeTtf.cmap;
if (origGSUB && origCmap) {
probedGsubBytes = origGSUB instanceof Uint8Array ? origGSUB : new Uint8Array(origGSUB);
probedSeedGids = new Set<number>();
probedSeedGids.add(0);
for (const cp of codePoints) {
const gid = origCmap[cp];
if (gid !== undefined) probedSeedGids.add(gid);
}
}
}
if (probedGsubBytes && probedSeedGids) {
const reachable = collectReachableGsubTargets(probedGsubBytes, probedSeedGids);
if (reachable.size > 0) extraSubsetGids = [...reachable];
}

View File

@ -0,0 +1,223 @@
/**
* GSUB probe GSUB + cmap codepointgid
* glyf parseSimpleGlyf collectReachableGsubTargets seed
*
* fontSubset probe Font.createsubset glyph
* glyf probe GSUB cmapcodepointgid
* CJK Font.create 36%~63% probe
*
* ttf/otf + cmap format 4BMP format 12
* FiraCode/CJK/ p3e1:fmt4 / p3e10:fmt12
* fonteditor-core readWindowsAllCodes/lookupFormat4/lookupFormat12
* seedGids probe reachable
*
* format 4/12 subtable format0/6 null退 Font.create
*
* @reference https://learn.microsoft.com/en-us/typography/opentype/spec/cmap
*/
/** cmap 查找结果:每个子集 codepoint 的原始 gid与 fonteditor-core cmap[cp] 语义一致) */
export interface CmapLookup {
/** codepoint → 原始 gid仅含能在 format4/12 查到的子集 codepoint */
get(cp: number): number | undefined;
}
/** ttf/otf 表目录条目 */
interface TableEntry {
offset: number;
length: number;
}
/**
* tag (offset, length)ttf otf
* otf CFF sfnt 0x00010000(ttf) / 'OTTO'(otf)
*/
function readTableEntry(dv: DataView, tag: string): TableEntry | null {
/** sfnt 头version(4) + numTables(2) + searchRange(2) + entrySelector(2) + rangeShift(2) = 12 字节 */
if (dv.byteLength < 12) return null;
const numTables = dv.getUint16(4, false);
if (numTables <= 0 || numTables > 100) return null;
let off = 12;
for (let i = 0; i < numTables; i++) {
const recOff = off + i * 16;
if (recOff + 16 > dv.byteLength) return null;
/** tag 是 4 字节 ASCII大端逐字节比较避免 String.fromCharCode 分配 */
const t0 = dv.getUint8(recOff);
const t1 = dv.getUint8(recOff + 1);
const t2 = dv.getUint8(recOff + 2);
const t3 = dv.getUint8(recOff + 3);
if (t0 === tag.charCodeAt(0) && t1 === tag.charCodeAt(1) && t2 === tag.charCodeAt(2) && t3 === tag.charCodeAt(3)) {
return { offset: dv.getUint32(recOff + 8, false), length: dv.getUint32(recOff + 12, false) };
}
}
return null;
}
/**
* format 4 segment gid lookupFormat4
* format4 subtable
* format(2) length(2) language(2) segCountX2(2) searchRange(2) entrySelector(2) rangeShift(2)
* endCode[segCount] reservedPad(2) startCode[segCount] idDelta[segCount] idRangeOffset[segCount] glyphIdArray[]
*/
function lookupFormat4(dv: DataView, subOff: number, unicode: number): number {
if (unicode > 0xFFFF) return -1;
const segCountX2 = dv.getUint16(subOff + 6, false);
const segCount = segCountX2 >>> 1;
/** endCode 起始 = subOff + 147 个 uint16 头字段) */
const endCodeBase = subOff + 14;
/** startCode 起始 = endCodeBase + segCount*2 + 2(reservedPad) */
const startCodeBase = endCodeBase + segCount * 2 + 2;
const idDeltaBase = startCodeBase + segCount * 2;
const idRangeOffsetBase = idDeltaBase + segCount * 2;
/** idRangeOffset 值是「相对 idRangeOffset[i] 字节位置」的偏移(指向 glyphIdArray 某项) */
const lo = 0;
let hi = segCount - 1;
/** 二分 endCode 找包含 unicode 的 segmentendCode 升序) */
let l = lo, h = hi;
while (l <= h) {
const mid = (l + h) >> 1;
const start = dv.getUint16(startCodeBase + mid * 2, false);
const end = dv.getUint16(endCodeBase + mid * 2, false);
if (unicode < start) {
h = mid - 1;
} else if (unicode > end) {
l = mid + 1;
} else {
const idDelta = dv.getInt16(idDeltaBase + mid * 2, false);
const idRangeOffset = dv.getUint16(idRangeOffsetBase + mid * 2, false);
if (idRangeOffset === 0) {
return ((unicode + idDelta) & 0xFFFF);
}
/** idRangeOffset[i] idRangeOffset[i] glyphIdArray
* = idRangeOffsetBase + mid*2 + idRangeOffset + (unicode - start)*2 */
const glyphOff = idRangeOffsetBase + mid * 2 + idRangeOffset + (unicode - start) * 2;
if (glyphOff + 2 > dv.byteLength) return 0;
const glyphId = dv.getUint16(glyphOff, false);
if (glyphId === 0) return 0;
return ((glyphId + idDelta) & 0xFFFF);
}
}
return -1;
}
/**
* format 12 group lookupFormat12
* format12 format(2) reserved(2) length(4) language(4) nGroups(4) groups[nGroups]
* group 12 startCharCode(4) endCharCode(4) startGlyphID(4)groups
*/
function lookupFormat12(dv: DataView, subOff: number, unicode: number): number {
const nGroups = dv.getUint32(subOff + 12, false);
const groupsBase = subOff + 16;
let l = 0, h = nGroups - 1;
while (l <= h) {
const mid = (l + h) >> 1;
const gOff = groupsBase + mid * 12;
const gStart = dv.getUint32(gOff, false);
const gEnd = dv.getUint32(gOff + 4, false);
if (unicode < gStart) {
h = mid - 1;
} else if (unicode > gEnd) {
l = mid + 1;
} else {
return dv.getUint32(gOff + 8, false) + (unicode - gStart);
}
}
return -1;
}
/**
* cmap format 12p3e10 format 4p3e1 subtable
* fonteditor-core readWindowsAllCodes format12 format4 BMP
* @returns { fmt4Off, fmt12Off } subtable -1
*/
function selectCmapSubtables(dv: DataView, cmapOff: number): { fmt4Off: number; fmt12Off: number } {
const numberSubtables = dv.getUint16(cmapOff + 2, false);
let fmt4Off = -1;
let fmt12Off = -1;
/** 子表目录项 8 字节platformID(2) encodingID(2) offset(4)offset 相对 cmap 表起始 */
let dirOff = cmapOff + 4;
for (let i = 0; i < numberSubtables; i++) {
const platformID = dv.getUint16(dirOff, false);
const encodingID = dv.getUint16(dirOff + 2, false);
const subRelOff = dv.getUint32(dirOff + 4, false);
const subOff = cmapOff + subRelOff;
if (subOff + 2 <= dv.byteLength) {
const format = dv.getUint16(subOff, false);
/** 优先 p3e1:fmt4 与 p3e10:fmt12与 readWindowsAllCodes 选择一致) */
if (format === 12 && platformID === 3 && encodingID === 10 && fmt12Off < 0) {
fmt12Off = subOff;
} else if (format === 4 && platformID === 3 && encodingID === 1 && fmt4Off < 0) {
fmt4Off = subOff;
}
}
dirOff += 8;
}
return { fmt4Off, fmt12Off };
}
/** probe result
* - { ok: true, gsubBytes, lookup } GSUB cmap reachable Font.create probe
* - { ok: false, needsFallback: false } reachableotf ttf GSUB Font.create probeextraSubsetGids=undefined
* - { ok: false, needsFallback: true } GSUB format4/12 cmap退 Font.create probe */
export type ProbeResult =
| { ok: true; gsubBytes: Uint8Array; lookup: CmapLookup }
| { ok: false; needsFallback: boolean };
/**
* probe GSUB codepointgid glyf
*
* @param fontBuffer
* @param codePoints codepoint cmap
* @param sourceType 'ttf'/'otf' otf fonteditor-core probe origGSUB=undefined
* otfttf GSUBreachable otf reachable
* otf otfttf probe
*/
export function probeGsubAndCmap(
fontBuffer: ArrayBuffer,
codePoints: number[],
sourceType: string,
): ProbeResult {
/** otf fonteditor-core probe origGSUB undefinedotfttf GSUB
* reachable extraSubsetGids=undefined GSUB */
if (sourceType === "otf") return { ok: false, needsFallback: false };
const dv = new DataView(fontBuffer);
const gsubEntry = readTableEntry(dv, "GSUB");
/** 无 GSUB 表 → 无 reachable与原 probe origGSUB=undefined 一致) */
if (gsubEntry === null) return { ok: false, needsFallback: false };
/** GSUB 字节切片offset/length 来自表目录,与 fonteditor-core 透传的字节一致) */
const gsubBytes = new Uint8Array(fontBuffer, gsubEntry.offset, gsubEntry.length);
const cmapEntry = readTableEntry(dv, "cmap");
if (cmapEntry === null) return { ok: false, needsFallback: true };
const { fmt4Off, fmt12Off } = selectCmapSubtables(dv, cmapEntry.offset);
/** 有 GSUB 但无 format4/12 cmap极罕见字体→ 无法轻量查找,回退 Font.create */
if (fmt4Off < 0 && fmt12Off < 0) return { ok: false, needsFallback: true };
/** codepoint gid readWindowsAllCodes subset
* format12 BMP format4 format12 */
const gidMap = new Map<number, number>();
for (const cp of codePoints) {
if (gidMap.has(cp)) continue;
let gid = -1;
/** BMP<0x10000且有 format4先查 format4与 readWindowsAllCodes 一致format12 存在时 BMP 仍先试 format4 */
if (cp < 0x10000 && fmt4Off >= 0) {
gid = lookupFormat4(dv, fmt4Off, cp);
}
/** format4 未命中(含补充平面)→ 查 format12 */
if (gid < 0 && fmt12Off >= 0) {
gid = lookupFormat12(dv, fmt12Off, cp);
}
if (gid >= 0) gidMap.set(cp, gid);
}
return {
ok: true,
gsubBytes,
lookup: {
get(cp: number): number | undefined {
const g = gidMap.get(cp);
return g === undefined ? undefined : g;
},
},
};
}