86 Commits

Author SHA1 Message Date
崮生(子虚)
f1c8315b0c perf(gsub-reachable): 大 coverage 反转路径用字节二分省掉数组展开
collectReachableGsubTargets 的 SingleSubst 反转遍历路径(reachable 小、coverage 大)
原 readCoverageGids 先把 coverage 全部 gid 展开到数组再二分。思源 locl lookup 的 coverage
达 8881/12000/11632 gid,展开是 reach 阶段 #1 热点(0.060ms / 0.165ms)。

新增 coverageIndexOf(format1 二分 / format2 range 二分+累计)与 coverageCount(不展开得
gid 总数),反转路径直接对 coverage 字节二分得 index,完全省掉大数组展开:
- 思源 collectReachable 0.165→0.042ms(-74%)
- 白狐 0.026→0.007ms(-73%)
- FiraCode 持平(coverage 小不走反转路径)

reachable 结果逐字体一致(思源 19 / 白狐 4 / FiraCode 89),基准测试全过:
思源 OTF ssim=1.0000、FiraCode 0.9923 保持。

otf-subset 顺带 findTable 去重(reachable 收集与布局表子集化共用提前解析的 GSUB/GPOS)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 00:27:56 +08:00
崮生(子虚)
07432852cf fix(otf-subset): 保留 GSUB locl 变体 target 字形,思源 otf SSIM 0.9530→1.0000
otf 子集化路径此前完全跳过 probeGsubAndCmap/collectReachableGsubTargets,
subsetGids 仅含 cmap 查到的 codepoint→基础 gid。思源黑体等 CID 字体含 locl
feature,浏览器按中文 locale 渲染时 GSUB 把基础字形替换为地区变体(如
"天" gid A → CN 变体 gid A+1)。subsetGids 不含这些 target,子集 GSUB 重
映射后 target gid 指向不存在/错误字形,渲染出错误变体。

逐字 ink 对比定位(地/黄/洪/波一致,天/玄/宇/法/海多 ink、宙少 ink)正是
locl 变体丢失特征。subsetOTF 内部复用 collectReachableGsubTargets 收集 seed
经 GSUB 可达的 target gid 合并进 subsetGids,subsetCFF/FDSelect/charset 与
GSUB/GPOS 重映射统一基于扩展后的 subsetGids,变体字形正确保留。

实测思源 9 字 otf/woff2 输出均 ink 6724/6724 SSIM 1.0000(修复前 0.9530),
白狐 5 字保持 1.0000,其余用例无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:50:37 +08:00
崮生(子虚)
94e75ea2ba perf(cff): Name/String/Global Subr INDEX 改 indexByteRange,删 getIndexBytes
与 Local Subr 同理:Name/String/Global Subr INDEX 仅需字节范围透传 + end
(下一 INDEX 起始),readIndex 全量解析 count+1 个 offset 是浪费。统一改用
indexByteRange,删除仅服务于这三个的 getIndexBytes。

思源场景 offset 数组不大故实测持平(subsetOTF min 0.49ms 不变),但消除潜在
全量解析浪费,与 Local Subr 处理一致;白狐 5字 min 0.052ms。
字节级零回归(cmp 输出逐字节一致);基准测试全字体 SSIM 与优化前完全一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:28:37 +08:00
崮生(子虚)
e8ae79a58c perf(cff): Local Subr INDEX 改 indexByteRange 不全量解析 offset,subsetCFF -46%
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) <noreply@anthropic.com>
2026-07-24 23:24:18 +08:00
崮生(子虚)
7cd960617a perf(otf): calcTableChecksum 改 DataView.getUint32 批量读,assemble -83%
原逐字节 bytes[i]<<24|bytes[i+1]<<16|... 手工拼装 uint32,对思源 742KB CFF
表是 assembleSfnt 主要开销(assembleSfnt min 0.643ms 中绝大部分)。

改 DataView.getUint32 一次读 4 字节大端 uint32(V8 对 DataView 专门内联,
[[dataview-getuint32-fastest]] 验证其快于 Uint32Array+swap 与手工位拼装),
尾部 1~3 字节补 0 单独处理保持规范语义。

assembleSfnt(思源8字): min 0.643→0.110ms(-83%);
subsetOTF: min 1.321→0.725ms(-45%)、p50 1.720→1.079ms(-37%);
(叠加上一 commit wholeSum 复用,相对原版 min 1.836→0.725 累计 -60%)
字节级零回归(cmp 优化前后输出逐字节一致,含 checksum/checkSumAdjustment);
基准测试全字体 SSIM 与优化前完全一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:18:37 +08:00
崮生(子虚)
98b56b4f35 perf(otf): assembleSfnt wholeSum 复用各表 checksum,消除全文件二次遍历
head.checkSumAdjustment 原先两遍遍历 ~750KB 输出:
1. calcTableChecksum 对每表(CFF 742KB 最大)算 checksum
2. wholeSum 再次遍历整个 out 文件 4 字节步进算全文件 sum

数学等价:全文件 sum = 目录区 sum + Σ 各表数据段 checksum,
而各表数据段 padded sum 正是写表记录时已算的 calcTableChecksum。
故 wholeSum 改为「dirSize(156B) 目录区 sum + 累加的 tablesDataSum」,
消除数百 KB 大表的第二次整段遍历。

