56 Commits

Author SHA1 Message Date
崮生(子虚)
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
崮生(子虚)
2da35e4257 perf(gsub): emitCoverage 去掉冗余的 new Set 去重
emitCoverage 原用 Array.from(new Set(newGids)).sort() 去重+排序。
但所有调用方传入的 newGids 逻辑上保证无重复:
- readCoverageRemapped 按原 coverage(规范要求 gid 唯一)升序过滤,每个原 gid 映射唯一新 gid
- entries.map(e=>e.from) 的 from 是 entry 主键(唯一)
实测 FiraCode 单次子集化 327 次 emitCoverage 调用 0 次发现重复,
new Set 去重纯属 GC 开销(Set 构造+迭代+Array.from)。

改为 newGids.slice().sort(),消除 Set 分配。sort 保留(新 gid 随原 gid 非单调,需排序)。

验证:与优化前字节逐字节一致(cmp 通过,8724==8724),
pnpx tsx 基准测试.test.ts 全部 SSIM 无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:16:45 +08:00
崮生(子虚)
5da4cd5b06 perf(gsub): fmt3 ChainContext 预检跳过 + 修复 covCache 共享语义污染
预检优化(type1-4 + fmt1 + fmt3 全覆盖):
- 重构 isPrimaryCoverageOutOfSubset → isSubtableSkipableByCoverage
  + 抽出 coverageAllOutOfSubset 辅助函数(内联遍历,不分配数组、不碰缓存)
- 新增 ChainContext format3 预检:遍历 back/input/look 三组 coverage,
  任一组「原非空且全子集外」即判定 subtable 失效,跳过深度解析
- FiraCode 403 lookup 中 ~330 个可预检跳过,子集化 23ms→16ms(-31%)

covCache 共享语义污染修复(fmt3 预检能安全启用的前提):
- CoverageCacheEntry 的 gids(原始 gid,fmt1 按 index 配对)与
  remapped(newGids,fmt3 序列化)两种语义原混写同一字段:
  旧 readCoverageRemapped 把 newGids 写进 gids,污染后续 fmt1 的
  readCoverageGids(拿到 newGids 当原始 gid 二次重映射,连字规则错位)
- 修复:readCoverageRemapped 只写 remapped,gids 留单例占位 EMPTY_GIDS;
  readCoverageGids 见占位即重新计算并 mergeGidsEntry(保留已有 remapped)
- 修复后 fmt3 预检开关下字节完全等价(8724==8724),SSIM 全程无回归

验证:pnpx tsx 基准测试.test.ts 全部 SSIM 无回归
(FiraCode=0.9923, otf-思源黑体=0.9310, 初夏明朝-纯标点=0.9920 不变)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:05:46 +08:00
崮生(子虚)
25da274c3a perf(gsub): subsetGSUB 主 coverage 预检跳过空 lookup,FiraCode 33ms→22ms
FiraCode 含 403 个 lookup,子集仅命中少数字形,多数 lookup 主 coverage 引用
的 gid 全部不在子集内——深度序列化后必然得到空 entries、回退 writeEmptySubtable。
预检在深度解析前用 gidLookup 内联判定主 coverage 是否全子集外,命中即跳过
(type1-4 + type6-format1,共 ~216 个空 lookup),消除其 entries 构建/rule 解析开销。

关键正确性:
- format2(class 驱动)/format3(多 coverage)不预检——主 coverage 非充分条件,
  误判全空会丢连字(实测 FiraCode calt format2,SSIM 0.9923→0.9368)
- 预检不经 covCache——该缓存被 readCoverageRemapped(存 newGids)与
  readCoverageGids(format1 期望原始 gid)共享,语义不一致,预检写入会污染
  format1 深度解析(newGids 被当原始 gid 二次重映射,连字规则错位)
- 输出字节与预检关闭时完全等价(已 dump 对比验证),SSIM 全部无回归

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:45:43 +08:00
崮生(子虚)
771f4fc6de perf(gsub): GidLookup 用 number[] + format1 预分配,FiraCode 30ms→23ms
- GidLookup 从 Int32Array 改为 number[]:实测 V8 下普通数组索引访问比
  TypedArray 快约 2×(coverage 过滤高频查询)
- readCoverageRemapped format1 预分配 newGids(count) 索引写入后截断,
  避免 push 动态扩容

