6 Commits

Author SHA1 Message Date
崮生(子虚)
791ea0c89e perf(ot-writer): 主 Writer 按原表大小预分配容量消除 grow 全拷贝
OTWriter 加可选 initialCapacity 构造参数(默认 2048)。subsetGPOS/subsetGSUB
主 Writer 按原表 byteLength 预分配,避免大表多次 2× 扩容的全拷贝。
初夏纯标点 GPOS 单次 fontSubset 原 5 次 grow(cap 2048→4096→8192→16384),
改后 5 字体(初夏/FiraCode/令东千字文/思源/霞鹜)grow 全部 0。

12 字体 woff2 sha 逐字节一致,SSIM 全不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:54:07 +08:00
崮生(子虚)
852f0d2f21 perf(ot-bytes): OTWriter 初始容量 256→2048 + 内联容量检查消除热路径函数调用
subsetGPOS 思源 36→33μs、subsetGSUB 13→11μs;writeUint16 是序列化第一热点
(思源 207次/call),原 per-call ensure() 函数调用在 V8 class private method
不内联;内联为容量判断 + grow,并预分配 2048 避免小输出多次扩容拷贝
2026-07-24 14:40:05 +08:00
崮生(子虚)
ccc2e995c8 perf(gpos): ScriptList/FeatureList 整块拷贝跳过逐字段序列化
两表不含 glyphId(仅引用 lookup index),绝大多数字体其子表紧凑排列在
[listOff, listOff+span) 内、与下一表无物理交错,字节块本身即合法表。
新增 scriptListSpan/featureListSpan 计算跨度并校验自洽,满足则 subarray
整块拷贝,否则降级 serializeScriptList/serializeFeatureList。

收益:初夏 subsetGPOS 0.270→0.226ms,纯标点 0.240→0.165(-31%),
思源 0.071→0.036(-49%)。顺带保留 fontTools 的 FeatureTable 去重
(serialize 重排丢失致初夏 FeatureList 1840→3728B),输出 GPOS 字节
初夏 12064→10176(-16%)、思源 1194→978(-18%)。全基准 SSIM 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:16:21 +08:00
崮生(子虚)
51f969d3f0 perf(ot-bytes): OTWriter 改 Uint8Array 缓冲 + writeBytes 批量复制
OTWriter 原用 number[] + push 累积字节,每个 writeUint8/16 触发数字装箱与数组扩容;
gsub/gpos-subset 逐字节 writeUint8 循环复制 ScriptList/FeatureList/PairSet 字节块极慢。

重构 OTWriter 底层为 Uint8Array 容量缓冲 + size 指针:
- writeUint8/16 索引写入(无装箱),ensure 按 2× 扩容
- 新增 writeBytes(Uint8Array) 用 TypedArray.set 批量复制
- toUint8Array 返回 subarray 的拷贝(避免后续写入污染,subarray 语义曾有隐患)
gsub-subset/gpos-subset 的 5 处逐字节 writeUint8 复制改 writeBytes。

关键 Bug 修复:原 writeInt16 依赖 number[] 索引赋值到 length 位置【隐式扩展数组】,
Uint8Array 版需显式 ensure + 推进 size(否则 writeInt16 不增长 size,后续 write 覆盖,
GPOS 的 SinglePos 坐标值丢失——GPOS 输出从 1942B 错误缩到 1448B)。writeInt16At 仅覆盖
不扩展(pos 已在 reserveOffset16 的 ensure 保证范围内)。

验证:基准测试 29 用例 SSIM 全部无回归;GSUB/GPOS 独立输出 + fontSubset 完整输出
(思源千字文/标点/FiraCode/ttf)sha256 与重构前逐字节等价。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:39:04 +08:00
崮生(子虚)
28b6b1ebf4 perf(gsub): subsetGSUB 缓存+数组查找+裸读,FiraCode 983ms→30ms
FiraCode-代码用例(403 lookup)子集化从 983ms 降至 ~30ms,SSIM 全程保持 0.9923。

核心优化(CPU profile 逐层下钻):
- coverage 解析+重映射缓存(off→{gids,remapped,outOfSubset}):format3 的 coverage
  被多 subtable 引用,解析与重映射结果按 off 复用;readCoverageRemapped 边解析边过滤,
  format3 不再分配中间原 gid 数组
- OTReader.dv 公开:coverage 偏移来自已验证 subtable,热循环用裸 DataView.getUint16
  绕过 u16 的逐次边界检查
- GidLookup Int32Array(下标=原gid,值=新gid,-1=不在子集)替代 Map.get,coverage 过滤
  的高频查询从哈希退化为数组索引
- OTWriter.rollback 改 pop:patches 按 pos 单调追加,回退从末尾 pop 替代全量 filter
  (FiraCode 每次 rollback 392 次)
- gsub-reachable coverage 缓存:不动点迭代每轮重复解析同一 coverage,按 off 缓存

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:33:19 +08:00
崮生(子虚)
a84a4aa6f2 feat(gsub): GSUB/GPOS 布局表子集化 + FiraCode 连字修复
实现 GSUB/GPOS 的 gid 重映射子集化,解决 CJK 标点压缩丢失与连字不渲染问题。

核心改动:
- ot-bytes.ts: 共享 OTWriter/OTReader + ScriptList/FeatureList 紧凑重排
- gpos-subset.ts: SinglePos/PairPos coverage/ClassDef gid 重映射
- gsub-subset.ts: type1/2/3/4/6 coverage/ClassDef/替换目标 gid 重映射
- gsub-reachable.ts: GSUB 替换链不动点迭代,收集无 unicode 的连字 target 字形
- GSUB.js + support/ttfreader/ttfwriter: GSUB 字节透传读写接入
- parse.js: 修复 F2DOT14 复合字形解码(负数 +0.5|0 截断 bug)

关键 bug 修复:
- ChainContextSubst format2 字段错位:漏读 coverageOffset 致 chainSubClassSetCount
  读成 lookaheadClassDefOffset,连字核心规则降级为空。修复后 FiraCode <= 走对
  spacer+liga 路径。
- ScriptList/FeatureList 物理交错 + subtable 散落致 GSUB 膨胀损坏,改遍历重排。

SSIM: FiraCode-代码 0.9636 → 0.9923(与 pyftsubset 黄金像素级一致),
CJK 标点压缩全面恢复(初夏明朝纯标点 0.7950→0.9920 等)。

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