思源 otf 8字 subsetOTF:min 1.836→1.321ms(-28%)、p50 2.433→1.720ms(-29%);
字节级零回归(cmp 优化前后输出逐字节一致,含 checkSumAdjustment);
基准测试全字体 SSIM 与优化前完全一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:12:05 +08:00
崮生(子虚)
e37593a552 perf(cff-subset): 三处全量解析改按需查询,思源 subsetCFF -77%
CID CFF 子集化原先对 numGlyphs(思源 65535) 全量解析三处,子集仅 11 字时是纯浪费:
1. readIndex(CharStrings) 遍历 65536 个 offset(0.435ms)→ 直接读 count+offSize,
   按 subsetGid 随机读 2 个 offset 取字节区间。
2. readCharsetCIDs 填充 65535 CID 数组 → lookupCharsetCID 遍历 range 查单个 gid
   (思源 format2 约 6 千 range,省去 65535 项填充;range 起始 gid 累积计算)。
3. parseFDSelect 填充 65535 FD 数组 → lookupFDSelect format3 二分 range 查单个 gid。

subsetCFF 1.577→0.357ms(-77%);subsetOTF 思源 3.56→1.89ms(-47%),基准 avg 5.5→3.7ms。
字节级零回归(思源/白狐子集字节与优化前 cmp 完全一致),SSIM 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:05:54 +08:00
崮生(子虚)
67439be486 fix(cff-subset): replaceDictOffsets 补全 op30(BCD) 跳过分支
CFF DICT 的 op30 是 BCD 实数,每字节两 nibble 遇 0xf 结束。replaceDictOffsets 原仅处理
28/29/247-254 整数编码,漏 op30,遇到 BCD 字节会逐字节误扫。当前思源 Top DICT 的
CIDFontVersion(BCD 2.004) 后紧跟非替换目标 operator 且 BCD 字节均 >21,operator 边界
未被破坏故输出恰好正确,但若 BCD 内出现 <=21 字节会致 operator 错乱——潜在 bug。

补 op30 分支完整跳过 nibbles 至 0xf。基准测试全用例 SSIM 无变化(思源 otf 0.9530、白狐 1.0)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:00:51 +08:00
崮生(子虚)
bebb8503f4 feat(otf): 独立 OTF(CFF) 子集化器,修复思源等 CID 字体 OTS 拒绝与 gid 错位
fonteditor-core 对含 idRangeOffset 的 CID cmap 解析有 bug,产出 gid 错乱子集(SSIM 0.93~0.97)。
新增独立 otf-subset + cff-subset,直接重建 CFF/cmap/hmtx,透传 charstring。

核心修复:
1. cff-subset 透传各 FD 的 Local Subr INDEX(op 19 指向)+ patch op19 新偏移。
   原仅透传 Private DICT 字节未带 Local Subr INDEX,思源等字形用 callsubr 引用本地 subr,
   子集 op19 指向越界致 OTS "Failed to parse Top DICT Data" 拒绝加载。
2. otf-subset 接入 subsetGSUB/subsetGPOS 按 subsetGids 做 gid 重映射。
   原透传原始 GSUB/GPOS 字节 gid 未重映射,coverage 引用旧 gid 指向错字,
   致 shaping 把字符替换成 .notdef(思源 otf SSIM 0.623,洪/法等字变空白)。
   OTF 与 TTF 的 GSUB/GPOS 表结构相同,字节级子集化器可直接复用。

结果:思源 otf OTS 拒绝→可加载,SSIM 0.623→0.953,白狐 otf SSIM 1.0,
逐字 charstring 字节级与原始一致,渲染像素级一致(5 种独立验证 diff=0)。
所有非 otf 用例无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 22:48:14 +08:00
崮生(子虚)
ce893976f1 perf(cmap): subset 模式复用 probe 的 codepoint→gid 映射跳过二分查找
Font.create 内 readWindowsAllCodes 在 subset 模式下对每个 subset 字符做
lookupFormat4/lookupFormat12 二分查找产出 codepoint→gid,这与之前 probeGsubAndCmap
的 probe.lookup(算法一致)完全重复。subset 模式下 format0/format14 不解析
(glyphIdArray/groups 为空),codes 仅来自 format4/12,故 probe.lookup 与
readWindowsAllCodes 产出严格等价。

fontSubset 在 probe.ok 时将 probe.lookup(Map)转为普通对象经 readOptions.presetCmap
注入,readWindowsAllCodes 检测到 presetCmap 直接返回,跳过 N 次(subset 长度)二分查找。

收益(best-of Font.create):千字文 -10.2%、初夏纯标点 -5.5%、FiraCode -3.7%
(与 subset 长度成正比,每 codepoint 省一次二分)。5 字体 woff2 sha 逐字节一致,
SSIM 全部不变。probe 不 ok(otf 或无 format4/12)时不注入,走原逻辑。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 20:25:03 +08:00
崮生(子虚)
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
崮生(子虚)
4ae7e05334 perf(gsub-subset): emitCoverage 合并两遍区间扫描为单遍
原实现两遍扫描连续区间:第一遍统计 rangeCount 决定 format1/format2,
第二遍重新扫描写出 ranges。合并为单遍收集 rangeStarts/rangeEnds 两个并行
number[](替代对象数组),再据 rangeCount vs n 选 format 直接写出。