SSIM 0.9923 保持,输出 8708B 稳定。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:38:05 +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
崮生(子虚)
7d0243e6f5 fix(v1.9.2): subsetCache 改用 Date.now() 启动时戳,修复 LLRT process.uptime 报错
process.uptime() 在 LLRT 运行时不存在(typeof === "undefined"),调用抛
TypeError: not a function,导致容器启动即崩溃(v1.9.1 修完 __filename 后暴露)。

改用模块加载时取一次 Date.now():Node/LLRT 均可用,进程重启即变化,
开发态「重启即重置内存缓存」语义不变,生产态靠 PACKAGE_VERSION。

另增 dev:backend:llrt / serve:llrt 本地命令,用 LLRT 跑后端提前暴露兼容问题。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:32:33 +08:00
崮生(子虚)
ef5514a008 fix: subsetCache 版本指纹改用构建期注入,修复 LLRT __filename 报错
createRequire(import.meta.url) 经 tsdown 打包为 CJS 后引入 __filename,
而 LLRT 运行时不提供 __filename,导致 ReferenceError: __filename is not defined。

改用 tsdown define 构建期把 PACKAGE_VERSION 替换为字面量字符串,
运行时无文件读取、无 __filename 依赖。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:14:01 +08:00
崮生(子虚)
168fc66544 feat(v1.9.0): OTF 子集化支持 + 法字笔画修复 + subsetCache 版本指纹
性能(vs v1.7.0 基线,基准测试全用例通过):
- 千字文 woff2 346ms → 9.8ms(35x)
- 8汉字 woff2 39.7ms → 2.8ms(14x)
- 千字文 ttf 6.0ms → 4.4ms
- 8汉字 ttf 2.7ms → 2.1ms

OTF 正确性:
- 修复 CFF.js readCFFIndexObject 漏读 off[idx+1] 导致子程序读成 0 字节,
  思源黑体「法」字三点水丢笔画的问题
- 修复非 subset 模式 cmap format12 崩溃(空数组误判为 subset)

缓存健壮性:
- subsetCache key 加版本指纹(package version + process.uptime),
  杜绝代码已修但缓存返回旧错误结果