省第二遍重新扫描 + covIndex 重算。V8 短命小数组近乎免费。

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:48:40 +08:00
崮生(子虚)
77029f1090 perf(gsub-reachable): collectChainRuleRefs 循环内 r.u16 改 dv.getUint16
chain format1 规则的 backtrack/input/lookahead/substRecords 读取(6 个内层
循环)从 r.u16 改 dv.getUint16。FiraCode 180 个 fmt1 规则 × 多次密集调用。
count 读取(backtrackCount 等)保留 r.u16 维持原行为。

FiraCode reachable 集合 size=89 不变,5 字体 sha 逐字节一致,SSIM 全不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:44:08 +08:00
崮生(子虚)
7719110b32 perf(gsub-subset): subsetGSUB 主函数 header+lookup 解析 r.u16 改 dv.getUint16
subsetGSUB 顶部本就构造了 dv(gsubBytes 的 DataView),header offset
(0/2/4/6/8) 与 lookup 解析循环的 r.u16 改 dv.getUint16 省方法调用+边界检查。
extension 的 r.u32 保留(越界降级安全网)。

8 字体(FiraCode woff2+ttf、CJK 标点、千字文)sha 逐字节一致,SSIM 全不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:39:54 +08:00
崮生(子虚)
bd68995fde perf(gpos-subset): 循环内 r.u16 改 dv.getUint16
延续 gsub-reachable/gsub-subset 的已验证模式:subsetGPOS 主函数、
readCoverageGids、readClassDefMap、serializePairPosFormat1 的循环内
连续 u16 读取从 r.u16(方法调用+边界检查)改为 dv.getUint16(off,false)。
函数级 header offset 与 errorFlag 安全网保留 r.u16/scriptListSpan。

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:36:31 +08:00
崮生(子虚)
17145e05fd perf(gsub-subset): serialize/parse 函数循环内 r.u16 改 dv.getUint16
延续 gsub-reachable 的 dv 缓存优化到 subsetGSUB 的序列化/解析函数:
serializeSingleSubst/Multiple/Alternate/Ligature、serializeChainedContextSubst
format1/format2、parseChainRuleFormat1or2、parseChainFormat3、readClassDefMap
顶部缓存 const dv = r.dv,循环内 gid/offset 连续读取改 dv.getUint16。
函数级 format/count 判定保留 r.u16 维持 errorFlag 安全网。

输出字节级等价(6 字体 sha 全一致),SSIM 全不变。
性能:初夏标点 -5.3%、霞鹜标点 -5.0%、FiraCode -2.8%(CJK 标点 GSUB 密集收益大)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:32:23 +08:00
崮生(子虚)
92d02c33bb perf(gsub-reachable): 循环内 r.u16 改 dv.getUint16 + coverage 批量读
精确 CPU profile(Session+Profiler API 排除模块加载)发现 GSUB 重字体
(FiraCode)brotli 不再主导,collectReachableGsubTargets 是 #1 热点,
其中 OTReader.u16(方法调用+边界检查)占 134Kμs。

collectReachableGsubTargets/collectSubtableTargets/collectChainRefs/
readCoverageGids/coverageFirstExcludedGid 顶部缓存 const dv = r.dv,
循环内连续 u16 读取改 dv.getUint16(off,false),省方法调用+边界检查。
函数级 format/count 判定保留 r.u16 维持 errorFlag 安全网。
readCoverageGids format1 加 Uint16Array 批量读(count>8 且对齐)。

collectReachable FiraCode 0.1525→0.1199ms(-21%),e2e 1.04→1.00ms。
基准测试全绿 SSIM 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:28:56 +08:00
崮生(子虚)
fb721a216b perf(gpos): 预扫描 coverage 未命中的 subtable 直接写空跳过 serializeSinglePos/PairPos
anyHit lookup 内逐 subtable 序列化时,subtableCoverageHits 预扫描已判定
coverage 是否含子集字形。初夏 96~98% SinglePos subtable coverage 完全不含
子集字形(hits[j]==false),原实现仍 readCoverageGids + 构建 kept(空) +
emit 空 coverage 走完整 serializeSinglePos,纯浪费。

coverageHit=false 时直接 writeEmptyPosSubtable(与 lookup 全空折叠同性质
等价变换:空 coverage 浏览器查不到字形即跳过,valueFormat 0 与原值渲染等价),
跳过 readCoverageGids/kept/emit 全部开销。

A/B(min 微基准,checksum 因空 subtable valueFormat 0 替代原值变化,SSIM 全不变):
- 初夏纯标点 subsetGPOS 0.110→0.084ms (-24%)
- 初夏汉字标点 0.101→0.073ms (-28%)
- FiraCode 0.002ms(无 GPOS 热点,符合预期)

全量基准 SSIM 全部不变(初夏纯标点 0.9920/汉字标点 1.0000/霞鹜 0.9987/FiraCode 0.9923 等固有值)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 18:08:49 +08:00
崮生(子虚)
6c59c3e7e6 perf(gsub-subset): parseChainRuleFormat1or2 边读边 remap 提前失败
原实现对 back/input/look 先读到 backRaw/inputRaw/lookRaw 三个临时数组,
再二次遍历 remapSeq 重映射,format1 含子集外 gid 时规则失效(return null),
但 raw 数组已分配——FiraCode 多数 fmt1 规则因 input gid 不在子集而失效,raw 分配纯浪费。

改为 readSeq 边读边重映射(format1):遇子集外 gid 立即返回 null,省掉三个
临时数组与二次遍历。format2(class index 始终有效)原样读取不重映射,行为不变。

subsetGSUB min(checksum 与改动前完全一致):
- FiraCode 0.296→0.290ms(-2%)
- 初夏纯标点 0.075→0.071ms(-5%)

全字体 SSIM/输出字节均不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:58:15 +08:00
崮生(子虚)
7b5d03391f perf(gsub-reachable): type4 Ligature first gid 短路跳过整个 LigatureSet
LigatureSubst 第一分量(coverage gid)不在子集时整条 ligature 不会触发,
原实现在 first 不在子集时仍读取 setOff/ligCount/每条 ligature 的
ligOff/compCount/target 后才 continue。改为 first 检查上提到 LigatureSet
展开之前,不在子集直接跳过,省掉内部全部 u16 读取。

初夏明朝 type4 first gid 常全不在子集(首轮 coverage 多为非子集字形),
此短路消除每条 ligature 的无谓读取。

collectReachableGsubTargets min(checksum 与改动前完全一致):
- 初夏汉字标点 0.070→0.059ms(-16%)
- 初夏纯标点 0.081→0.068ms(-15%)
- FiraCode 0.105→0.101ms(-4%)

全字体 SSIM/输出字节均不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:49:42 +08:00
崮生(子虚)
799d284d4b perf(gsub): emitCoverage 跳过 slice+sort,readCoverageRemapped 缓存前统一排序
原 emitCoverage 对输入 newGids 做 slice().sort()——防御性复制+排序。
但 readCoverageRemapped 的输出(fmt3/writeChainFormat2 coverage)因 原gid→新gid
映射不保序而可能乱序,必须排序;type1/2/3/4 的 entries.map(e=>from) 虽已按 from
排序故升序,却仍付出 slice+sort 开销。

将排序职责上移到 readCoverageRemapped 缓存入口(covCache 命中率极高,FiraCode
604 引用/83 独立 coverage,排序分摊几乎免费),使所有调用方输出天然升序无重复。
emitCoverage 据此改为直接扫描区间(不 slice、不 sort、不分配 ranges 对象数组),
同时两遍扫描统一为 rangeCount 计数。

FiraCode subsetGSUB 417→390μs(-6.5%),输出 sha 全字体一致,SSIM 全不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:33:48 +08:00
崮生(子虚)
f053112e44 perf(gsub-reachable): type1 fmt1 + type3 条件反转遍历方向
延续 9d6e982(type1 fmt2 反转)的思路,对 SingleSubst format1 与
AlternateSubst(type3)同样反转遍历方向:遍历 reachable 二分查 covGids
(按 gid 升序)得 index,替代遍历 covGids 逐个查 reachable。

初夏明朝纯标点实测 fmt1 covLen 405、type3 covLen 987(命中率 2.3%),
均属「展开全量命中极少」模式,反转后遍历量从 covGids.length 降到
reachable.size。

关键:加条件 covGids.length > reachable.size 才反转。无条件反转在
霞鹜/FiraCode 上负优化(covGids 短,FiraCode type1 avg 5.5,二分开销
> Set.has 遍历)。条件反转使短 covGids 走原 Set.has 路径。

type4(Ligature)不反转:firstHit 判定反转后遍历 reachable(53)× 18
calls = 954 次二分 > 原 684 次 Set.has,必负。

A/B vs 9d6e982(3000 次 p50,reachable sha 全一致):
- 初夏标点 102→92μs(-10%)
- 初夏8字 54→51μs(-5%)
- 霞鹜标点 12→10μs(-12%,无条件版 +8% 已修正)
- 霞鹜8字 6.8→6.4μs(-7%)
- FiraCode 标点 70→69μs(-1%,无条件版 +2% 已修正)

相比最初 baseline(fee9e49 前)初夏标点 129→92μs(-29%)。基准测试
SSIM 全不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:07:13 +08:00
崮生(子虚)
9d6e982682 perf(gsub-reachable): type1 fmt2 反转遍历方向
collectSubtableTargets 的 SingleSubst format2 原遍历 coverage gid
逐个查 reachable(inSubset=Set.has)。初夏明朝纯标点实测 fmt2 covLen
合计 9180 gid/round,但 reachable 仅 53 → 命中率 <4%,9180 次 Set.has
几乎全 miss。

反转:遍历 reachable(小集合)二分查找 covGids(按 gid 升序,fmt1 list
/ fmt2 range 展开均天然排序)得 index,命中则读 gidArray[i]。遍历量从
covGids.length 降到 reachable.size。

A/B(3000 次 p50):
- 初夏标点 129→105μs(-19%)
- 初夏8字 64→58μs(-9%)
- 霞鹜标点 13→11μs(-13%)
- 霞鹜8字 7.8→6.6μs(-15%)

reachable 集合 sha256 全部一致,基准测试 SSIM 全不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:03:26 +08:00
崮生(子虚)
fee9e49164 perf(gsub-reachable): coverageFirstExcludedGid 边解析边查不展开 coverage 数组
原实现调用 readCoverageGids(off, cache) 完整展开 coverage 到数组再遍历找首个 excluded,
对 format2 range(初夏明朝 coverage 常覆盖上千 gid)即使首个 gid 就 excluded 也要展开全量,
是「展开全量命中极少」浪费(同类见 gsub-classdef-format2-bsearch)。