基准测试:
- 新增白狐/思源 OTF 用例(含法海波等复杂笔画字)守护 OTF→TTF 转换
- 修复 calculateSSIM 的 width 推断 bug(sqrt 假设正方形)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:01:19 +08:00
崮生(子虚)
b4a7a820eb feat: AI Typography Skill 系统 + 中英双语 i18n + Before/After Demo
- 新增 skills/chinese-web-typography.md 作为提供给 AI agent 的排版智能文件
- 重写 README.md(中文默认),新增 README.en.md(英文版),互相引用
- 新增 TypographyDemo.vue 前后对比组件,展示中文字体效果差异
- 新增 src/i18n.ts 轻量国际化方案,所有组件文案支持中英切换
- 后端 /api/fonts 返回 temporary 字段标识临时字体
- 升级至 v1.8.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:41:47 +08:00
崮生(子虚)
53d170dc90 feat: 前端迁移至 Vue3 + TS,后端构建迁移至 tsdown
- 前端从 SolidJS 迁移到 Vue3 Composition API(<script setup lang="ts">)
- 后端构建从 tsup 迁移到 tsdown,动态 import 改为静态以支持单文件输出
- FontSelector 添加 defineProps 修复下拉无选项问题
- StatsPanel 添加"服务状态"标题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:46:30 +08:00
崮生(子虚)
c18dc44e72 chore: 升级至 v1.7.0,新增内存缓存与运行时统计
- 新增通用 LruCache 类,支持按条目数/字节容量两种淘汰策略
- 字体裁剪结果 LRU 内存缓存(默认 10MB,X-Cache 响应头标识命中)
- 新增 GET /api/stats 运行时统计接口
- 前端统计面板(10s 轮询,页面不可见时暂停)
- API handler 拆分到 routes/ 目录,提取 shared.ts 共享模块

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 19:34:30 +08:00
崮生(子虚)
bd3189b459 优化性能 2026-04-11 17:53:57 +08:00
崮生(子虚)
6ba093ff70 优化性能 2026-04-11 10:25:54 +08:00
崮生(子虚)
2f9eb3fd1a 支持 woff2 2026-04-11 00:17:27 +08:00
崮生(子虚)
161bafc02a 实现 js 版 woff2 处理 2026-04-10 20:42:30 +08:00
崮生(子虚)
1e543d58ab fix: 修复 OTF→TTF 子集字体浏览器渲染空白 + 改进基准测试
- API 默认 outType 改为 ttf(兼容性最好)
- Cache-Control 从 immutable 改为 24h 缓存
- 基准测试改为 DOM 渲染 + puppeteer 截图 + pngjs 解码(更贴近真实浏览器)
- 增加 maxp 表验证(maxPoints/maxContours 为 0 直接报错)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 13:55:27 +08:00
崮生(子虚)
18e60fe940 优化性能 2026-04-09 16:47:41 +08:00
崮生(子虚)
e2cc7c1144 feat: 前端格式选择器组件,服务端支持能力透传
- 新增 FontSelector.tsx 独立组件,字体和输出格式并排选择
- /api/config 新增 supportedOutTypes 字段(LLRT 仅 ttf)
- 前端加载配置后自动适配可用格式

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 11:56:10 +08:00
崮生(子虚)
820fc71166 feat: 新增 woff2 输出格式支持,默认使用 woff2
- 后端支持 outType URL 参数(woff2/ttf),Node.js 默认 woff2,LLRT 默认 ttf
- woff2 wasm 延迟初始化,仅首次请求时加载(~8ms)
- SDK 三个入口函数支持 outType 选项,默认 woff2
- 前端 CSS、下载、使用说明适配 woff2
- 基准测试新增 woff2 测试(耗时、体积、压缩率)
- 构建脚本自动复制 woff2.wasm 到 dist 目录

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 11:48:07 +08:00
崮生(子虚)
765a301649 修改为本地引用,清理冗余文件 2026-04-09 09:50:26 +08:00
崮生(子虚)
c74278cf2f 更改优先级 2026-04-08 22:35:58 +08:00
崮生(子虚)
79ee7dd3f0 内存缓存最近三个字体,避免高频的文件io 2026-04-08 22:31:53 +08:00
崮生(子虚)
b902b4e1cc 增强安全性,重构代码 2026-04-08 22:05:17 +08:00
崮生(子虚)
878b54a0fd 修复上传功能、移除 tjs 运行时支持、添加 vite-plugin-pilot
- 修复 HTTP header 大小写不敏感匹配,解决浏览器上传请求体为空的问题
- 重写 createStreamAfterTarget,修复大文件上传时 body stream 数据流断裂
- 添加 chunked transfer encoding 解码支持
- 读取完 body 后 cancel stream,防止后台循环抛异常炸进程
- 修复 parseHttpRequest 中 split(":") 对含冒号 header value 的错误拆分
- 临时上传同名文件直接覆盖,不再加时间戳前缀
- 移除 tjs (txiki.js) 运行时支持及相关代码
- 安装并配置 vite-plugin-pilot 浏览器测试工具

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 20:31:43 +08:00
崮生(子虚)
e0f34c36b6 修复上传功能和服务器健壮性问题
- 修复 multipart 解析器:endDelimiter 提前检查导致循环退出
- 修复 Node.js v24 兼容:改为先读取 body 再构造 Request
- 修复 HTTP keep-alive 死锁:按 Content-Length 精确读取 body
- 修复服务器崩溃:connectionHandle 加 try-catch,单连接错误不影响全局
- 修复 URL 写死:webfont.shenzilong.com 改为 localhost
- 复制按钮加"已复制/复制失败"反馈提示
- TEMP_MAX_FILES 改为环境变量可配置
- dev 环境默认启用临时上传和管理员上传
- CSS 代码默认展示,使用技巧不再折叠

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 17:11:55 +08:00
崮生(子虚)
7fceccfe4d 重构前端UI,新增字体上传/下载/模糊匹配功能
- 前端UI重构:字体选择器、文本预览、CSS输出+复制、下载字体、上传区域
- 后端新增API:/api/fonts(列出字体)、/api/config(公开配置)、/api/upload(上传字体)
- 字体查找支持模糊匹配(精确 > 前缀 > 包含)
- 上传功能分两种模式:临时上传(ENV开关,FIFO上限10个)和管理员上传(API Key认证)
- 新增multipart解析器、配置模块、文件系统抽象(writeFile/readdir/mkdir/unlink)
- 后端启动时等待运行时初始化完成,确保接口可用
- dev-all.ts后端启用tsx watch模式自动重载

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 16:18:46 +08:00
崮生(子虚)
1a611e7e28 自行下载 llrt 2026-04-08 15:41:39 +08:00
zixu
1a7a131469 fix gitignore 2024-09-04 10:45:58 +08:00
zixu
4230db9058 微调接口 2024-08-29 10:00:22 +08:00