改为 cache 命中遍历缓存数组,miss 时直接边解析 coverage 边查 inSubset,命中即返回,
不分配数组、不填 cache。fmt3 失败路径的 coverage 永不被 collectSubtableTargets 读
(失败不产 target),故不填 cache 无碍。

reachable 集合 sha256 三字体完全一致,SSIM/ink 全不变。
初夏纯标点 collectReachable p50 0.159→0.136ms(-14%)。
2026-07-24 16:52:23 +08:00
崮生(子虚)
694364769e perf(gsub-reachable): 合并 lookupRelOffs 读取到 lookup parse 主循环
原实现先收集 lookupRelOffs 到中间数组,主循环再按下标回读。合并后直接在
主循环内读取偏移,省去中间数组分配与一次回读。FiraCode collectReachable
-2.3%(0.116→0.113ms),reachable 集合 sha256 不变

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 16:34:08 +08:00
崮生(子虚)
a1fe4da1ce perf(gsub): readCoverageRemapped format2 range 改子集 gid 二分
与 readClassDefMap format2 同思路:原实现逐 gid 遍历 [start..e] 全部
查 gidLookup(FiraCode coverage format2 首次展开共 2317 gid,子集占极小
部分)。改为在升序子集 gid 数组上二分定位 [start,end] 内的 gid,仅 push
命中的 newGid。range 非空即 origNonEmpty=true(无论是否命中子集),与原
逐 gid 遍历语义一致。

subsetGSUB 再 -8.7%(0.314→0.287ms),FiraCode woff2 min 1.8→1.6ms
GSUB 输出 sha256 三字体全一致,SSIM 不变

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 16:30:00 +08:00
崮生(子虚)
06c8fcf18c perf(gsub): readClassDefMap format2 range 改子集 gid 二分
原实现遍历每个 ClassDef range 的 [start..end] 全部 gid 查 gidLookup
(FiraCode 实测 33 次 readClassDefMap 展开共 9686 gid 仅命中 19,命中率
0.2%,子集仅 89 gid)。改为对每个 range 在升序子集 gid 数组上二分定位
[start,end] 内的 gid,仅对命中的 set class——从「展开 range 全部 gid」转为
「只处理落在 range 内的子集 gid」。

subsetGSUB: 0.330ms → 0.284ms (-14%),FiraCode woff2 min 2.1→1.8ms
GSUB 输出 sha256 三字体全一致,SSIM 不变(FiraCode 0.9923)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 16:24:25 +08:00
崮生(子虚)
7f983ff969 perf(gsub): readClassDefMap 用 gidLookup 数组替代 Map.get
fmt2 ChainContext 的 ClassDef 重映射原用 origToNew.get() 逐 gid 查询,
FiraCode fmt2 ClassDef 展开后 9699 个 gid 每个 Map.get(~9ns) 是 fmt2 路径
热点。改用 gidLookup Int32Array 索引(~1ns),语义等价(gidLookup[g]>=0 ⟺
origToNew.has(g) 且值相同)。输出字节三字体 sha256 全一致,SSIM 不变
(FiraCode 0.9923)。writeChainFormat2 移除冗余 origToNew 参数。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 15:44:53 +08:00
崮生(子虚)
b17d5a3700 perf(gsub): lookup 级全空预扫描跳过逐子表序列化
subsetGSUB 对每个 supported lookup 预扫描所有子表是否都 coverage 全子集外
(isSubtableSkipableByCoverage)。全空 lookup 序列化结果必然是 N 个空 subtable
(与逐子表 serializeSubtable 失败后 writeEmptySubtable 逐字节相同),直接在 lookup 级
批量写空,跳过 N 次 serializeSubtable 函数调用 + 预检 + rollback 开销。

初夏 lookup[5] type6 268 子表(小子集全不命中)是典型受益场景。

安全性:allEmpty 当且仅当所有子表 isSubtableSkipableByCoverage===true,而 serializeSubtable
内部对 skipable 子表直接 return false(→空 subtable),故输出逐字节相同(含 FiraCode 连字)。
format2 class 驱动子表不预检(返回 false),含 format2 的 lookup allEmpty 必为 false 走原路径。
subCount 不变、lookup 不删(区别于已证危险的 GSUB lookup 删除)。

subsetGSUB 初夏 0.215→0.178ms(-17%),全基准 SSIM 与输出字节完全不变(diff 验证 hash 一致)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 15:14:00 +08:00
崮生(子虚)
ee7b6ec230 perf(gpos): 全空 lookup 折叠为 subCount=1 + subtable 偏移槽消除闭包
优化330: 预扫描 supported lookup 的 subtable coverage 命中,全无命中的 lookup
折叠为单个空 subtable(feature 按 lookup index 引用,subCount 改变语义等价,
浏览器查空 coverage 即跳过)。大字体小子集场景(初夏 GPOS lookup[10] 283 个
SinglePos subtable 全空、lookup[7] 285 个仅 3 命中)原先逐空 subtable 序列化
是 subsetGPOS 主热点。unsupported lookup 同理折叠。

subsetGPOS 初夏纯标点 570→380μs(-33%)、思源 8 字 85→26μs(-70%)。
附带 woff2 体积下降(空 lookup 不再逐 subtable 输出):思源 8字 2632→2536B、
初夏纯标点 6940→6912B。SSIM 全部不变。

优化329: lookup 循环 subtable 偏移槽用 writeUint16 占位 + writeInt16At 统一回填,
替代 reserveOffset16 的 per-slot 闭包分配(短命闭包虽 V8 近乎免费,但简化代码)。
2026-07-24 14:57:02 +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
崮生(子虚)
aeb2622a57 perf(gsub-reachable): format3 失败路径跳过 contextGids 收集
format3 triggerable=false 是常态(初夏纯标点 280 个 format3 首轮全 false)。
失败时只需找首个不在 reachable 的 gid 记 failGid——此刻 contextGids 收集
无意义:break 前已检查的 gid 都在 reachable(inSubset=reachable.has),
调用方对其 reachable.add 是 no-op。改用 coverageFirstExcludedGid(复用
readCoverageGids 带 cache + 遍历短路)分离失败/成功两遍,失败路径省掉
contextGids.add 调用。仅 triggerable=true(coverage 全在子集、量小)时
第二遍收集 contextGids。

reachable 输出集合逐元素一致(初夏 23/FiraCode 89),全基准 SSIM 不变。
初夏 reach 0.265→0.245ms(-8%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:28:34 +08:00
崮生(子虚)
3103dd5db1 perf(gsub): ScriptList/FeatureList 整块拷贝快路径(同 GPOS)
subsetGSUB 复用 scriptListSpan/featureListSpan,span 自洽且不越过相邻 list
header offset 时整块 subarray 拷贝。交错判定用 SL span≤featureListOff、
FL span≤lookupListOff(LookupList subtable 散落,header offset 不能代表其
字节范围,故不用「下一更大 offset」作上界)。霞鹜文楷 GSUB 物理交错
(LL<SL<FL 且 ScriptTable 跨越 FL) 自动降级 serialize,字节与优化前完全一致。

收益:初夏 GSUB 11474→8474B(-26%)、FiraCode 15504→14878B(-4%),
保留 fontTools FeatureTable 去重。霞鹜降级路径 cmp 逐字节一致。全基准 SSIM 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:23:06 +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
崮生(子虚)
8c311768f6 perf(gsub-reachable): format3 失败 gid 跨轮跳过冗余扫描
不动点迭代中 format3 triggerable=false 的 subtable,记录使其失败的第一个
coverage gid。下一轮若该 gid 仍未进 reachable,本 subtable 必定仍 false,
直接跳过 collectChainRefs 重扫。

原 settledChain 只记忆稳定 subtable(format2 / format3 triggerable=true),
format3 triggerable=false 每轮都重扫。初夏纯标点 280 个 format3 全 false,
3 轮 × 280 = 840 次 collectChainRefs;改后 chainCalls 280→(第 2 轮起全跳过),
-67%。失败 gid 进 reachable 时重扫(可能仍 false 则更新失败 gid,可能变 true
则转 settledChain)。

reachable 集合字节级一致(初夏/得意黑/思源全 cmp 通过),SSIM 全程不变。
初夏 reach 0.33→0.29ms(-13%,collectChainRefs 非 reach 全部开销故收益有限)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:05:52 +08:00
崮生(子虚)
aa2818b13e perf(gsub): gidLookup 改 Int32Array,大数组 fill 快 3×
GidLookup(原gid→新gid 查找表)原用 number[].fill(-1), TypedArray 的 fill
是 native memset,比 number[].fill 快约 3×(35515 项:219μs→67μs)。

初夏明朝 subsetGids 仅十余个但 maxOrigGid 达 3.5 万(reachable 的高 gid target),
number[].fill(-1) 耗时 ~200μs,占 subsetGSUB 总耗时 ~47%。Int32Array.fill
降至 ~67μs,索引访问速度实测与 number[] 一致(FiraCode 密集查询场景同 8μs)。

初夏 rewrite 0.663→0.501ms(-24.5%),总 fontSubset -9.2%;FiraCode 略快 -2.8%。
GSUB 输出字节级一致(cmp 通过),30 项基准 SSIM 零变化。

推翻记忆 gsub-subset-perf-coverage-cache 中「number[] 比 Int32Array 快 2×」结论
(旧 V8 或不同测试条件),当代 V8 下两者访问速度相同,TypedArray fill 显著更快。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:38:27 +08:00
崮生(子虚)
6d897f1987 perf(gsub-reachable): collectChainRuleRefs 消除 3 个临时数组分配
ChainSubRule 解析原先读 backtrack/input/lookahead 到 3 个 number[] 再两遍遍历
(先 inSubset 校验再 contextGids.add),format1 每 rule 分配 3 数组
(FiraCode 424 次/call × 3 = 1272 次数组分配 + GC 压力)。

改为两遍扫描同一段字节,不分配中间数组:
- 第一遍仅 inSubset 校验全部 gid(遇子集外即 return,放弃规则)
- 通过校验后再第二遍收集 context gid + 读 subst records

reachable 输出字节级一致(FiraCode 132 / 初夏 63 均通过 cmp),
FiraCode reach 0.368→0.343ms(-6.8%),30 项基准 SSIM 零变化。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:30:05 +08:00
崮生(子虚)
dd4f7ba55a perf(gsub-subset): format3 预检用 readCoverageRemapped 保证判定一致
opt317 的 format3 预检用 coverageAllOutOfSubset 内联判定,其 COV_RANGE
累积超限逻辑与 parseChainFormat3 实际用的 readCoverageRemapped 不一致,
导致 FiraCode 13 个 format3 预检未跳过却深度解析失败(0.328ms/call 浪费)。

预检改用 readCoverageRemapped(与深度解析同一判定函数 + 共享 covCache),
精确保证「预检跳过 ⟺ 深度解析失败」,输出完全一致(都是空 subtable)。
readCoverageRemapped 触碰 covCache 无副作用(gids 留 EMPTY_GIDS 占位,
readCoverageGids 命中按 miss 重算,已有逻辑)。

另:writeEmptySubtable 的 coverageOffset 恒为 6,改直接写常量,
省 reserveOffset16 的 patches push + 闭包分配。

FiraCode subsetGSUB 1.049→0.768ms(-27%),fontSubset 2.49→2.27ms(-9%)。
GSUB/woff2 字节 cmp 全绿,30 项基准 SSIM 零变化(FiraCode 0.9923、
千字文 0.9986、初夏纯标点 0.9920、otf 0.9681/0.9310 均不变)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:14:38 +08:00
崮生(子虚)
1da81eb0c0 chore(gsub-subset): 清理 profiling 残留的 __sp 插桩
上一轮 opt317 提交时误带入了 format1/format3 序列化路径上的 __sp 计时
插桩(globalThis 属性读取 + performance.now 调用)。热路径每个 type6
subtable 都执行,属无关生产代码。删除并移除无用的 perf_hooks import。

GSUB 字节输出零变化(cmp 一致),性能持平。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:02:46 +08:00
崮生(子虚)
4c9022adb6 perf(gsub-subset): format3 预检判定修正跳过不可能触发的规则
subsetGSUB 真 hot path(FiraCode 2.2ms)是 type6 format3 失败路径占 69%
(16 次×0.094ms)。根因:isSubtableSkipableByCoverage format3 旧判定
「整组 coverage 全子集外」语义过强,当一组内部分 coverage 全子集外、
部分含子集内 gid 时预检不跳过,但 parseChainFormat3 遇到那个全子集外的
coverage 即返回 null 失败,白白深度解析。

format3 规则触发需三组 coverage 的 gid 全在子集内,故任一 coverage 原非空
且全子集外(coverageAllOutOfSubset===true,等价 readCoverageRemapped===null)
即不可能触发,预检直接跳过。

FiraCode fontSubset 3.751→2.029ms(-46%)。GSUB 字节 cmp 完全一致,
woff2 端到端字节 cmp 全绿,SSIM 0.9923 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:55:49 +08:00
崮生(子虚)
56e8b6db4e perf(gsub-reachable): format3 闭包消除 + 固定点迭代稳定性记忆
collectChainRefs 在固定点迭代中高频调用(初夏纯标点 280 个 format3 subtable ×
多轮),每次创建 readCovGidsChecked 闭包有分配开销。

优化315: format3 分支消除闭包,改显式三段循环手动推进 p,保留 triggerable 短路。
优化316: collectChainRefs 返回稳定标志(format2 总稳定 / format3 triggerable=true
稳定),主循环用 settledChain 记忆已稳定 subtable 偏移,后续轮直接跳过。

稳定测量(N=300 预热):
  FiraCode       3.757 → 3.529ms (-6%)
  初夏-纯标点     1.847 → 1.548ms (-16%)
  初夏-汉字标点   1.503 → 1.260ms (-16%)
reach 阶段(单测): FiraCode 0.449→0.318ms, 初夏 0.544→0.315ms
无 GSUB 字体(令东/思源)无影响。所有用例 reach 输出与黄金基准逐字节一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:43:55 +08:00
崮生(子虚)
d68551d23e feat(stats): 统计计数跨重启持久化 + 依赖升级
- 新增 stats_store.ts:防抖写盘 font/.stats.json,热路径零磁盘 IO
- shared.ts 暴露 initStats/markStatsDirty/snapshotStats
- app.ts 启动恢复累计计数 + 退出前尽力落盘
- 路由热路径打脏标记(防抖 30s 批量落盘)
- 清理未用变量/导入/常量
- 升级依赖:vue 3.6.0-beta.17、typescript 7.0.2、vite 8.1.5 等

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 21:43:12 +08:00
崮生(子虚)
f7a2a4dc86 perf(gsub-reachable): collectChainRefs format3 短路 + 去闭包/中间数组
固定点迭代中 collectChainRefs 高频调用(初夏纯标点 840 次/call)。format3 原实现:
读完全部 backtrack/input/lookahead coverage 把 gid 收进 allGids 中间数组,任一不在
子集才置 triggerable=false,且每次调用分配 readCovGids 闭包 + allGids 数组。

优化:遇到不在子集的 gid 立即短路返回(triggerable=false 时 contextGids/refs 本就不
收集,后续 coverage 无需再读);去掉闭包与 allGids,triggerable 时 gid 直接 add 进
contextGids。逻辑完全等价(triggerable=false 时原代码也不 add contextGids)。

基准 29 用例 SSIM/ink/字节全无回归。初夏纯标点 2.34→1.86ms(-20%),FiraCode 3.7→3.4ms。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:45:08 +08:00
崮生(子虚)
7d62bce1df perf(gsub): readCoverageRemapped format2 range clamp 到 numGlyphs
承接 fda7ab8(format1 损坏 coverage 跳过)。插桩定位剩余 miss 热点:format2
(range) 仅 11 次 miss,但 439 个 range 的 end >= numGlyphs(1652),逐 gid 展开
浪费 ~320 万次 gidLookup 索引(全部 g>=numGlyphs 越界跳过),是 readCoverageRemapped
剩余 55% self time 的绝大头。

将每个 range 的 end clamp 到 numGlyphs-1:start..numGlyphs-1 段与原代码逐 gid 处理
完全相同,numGlyphs..end 段原代码全越界跳过、clamp 后省去空循环。整个 range 越界
(start>=numGlyphs) 时保留 origNonEmpty=true 语义(→ outOfSubset → null),与原代码
逐 gid 遍历全空结果完全等价。

基准 29 用例 SSIM/ink/字节全无回归,FiraCode fontSubset 6.8ms→3.75ms(累计较
优化前 12.2ms -69%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:39:12 +08:00
崮生(子虚)
fda7ab8902 perf(gsub): readCoverageRemapped 跳过损坏 coverage 的空循环
FiraCode subsetGSUB 的 readCoverageRemapped 占 70.5% self time。插桩定位:
77 次 format1 miss 中有 8 次 count 高达 15460(远超字体 glyph 总数 1652),
这些是 ChainContextSubst format3 指向错误偏移读到的垃圾 count,原代码循环
count 次全部 gidLookup[g] 越界返回 undefined 被跳过,浪费 107852 次迭代
(占 format1 miss 总工作量的 95%)。

count > numGlyphs 时跳过必然全空的循环,仅保留 origNonEmpty=count>0 语义
→ newGids 空 → outOfSubset=true → 返回 null,与原代码循环全空结果完全等价。
基准 29 用例 SSIM/ink/字节全无回归,FiraCode fontSubset 12.2ms→6.8ms(-45%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:34:10 +08:00
崮生(子虚)
055425e852 perf(gsub): readCoverageRemapped format1 用 Uint16Array 批量读
coverage format1 的 gid 列表是连续 count 个大端 u16,原 DataView.getUint16
逐次调用有边界检查 + 大端组装开销。gid 数组 2 字节对齐时改用 Uint16Array
view 共享 buffer 读取 + 内联翻转 ((raw&0xff)<<8)|(raw>>8)(与 hmtx/loca 同思路),
未对齐回退 DataView。

readCoverageRemapped 是 FiraCode subsetGSUB 第一大热点(75.5%),miss 路径
sum 遍历 ~26000 gid。实测 FiraCode fontSubset 中位数 13.65→12.43ms(-8.9%),
5 轮稳定。基准 SSIM/ink/字节全无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:03:40 +08:00
崮生(子虚)
6ea19550a1 perf(gsub-reachable): collectReachableGsubTargets 单 Set inSubset + 复用临时 Set
初夏明朝纯标点(最慢 CJK 用例 4ms)profile 显示 collectReachableGsubTargets 占 19.2% +
collectChainRefs 7.8%(合计 27%,超过 subsetGSUB 本身 6.4% 的 3 倍)。单次 1.22ms。

优化:
1. inSubset 闭包原为 seedGids.has(g) || reachable.has(g) 两次 Set 查询(热路径每 subtable
   多次调用)。改为 reachable 初始即含 seedGids,inSubset 只查 reachable 一个 Set。
   返回的 reachable 可能含 seed gid——调用方注入 extraSubsetGids 后 Font.create 去重,无害。
2. 每个 type6 subtable 原 new Set() × 2(refs + ctxGids),多轮迭代 × 51 lookup 累积 GC 压力。
   改为循环外复用 + clear。

实测初夏明朝 1.22→1.02ms(-16%),FiraCode 0.75→0.69ms(-8%)。
基准测试 29 用例 SSIM/ink/输出字节全部无回归(初夏纯标点 4.4→3.6ms)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:15:50 +08:00
崮生(子虚)
44f84e5659 perf(gsub-subset): serializeSingle/Multiple/Alternate/Ligature 改用 gidLookup 数组查找
serializeSingleSubst/MultipleSubst/AlternateSubst/LigatureSubst 原用 remapGid(origToNew Map)
对每个 coverage gid + 分量 gid 做哈希查询。subsetGSUB 入口已构建 gidLookup 数组
(下标=原gid,值=新gid/-1,比 Map.get 快 ~2×),但这四个函数未使用,仍传 origToNew Map。

serializeLigatureSubst 是 CJK ligature 子集化热点(50gid 子集占 subsetGSUB 65%,
每条 ligature 对全部分量密集 remapGid)。改为收 gidLookup 参数,remapGid(Map.get)
→ gidLookup[gid](数组索引,-1 即子集外)。

四个函数签名 origToNew: Map → gidLookup: GidLookup,serializeSubtable 调度相应更新。
ChainContext format1 的两处 remapGid(serializeChain/parseChainRuleFormat1or2)暂未改
(FiraCode 路径 serialize 占比小,readCoverageRemapped 是主瓶颈)。

验证:基准测试 29 用例 SSIM 全部无回归;4 用例 sha256 输出字节逐字节等价。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:45:39 +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
崮生(子虚)
2c56d2cbb2 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>
2026-07-23 17:41:16 +08:00