崮生(子虚)
|
d2f6717348
|
feat: 排队请求按字体分组,同字体连续处理
- withMemoryGate 新增 groupKey 参数,subset.ts 传入 fontPath
- pickNext: 优先唤醒与上一个相同 groupKey 的等待者
- 同字体请求连续处理:字体 buffer/解析对象在 GC 窗口内被复用
- 大幅降低排队积压时的内存峰值(避免反复加载不同字体)
|
2026-07-30 22:53:39 +08:00 |
|
崮生(子虚)
|
7403414439
|
feat: 用内存水位闸门替代固定并发限制
- subset_queue: 新增 withMemoryGate,基于 RSS 决定排队/执行
- getRssMB: Node 用 process.memoryUsage(),LLRT 用 /proc/self/statm
- 请求完成后主动 __gc() 加速内存回收
- config: SUBSET_MEM_SOFT_LIMIT_MB=600 替代 SUBSET_CONCURRENCY=4
- 小请求可高并发(内存够),大请求自动排队(防 OOM)
|
2026-07-30 22:51:37 +08:00 |
|
崮生(子虚)
|
b86d2fcb53
|
fix: 并发默认调到4,队列超时改为可配(30s)
- SUBSET_CONCURRENCY 默认 2 → 4(900M 内存够用)
- 新增 SUBSET_QUEUE_TIMEOUT 环境变量(默认30秒,原硬编码10秒)
- 压测发现10秒超时太短:50并发只有8成功,其余排队超时
|
2026-07-30 21:46:21 +08:00 |
|
崮生(子虚)
|
65ba984972
|
feat: 字体子集化并发队列控制(服务端+客户端)
后端:
- subset_queue.ts: Semaphore 并发控制器,排队超时返回 503
- subset.ts: fontSubset 调用包裹在 withConcurrencyLimit 中
- config.ts: 新增 SUBSET_CONCURRENCY 环境变量(默认 2)
- routes/config.ts: 暴露 subsetConcurrency 到配置 API
前端:
- api.ts: ServerConfig 新增 subsetConcurrency 字段
- webfont-sdk.js: 客户端并发池(默认 4),setMaxConcurrent API
通过 document.fonts.load 精确追踪字体加载完成释放槽位
|
2026-07-30 21:33:45 +08:00 |
|
崮生(子虚)
|
abd75985b7
|
feat: 临时字体保留时限改为秒为单位(TEMP_RETENTION_SECONDS)
- TEMP_RETENTION_HOURS → TEMP_RETENTION_SECONDS,默认 10800(3小时)
- 前端显示自适应:秒/分钟/小时
- README docker-compose 示例同步更新
|
2026-07-30 21:24:38 +08:00 |
|
崮生(子虚)
|
19b14f58ea
|
feat: 字体元数据三层架构 + 临时字体保留机制 + 列表页覆盖率排序
- font_meta.ts: cmap 解析覆盖率 + name 表提取字体信息(版权/作者/许可) + 人工配置(font-config.json)
- 三层缓存: 进程内存 → .meta.json 磁盘 → font-config.json stat mtime 热更新
- 临时字体保留机制: TEMP_RETENTION_HOURS(默认3h), 超时未使用自动删除
- 列表页: 覆盖率标签 + 按字符集覆盖率排序
- 详情页: 标签/开源链接/简介 + 字体信息面板(设计师/版权/许可)
- 首页: 红色警告提示切勿上传非商用字体 + 显示保留时限
- SSG noindex bug 修复
|
2026-07-30 21:19:09 +08:00 |
|
崮生(子虚)
|
5b82dec19a
|
fix: 首页 header 窄屏适配,按钮文字不换行
- header 外层 flex-wrap: wrap,窄屏时标题与按钮组自动换行
- 按钮组 flex-wrap: nowrap + flex-shrink: 0,保持在一行
- 所有按钮/链接 white-space: nowrap + flex-shrink: 0,文字不折行
|
2026-07-30 20:33:32 +08:00 |
|
崮生(子虚)
|
cdffd4cb57
|
feat: 字体列表页 + 详情页 SSG + LazyTrigger 懒加载组件
- FontList.vue: 字体列表页,卡片网格布局,支持拼音搜索,点击进入详情
- FontDetail.vue: 字体详情页,SSG 模板 + 服务端占位符替换
- LazyTrigger.vue: 可复用懒触发组件,IntersectionObserver 进入视口回调
- 首页添加字体列表入口,FontDetail 返回列表
- placeholders.ts: 前后端共享的占位符常量
- 后端 font_detail.ts: 读取 SSG 模板并替换占位符
|
2026-07-30 20:32:42 +08:00 |
|
崮生(子虚)
|
d064e9bc8f
|
fix: 进度条方向 + 宽度
- 进度条缺少 width:100%,导致 absolute 元素宽度为 0 不可见
- keyframes 改为 scaleX(0→1),从空到满增长(符合进度直觉)
|
2026-07-30 19:53:57 +08:00 |
|
崮生(子虚)
|
3af1a19dce
|
fix: StatsPanel 卡片不显示 — IntersectionObserver 死锁
根因: v-if=data 和 ref=rootRef 在同一元素,data 为 null 时
元素不存在 → observer 无法 observe → data 永远不会加载。
修复: initialLoad() 在 onMounted 时立即调用,不依赖 observer。
IntersectionObserver 仅负责后续轮询的暂停/恢复。
同时修复 formatUptime 中文分支的秒数计算 bug。
|
2026-07-30 17:33:20 +08:00 |
|
崮生(子虚)
|
8273508c79
|
feat: StatsPanel 视口内才轮询 + 底部进度条动画
- IntersectionObserver + document.visibilityState 双条件控制轮询
- 卡片底部进度条 (scaleX 动画),每次刷新数据后重启动画
- 避免不可见时浪费请求
- bump version to 1.11.1
|
2026-07-30 17:26:24 +08:00 |
|
崮生(子虚)
|
5c3ff96c00
|
docs: 更新 README 裁剪性能数据(node 实测亚毫秒级)
node v24 实测令东齐伋复刻体 50 轮 min:拉丁 0.05ms、8 汉字 0.08ms、
千字文88字 0.19ms(ttf),均达亚毫秒级。补充 X-Timing-* 响应头说明。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-26 10:57:55 +08:00 |
|
崮生(子虚)
|
1c87e8ff46
|
chore: bump version to 1.9.5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-26 10:48:42 +08:00 |
|
崮生(子虚)
|
7a69792730
|
perf(sdk): 跨域 baseUrl 自动注入 preconnect 提前握手
SDK 部署到第三方站点时,字体请求是跨域的,首个片段要付出 ~60ms
TCP+TLS 握手。getLoader 首次创建 loader 时按 baseUrl 注入
<link rel="preconnect" crossorigin>,让浏览器提前完成握手,首个
增量片段延迟从 ~90ms 降到 ~30ms。同源不注入,每个 origin 只注入一次。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-26 10:48:42 +08:00 |
|
崮生(子虚)
|
4c11470d8c
|
chore: bump version to 1.9.4
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-26 10:39:35 +08:00 |
|
崮生(子虚)
|
27c91bcdc2
|
perf(observability): /api subset 响应头加 X-Timing-* 分阶段耗时
38ms 请求定位用:X-Timing-Find/Read/Subset/Total 分别对应 findFontPath、
readFontBuffer、fontSubset、总计。本地实测 60ms 中 58ms 在读字体文件、
裁剪仅 1ms,证伪「裁剪慢」假设。缓存命中只返回 X-Timing-Total。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-26 10:39:31 +08:00 |
|
崮生(子虚)
|
259cf7f2d8
|
chore: bump version to 1.9.3
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-26 10:23:47 +08:00 |
|
崮生(子虚)
|
519ce0473b
|
perf(gpos/gsub): 部分命中 lookup 跳过空 subtable(subCount 只数命中的)
此前「部分命中」的 lookup(anyHit=true 但非全命中)仍把每个未命中 subtable
序列化为空 subtable(writeEmpty(Pos)Subtable,~10B/个)。这些空 subtable 的
coverage 必为空,浏览器查不到字形即跳过——直接不输出渲染等价:lookup 的
subtable 按序遍历,第一个匹配 coverage 应用,删掉必然空的 subtable 不改变
非空 subtable 的匹配结果。
GPOS:hits[j]=false 的 subtable 跳过;初夏 lookup[7] 285 subtable 仅 6 命中,
省 (279-1)×10B。
GSUB:subtableSkipable[j]=true 的 subtable 跳过(format2 永远 not skipable
故含 class 驱动 subtable 的 lookup 不受影响);not skipable 但序列化失败的
仍走 fallback 写空(保留在输出)。
与优化330/337(全空 lookup 折叠 subCount=1)互补:那些处理「全部不命中」,
本次处理「部分不命中」。anyHit=true 保证至少 1 个命中,subCount>=1 合法。
收益(30 用例 SSIM 全不变,总体积 -2408B):
- 初夏明朝-汉字标点 woff2 6012→4900 (-1112B)
- 初夏明朝-纯标点 woff2 6252→5260 (-992B)
- FiraCode woff2 8672→8560 (-112B)
- 思源黑体-标点 woff2 2940→2884 (-56B)
- 霞鹜文楷-汉字标点/纯标点 woff2 -20/-12B
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 13:42:00 +08:00 |
|
崮生(子虚)
|
4520a901ef
|
perf(otf): hmtx/vmtx 尾部相同 advance 压缩(补齐 TTF 侧 fonteditor 早做的压缩)
subsetOTF 的 buildSubsetMetrics 原把所有 subsetGids 展开为完整 4 字节
(advance+lsb),丢失 hmtx 规范的 numberOfHMetrics 压缩语义。CJK 全角字体
子集后几乎所有 advance 相同(如 1000),尾部 N-1 个 glyph 可只写 lsb(2字节)。
从尾部向前找最长连续相同 advance 段,numberOfHMetrics 收缩为最后一个不同
advance 的位置+1,hhea/vhea 的 numberOfHMetrics 同步 patch。
渲染等价(浏览器对 gid>=numberOfHMetrics 复用最后一个 advance),SSIM 不变。
收益(3 otf 用例 woff2 共 -144B):
- otf-白狐千字文 35880→35792 (-88B)
- otf-五个汉字 15020→14984 (-36B)
- otf-思源黑体 10120→10100 (-20B)
TTF 路径 fonteditor-core 已做此压缩(思源/霞鹜/FiraCode numHM=1),
OTF 路径手写 buildSubsetMetrics 漏做,属 TTF/OTF 对称补齐(模式③)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 13:37:17 +08:00 |
|
崮生(子虚)
|
ade5d279a0
|
fix(gsub): emitCoverage format 选择改精确字节比较(原判据过宽)
emitCoverage 选 format1(list) vs format2(range) 的判据从 rangeCount < n
改为精确字节比较 rangeCount*6 < n*2(即 rangeCount < n/3),与 subsetGPOS
emitCoverageFromGids 一致。
format1=4+n*2,format2=4+rangeCount*6。原判据 rangeCount<n 在 rangeCount
∈ [n/3, n)(中等长度 range,avg 1~3 gid/range)时误选 format2 致体积更大。
coverage 的 format1/format2 是等价表达(同一 gid 集合),选 format 不影响渲染。
体积(30 用例 SSIM 全不变,总体积 -168B):
- FiraCode woff2 -64B / 初夏纯标点 -32B / otf-思源 ttf -20B。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 13:17:49 +08:00 |
|
崮生(子虚)
|
1adbf2e0d1
|
refactor(gsub): unsupported lookup 也折叠 subCount=1(与 GPOS/supported 对称)
GSUB unsupported lookup(type5 ReverseChain 等)原逐 subtable 写空 subtable,
现折叠 subCount=1 单空 subtable,与 supported 分支(优化337) + subsetGPOS 优化330 一致。
N 个空 subtable 与 1 个渲染语义等价(都查不到 coverage 跳过)。
基准字体无 unsupported lookup(type5 罕见),30 用例 SSIM+体积完全不变,
此为模式③对称补齐,对未来含 type5 的字体严格更优。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 13:15:16 +08:00 |
|
崮生(子虚)
|
665a089019
|
perf(gsub): 全空 lookup 折叠 subCount=1(补齐 GPOS 优化330)
GSUB supported lookup 全空(allEmpty)时,把 N 个空 subtable 折叠为 subCount=1
的单个空 subtable,与 subsetGPOS 优化330 同类。feature 仅按 lookup index 引用,
subCount 改变不影响 feature;浏览器遍历 subtable 查 coverage,单个空 subtable
与 N 个全空 subtable 渲染语义等价(都查不到字形跳过)。
与 gsub-lookup-deletion-failed-fira 区别:不删 lookup(index 不变、feature 不受影响),
仅把全空 lookup 内的 N 个空 subtable 折叠为 1 个。
体积(30 用例 SSIM 全不变,总体积 -4520B):
- otf-思源黑体 ttf -3204B / woff2 -532B(GSUB 全空 lookup 多)
- 初夏纯标点 woff2 -520B / FiraCode woff2 -124B
FiraCode SSIM 0.9923 不变(连字字形完整保留,ink 3856/3695 一致)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 13:13:07 +08:00 |
|
崮生(子虚)
|
2a224b1df7
|
refactor(cff): origFd→privInfo 查找改 fdRemap 下标 O(1)
subsetCFF 重建 CharStrings 循环里,原用 usedFds 线性扫描找 origFd 对应的
privInfo(O(glyphs×FDs))。改为复用已有的 fdRemap(origFd→newFdIndex) 直接
下标取 fdInfos[fdIdx].priv(O(1))。gidOrigFids 与 fdRemap 同源构建,每个 fd
必命中;缺失则 null 走透传(与原线性扫描未命中等价)。
字节一致(思源/白狐 3 OTF 用例 sha 全同)。基准无性能收益(思源 usedFds 仅
1-2 个 FD,线性扫描 break 在第一步),纯清理 + 对未来多 FD CID 字体严格更优。
otf-subset.ts: buildSubsetGids 补充 gidToNewGid Map vs Int32Array 边界判据注释
(CID 字体 numGlyphs=65535 致 fill 成本远超 Map 低频查询),防未来重复尝试。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 12:54:41 +08:00 |
|
崮生(子虚)
|
cda0215b36
|
perf(otf): woff2 输出跳过 OTF sfnt checksum 计算
assembleSfnt 在 OTF→woff2 路径原总计算各表 checksum(calcTableChecksum 遍历每表全部字节,CFF 是大头)+ 目录 dirSum + head.checkSumAdjustment 回填。但 encodeTTFToWOFF2 解析 sfnt 目录只读 tag/offset/length(line 742-748 不读 off+4 checksum),也不消费 adjustment,故 woff2 输出时这些全是浪费。
与 [[woff2-skip-ttf-checksum]] 的 TTFWriter.skipCheckSum(font.js:175 woff2 分支早做)完全同类,OTF 侧原遗漏,本轮补齐。裸 OTF 输出(otf/ttf)仍算 checksum(规范要求)。
otf-思源黑体 woff2 avg 1.2→0.7ms,所有 OTF woff2 case SSIM 不变(1.0000),TTF/裸 OTF 用例不受影响。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 09:54:44 +08:00 |
|
崮生(子虚)
|
51c9f81472
|
fix(cmap): BMP 字符不再写冗余 format12 subtable
hasGLyphsOver2Bytes 原赋值 len>0(任何非空子集都 true),与变量名语义(>0xFFFF 字符才需
format12)相悖。BMP 字体的 format12 与 format4 内容完全相同(均覆盖 BMP),纯冗余。
改用真实 hasOver2Bytes 判定(getSegmentsFlat 产出段检查 >0xFFFF)。
fonteditor 原始逻辑本就如此(41c2741 some(glyph.unicode>0xFFFF)),后续性能优化误改。
体积收益(woff2):千字文 -396B、思源千字文中段 -480B、思源8字 -52B、标点 -56B、
FiraCode -32B、令东8字 -64B。29 基准用例 SSIM 全通过且与历史固有值完全一致
(浏览器用 format4 渲染 BMP 足够,format12 仅 >0xFFFF 必需)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 08:52:30 +08:00 |
|
崮生(子虚)
|
01f920f3f0
|
perf(gsub): currentSortedSubsetGids 延迟构造,无 format2 ClassDef 时省排序
sort 仅 ClassDef format2 / coverage range 路径需要,原入口点无条件 Array.from(origToNew.keys()).sort()。
改首次访问时惰性构造(getter),无 format2 ClassDef 的字体(令东无 GSUB / 霞鹜/初夏 GSUB 多为 format3)永不算。
小字集收益不可测(sort <70μs),全字符集场景省 ~3.2ms 固定开销。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 08:33:26 +08:00 |
|
崮生(子虚)
|
7b3b0ef592
|
perf(otf): buildSubsetGids 用 gidToNewGid Map 替代 indexOf O(n) 查找
subsetOTF 的 buildSubsetGids 原 `cpToNewGid.set(cp, subsetGids.indexOf(gid))`
对每个 codepoint 线性遍历 subsetGids 找索引,O(N×M),全字符集场景退化为 O(N²)。
改用 gidToNewGid Map(gid→新gid)在 push 时记录,O(1) 查找;同时替代 seenGid
Set(gidToNewGid.has 等价)。
基准测试 OTF 用例(小字集)SSIM 全一致、输出字节 IDENTICAL(otf-五个汉字
26536/15024、otf-思源 19188/10664、otf-白狐千字文 35896)。算法缺陷修复,
大字集 OTF 子集化(完整 CJK OTF)有实质收益。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 08:23:01 +08:00 |
|
崮生(子虚)
|
71105374ee
|
perf(gsub/gpos): lookup 偏移槽改 writeUint16 占位 + writeInt16At 回填
subsetGSUB/subsetGPOS 的 lookup 偏移槽(lookupCount 个)与 gsub unsupported
分支的 subtable 偏移槽,从 reserveOffset16 闭包模式改为 writeUint16(0) 占位 +
记录 slot 起点 + 序列化后 writeInt16At 回填——与已做的 subtable 槽优化329、
writeChainFormat3 coverage 槽(gsub-writechainformat3)统一。
消除 lookupCount 个 per-lookup 闭包 + patch 对象分配。思源 GSUB 56 lookup、
FiraCode/霞鹜同。gsub unsupported 分支 subtable 槽也随之统一(原仅 supported
分支用优化329)。
收益不可测(FiraCode min 1.8346 vs 1.8439、思源 1.505 vs 1.479 均噪声内)——
lookupCount 级闭包非瓶颈,被 brotli/glyf transform 淹没。但消除语义冗余 +
统一两侧偏移槽写法。基准测试全 SSIM 一致、输出字节 IDENTICAL。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 08:05:55 +08:00 |
|
崮生(子虚)
|
67d2eb5fa5
|
perf(woff2): ttf→woff2 跳过 TTF checksum 计算
woff2 重新编码,不消费 TTF directory 的 per-table checksum 与
head.checkSumAdjustment(encodeTTFToWOFF2 只读 directory 的 tag/offset/length,
重建自己的 directory)。TTFWriter 加 skipCheckSum 选项,font.write 的 woff2
分支传入,跳过 checkSumArrayBuffer 调用 + directory 回填 + head adjustment。
node --prof 思源 3500 字大字集:checkSumArrayBuffer 5.1%(JS nonlib 第二大)
从热点列表消失。端到端 min -2%(brotli 主导稀释收益),但消除的是语义上
纯无用的计算(被 woff2 丢弃)。head.checkSumAdjustment 保持 0 占位(resolveTTF
已置 0),浏览器渲染不依赖该字段。
ttf 直出(消费 directory checksum)与 woff(ttf2woff 读 directory checksum
写 WOFF 表目录)分支不受影响,仅 woff2 生效。
基准测试全 SSIM 与历史固有值一致(FiraCode 0.9923/初夏纯标点 0.9920/篆体
0.9876 等),woff2 输出字节 IDENTICAL(思源千字文中段 17052/FiraCode 8892/
霞鹜 3840/初夏 6164/6864/得意黑 2532/1628)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 08:02:11 +08:00 |
|
崮生(子虚)
|
2ed666ec58
|
perf(gsub): parseChainFormat3 coverage/records 数组按 count 预分配
arr: number[][] 与 records 对象数组原用空数组 + push,fmt3 高频路径(每 subtable
三组 coverage + records)触发 GrowFastSmiOrObjectElements。改按已知 count/seqCount
预分配容量 + 下标赋值。
node --prof GrowFast 4.3%→3.6%、ArrayPrototypePush 3.7%→2.5%。FiraCode 输出字节
不变(acc 一致),29 用例 SSIM 全通过。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 07:42:40 +08:00 |
|
崮生(子虚)
|
98102bc3be
|
perf(gsub): writeChainFormat3 coverage 槽改 writeInt16At 回填
type6 format3 的 coverage 偏移槽原用 allHolders: number[][] + reserveOffset16
闭包模式(每槽 [0] 单元素数组 + patches push + flush 回填)。改为 writeUint16(0)
占位 + covSlotPositions 记录字节位置 + writeInt16At 直接回填(与优化329 subtable
slot 同思路),消除每槽的 [0] 数组分配 + reserveOffset16 闭包。
node --prof 显示 GrowFastSmiOrObjectElements 3.7% + ArrayPrototypePush 3.2%
主来自 writeChainFormat3/serializeChainedContextSubst 的 allHolders grow。
FiraCode subsetGSUB min 0.229→0.216ms(-5.7%),端到端 avg 3.4→2.9ms(-15%),
29 用例输出字节全相同 SSIM 一致。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 07:39:09 +08:00 |
|
崮生(子虚)
|
e8989c7fa4
|
perf(cff): rewriteCharstring 分支按热度重排
与 collectSubrRefs 同构的字节分派,operand(b0>=32)提前到首判定。思源千字文
subsetCFF 0.4920→0.4819ms(-2.1%),CFF 输出 sha 一致。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 07:27:00 +08:00 |
|
崮生(子虚)
|
ca34a9e648
|
perf(cff): collectSubrRefs 分支按热度重排
operand(b0>=32)占 CJK charstring 绝大多数字节,提前到首判定。旧顺序按字节值
升序(255→28→29→32-246),最热的 32-246 单字节 operand 要先 3 次比较不命中才进入;
现 b0>=32 一次比较命中热路径。
思源千字文 subsetCFF 0.5137→0.4884ms(-4.9%),CFF 输出 sha 一致(纯分支顺序调整)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 07:25:37 +08:00 |
|
崮生(子虚)
|
d2b3b1939f
|
perf(cff): 跳过无 CALLSUBR 字形的 rewriteCharstring
collectSubrRefs 返回是否含 CALLSUBR,subsetCFF 据此标记每个字形。重建 CharStrings
时仅含 CALLSUBR 的字形走 rewriteCharstring(重映射 local subr 编号),无 CALLSUBR 的
直接透传原字节——含 CALLGSUBR 但无 CALLSUBR 的透传等价(global subr 不子集化,
operand 原样)。
思源千字文 241 字形仅 65(27%)含 callsubr,176 个无 callsubr 字形原先白白跑一遍
rewriteCharstring 完整字节扫描。subsetCFF 0.74→0.51ms(-30%)。
三 OTF case CFF 输出 sha 全 IDENTICAL(纯等价变换)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 07:17:12 +08:00 |
|
崮生(子虚)
|
7a76208558
|
perf(gsub): coverageAllOutOfSubset range 相交判定改二分
COV_RANGE 分支逐 gid 展开 range 查 gidLookup(O(range length)),
对全子集外的 coverage 要遍历完所有 gid 才确认 outOfSubset。初夏 fmt3
range 展开总 20388 gid(300 range / 12 子集 gid),逐 gid 是预扫描主开销。
改用 currentSortedSubsetGids 二分:range [start,end] 是否含任一子集 gid
等价于排序子集数组存在 gid ∈ [start,end](gidLookup[g]>=0 ⟺ g 是子集 gid)。
O(log n) per range。微基准 10×(0.012→0.001ms),subsetGSUB 端到端初夏 -6%。
等价性:end clamp/越界语义保留(sortedGids 全 <numGlyphs,二分天然限范围);
COVERAGE_MAX_EXPAND 守卫保留(与 readCoverageRemapped origNonEmpty 一致);
origNonEmpty 对所有有效 range 非空标记(含越界 range,原版同等价)。
字节一致性:11 用例(含 FiraCode 两文本)subsetGSUB 输出 sha 全 IDENTICAL。
SSIM 30 用例全通过(最低 0.9876 篆体固有)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 07:01:35 +08:00 |
|
崮生(子虚)
|
6d224c2405
|
perf(gpos): writeEmptyPosSubtable 空 subtable coverageOff 直接写常量
supported lookup 未命中 subtable(初夏 577 个)走空路径,原用
reserveOffset16(subStart, () => coverageAfter) 写 coverageOff——但 SinglePos/PairPos/
Cursive 的 coverageOff 是相对 subStart 的编译期固定常量(6/10),无需 patch 回填。
改直接 writeUint16(常量),省 per-subtable 的 patch 对象 + 闭包分配 + flush 遍历回填。
字节一致性:8 字体 GPOS 输出 sha 全 IDENTICAL。
性能 best-of 6000(min):初夏纯标点 0.0354→0.0303(-14%)、初夏汉字标点 -14%、
FiraCode -14%、霞鹜/得意黑 -3~5%;思源/鸿蒙噪声级(绝对 <0.0003ms)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 06:46:55 +08:00 |
|
崮生(子虚)
|
bda73ccea8
|
perf(gsub-reachable): format3 失败快判内联主循环,省完整 collectChainRefs 调用
初夏纯标点 280 个 format3 首轮全失败(276 个 backtrack 段为空,失败在 input 段),
FiraCode 207 个 format3 中 121 个 backtrack 首 gid 即不在 reachable。原实现每个失败
format3 都要走完整 collectChainRefs(函数调用 + refsReuse/ctxGidsReuse.clear +
format3 分支进入 + coverageFirstExcludedGid),是首轮固定开销。
format3 触发需 backtrack/input/lookahead 三段全部 gid 在子集,故任一非空段的首个
coverage 首 gid 不在 reachable 即可判定失败。内联读 format + 依次查三段第一个非空段
首 gid,命中失败则直接记 failGidMap 跳过 collectChainRefs。
字节一致性:5 字体 reachable sha 全 IDENTICAL。
性能 best-of 5000(min):初夏 0.0557→0.0435(-22%)、FiraCode 0.0999→0.0903(-10%)、
霞鹜 -3%、得意黑 -7%、鸿蒙 -8%。快判未命中走完整路径(含 errorFlag 安全网),越界保护
subAbs+6/covOff+6 ≤ byteLength,损坏表降级 collectChainRefs。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 06:41:02 +08:00 |
|
崮生(子虚)
|
0cfd9851df
|
perf(gpos): subsetGPOS 全模块 origToNew(Map)→gidLookup(Int32Array)
套用 gsub 侧已验证的 Int32Array gidLookup 模式到 GPOS:
- subsetGPOS 入口构建 gidLookup(动态容量 maxOrigGid+1,fill -1)
- remapGid 的 Map.get(7处)+ subtableCoverageHits 的 Map.has 改数组索引
- 涉及函数签名全部 origToNew:Map → gidLookup:GidLookup
A/B 11 字体字节全 IDENTICAL;基准测试 SSIM 全通过。
性能 best-of N=2000:
- 思源千字文中段 min -2.6%
- 思源标点 min -2.5%
- 千字文96 min -1.2%
- FiraCode 持平(GSUB 已用 gidLookup,GPOS 增量小)
关键:gidLookup[越界] 返回 undefined(非 -1),GPOS coverage 常引用
大于子集 maxGid 的 gid,remapGid 须 origGid < length 保护(否则
undefined < 0 = false 误判有效,同 gsub type234 undefined bug)。
容量必须动态,65536 固定容量 fill 在小 GPOS 用例倒贴(篆体 +12.5%)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 05:36:49 +08:00 |
|
崮生(子虚)
|
ae41a99e95
|
perf(gsub): serialize type2/3/4 大 coverage 反转遍历 + 修越界 undefined bug
serializeMultipleSubst/serializeAlternateSubst/serializeLigatureSubst 原实现
readCoverageGids 全量展开 coverage 再逐个查 gidLookup,与 serializeSingleSubst
([[gsub-serialize-single-bigcov-reverse]] 早修)同类的 serialize 侧浪费。
probe 实测:白狐千字文 type3 coverage=1000、初夏 type3=329/type4=97、霞鹜 type4=94,
但子集仅命中个位数。CJK 字体(令东/思源 TTF+OTF)type2/3/4 全无调用。
优化:套用 serializeSingleSubst 的反转模式——coverageCount 判大小,coverage 远大于
子集(covGidCount > subsetLen*4 && >16)时遍历 currentSortedSubsetGids 用
coverageIndexOf 二分定位 coverage 下标 idx(即 set 偏移数组下标),替代全量展开。
附带修复原版越界 bug:原版 gidLookup[covGids[i]] 当 covGids[i] 超出 numGlyphs
(损坏 coverage,白狐实测含)返回 undefined,`undefined < 0` 为 false(NaN 比较)漏过,
产生 from=undefined 的非法 AlternateSet entry(emitCoverage 写无效 gid)。
两路径统一改 `!(fromNew >= 0)`(undefined>=0 也是 false → 跳过),与反转路径行为一致。
区别于 reachable 侧 readCoverageRemapped 早有 numGlyphs 越界保护(line 221-261)。
A/B:初夏/霞鹜千字文(type3/4 触发反转)字节与基准线 IDENTICAL;白狐千字文字节变小
(-12B ttf/-16B woff2,即去掉的非法 entry)。
基准测试新增「otf-白狐千字文」用例验证:SSIM=1.0000 ink=19735/19735(渲染像素级一致,
证明去掉的是浏览器本就忽略的无效 gid entry)。
性能 best-of N=2000:白狐千字文 min -5.4%/p10 -6.7%、初夏 -5.1%/-3.9%、霞鹜 -5.6%/-3.5%;
令东千字文(无 type2/3/4)持平。29 用例全 SSIM PASS。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 04:58:34 +08:00 |
|
崮生(子虚)
|
01b7ee432d
|
perf(ot-bytes): toUint8Array 返回 subarray view 省去 size 字节 memcpy
原 new Uint8Array(buf.subarray(0,size)) 会把已写入字节再拷贝到新 buffer,
而所有调用方(serializeScriptList/FeatureList、subsetGPOS/GSUB 主 writer、
pairSet writer)都是只读消费(writeBytes.set / 存数组后读),toUint8Array 后
writer 不再写入,故直接返回 subarray view(零拷贝共享 buf)完全等价。
24 产物(12 字体 × ttf/woff2)A/B 逐字节 IDENTICAL。
性能 best-of N=3000:霞鹜文楷 p10 -3.2%、初夏明朝 p10 -3.0%、思源OTF p10 -1.6%、
千字文持平(小子集 list/subtable 多、toUint8Array 调用频繁场景收益明显)。
基准测试全 SSIM 通过。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 04:39:22 +08:00 |
|
崮生(子虚)
|
e1190eeb82
|
perf(woff2): transformGlyfAndLoca simple glyph 跳过 bbox 读取
原每个 glyph 入口都读 numberOfContours + xMin/yMin/xMax/yMax 共 5 次 readI16,
但 simple glyph 的 bbox 由解码端从轮廓点重建(优化314,bboxBitmap 位恒为 0,
不写入 bboxStream),xMin/yMin/xMax/yMax 读取纯属浪费。仅复合 glyph 需显式存
bbox(raw 组件数据剥离 glyph 头,解码端无法重建)。
将 bbox 4 次 readI16 移入复合 glyph 分支(numberOfContours<0),simple glyph
路径只读 numberOfContours 一次。CJK 字体几乎全 simple,每字省 4 次 readI16。
7 产物 A/B 全 IDENTICAL。性能 best-of min N=4000:
篆体 -4.6%、思源8字 -2.3%、FiraCode -1.7%、令东8字 -1.4%、千字文 -1.1%。
基准测试全 SSIM 通过。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 03:59:53 +08:00 |
|
崮生(子虚)
|
32afa17b5c
|
perf(woff2): transformGlyfAndLoca x/y 直接用 TTF delta 编码 triplet
原 x 解码循环把 TTF delta(相对前点的有符号差)累积成绝对坐标 px,
y+triplet 循环又用 cx-prevX / cy-prevY 把绝对坐标还原回 delta——无意义的
往返计算。因 TTF xCoordinates/yCoordinates 本就是 delta,triplet 的 dx/dy
语义 = 当前点与前点坐标差 = TTF delta 本身,可直接用:
- x 循环存 delta 到 xCoords(省 px 累积)
- y+triplet 循环 dx=xCoords[yi]、dy=解码出的 y delta(省 py 累积 + cx-prevX
+ cy-prevY + prevX/prevY 维护,每点省 4 次 ALU)
case 分支逻辑不变(delta=0 ⟺ 绝对差=0),输出逐字节一致。
8 产物 A/B 全 IDENTICAL(含 ttf/otf 路径)。性能 best-of min:
千字文 woff2 -2.4%(1.56→1.52ms)、令东8字 -2.8%、FiraCode -1.1%。
基准测试全 SSIM 通过。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 03:54:54 +08:00 |
|
崮生(子虚)
|
1bc87763ad
|
perf(gsub): fmt3 预检改用 coverageAllOutOfSubset 内联快判,跳过 readCoverageRemapped
isSubtableSkipableByCoverage 的 ChainContext format3 分支原对每个 coverage 调
readCoverageRemapped(判全子集外)——它 new Array+sort+Map.set 填 covCache,但思源
fmt3 预检 281 个 coverage 全子集外且 avg 仅 1.3 gid:数组分配/sort/cache 操作是纯固定开销,
且这些 skipable coverage 的 cache entry 永不被命中(skipable subtable 不深度解析)。
改用 coverageAllOutOfSubset 内联遍历字节判「全子集外」(不分配数组、不读写 covCache),
预检只判 skipable 不再填 cache;非 skipable subtable 深度解析时 readCoverageRemapped
自行首次填 cache(命中率不变,无双重遍历)。同时从 isSubtableSkipableByCoverage 移除
不再使用的 covCache 参数。
一致性([[gsub-subset-fmt3-prefetch-consistency]]):对齐 coverageAllOutOfSubset 的
COV_RANGE 分支与 readCoverageRemapped——end clamp 到 numGlyphs-1、start>=numGlyphs
的越界 range 计 origNonEmpty 不展开 gid。两者「全子集外」判定完全等价,故预检 skipable
⟺ 深度解析 coverage 失效,输出逐字节一致。
A/B:6 字体 × ttf/woff2 共 12 产物逐字节 IDENTICAL(含 FiraCode calt)。
性能(best-of-min,两轮一致):otf-思源 -12%、思源ttf -6%、FiraCode -2%、初夏 -3%。
基准测试全通过,SSIM 维持。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 03:32:15 +08:00 |
|
崮生(子虚)
|
49337c72bc
|
perf(gsub): 缓存 isSubtableSkipableByCoverage per-subtable 结果,serialize 复用
subsetGSUB 预扫描对每个子表调 isSubtableSkipableByCoverage(判全空),serialize 阶段
serializeSubtable 内部对同一子表再调一次——重复。预扫描原 allEmpty=false 时 break 早退,
仅记录 lookup 级 allEmpty 布尔,serialize 无法复用逐子表结果。
改为预扫描完整扫描填 subtableSkipable: boolean[](与 subtableAbsOffs 同序),serializeSubtable
加 preCheckedSkipable 参数,serialize 主循环传 lk.subtableSkipable[j] 跳过内部重复预检。
allEmpty 从数组派生(全 true)。
安全性依赖 [[covCache 修复]](上一提交):isSubtableSkipableByCoverage 对 fmt3 调
readCoverageRemapped,covCache 顺序依赖 bug 已修复后结果真正顺序无关,故完整预扫描
改变填充顺序不再触发 FiraCode calt 输出变化。
A/B:6 字体(思源/白狐/令东/思源ttf/FiraCode/初夏)× ttf/woff2 共 12 产物逐字节 IDENTICAL。
性能(best-of-min):otf-思源 -9.2%、FiraCode -2.9%、初夏纯标点 -3.8%、白狐 -2.0%、
思源ttf -1.3%、令东持平(无大 GSUB)。基准测试全通过,SSIM 维持。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 03:20:05 +08:00 |
|
崮生(子虚)
|
d0dbbece82
|
fix(gsub): readCoverageRemapped 命中 readCoverageGids 的 cache entry 时正确重映射
covCache 被 readCoverageRemapped(fmt3 用,产 newGids)与 readCoverageGids
(type1/2/3/4 用,需原始 gid)共享同一 CoverageCacheEntry。readCoverageGids 填的
entry remapped=null/outOfSubset=false,readCoverageRemapped 命中时原直接
`return entry.outOfSubset ? null : entry.remapped` 会返回 null——null 被当作
「outOfSubset」语义,误判 coverage 失效。
修复:命中 entry 时若 gids!==EMPTY_GIDS(readCoverageGids 填的,非 readCoverageRemapped
自己的占位),从已缓存的原始 gids 现场重映射(gidLookup 过滤),并回填 remapped/outOfSubset
供后续命中。仅 readCoverageRemapped 自己的 entry(gids===EMPTY_GIDS)才走原快路径。
此为潜在顺序依赖隐患:改变 covCache 填充顺序(如预扫描完整扫描)会暴露它致 FiraCode
calt 输出变化。修复使 covCache 真正顺序无关。
A/B:思源/白狐/令东/思源ttf/初夏 逐字节 IDENTICAL;FiraCode ttf +32B(一个被误判空的
fmt3 subtable 现正确序列化)、woff2 不变(brotli 压掉)。基准测试全通过,FiraCode SSIM
维持 0.9923(多保留的规则不影响 => !== 渲染)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 03:18:19 +08:00 |
|
崮生(子虚)
|
a87f2eb0ff
|
perf(cff): rewriteCharstring 用标量替代 stackStart[]/stackVal[] 两数组
对栈的所有读取仅需栈顶:HSTEM 用 stackLen 算 stemCount,CALLSUBR 用
lastVal 取调用编号、用 lastStart 截断回栈顶 operand 起始处重写。其余
operand 的值/位置从不被读取,故 stackStart/stackVal 两数组可标量化为
stackLen+lastVal+lastStart 三标量(与 collectSubrRefs 同构),消除每个
operand 的 push 装箱 + length=0 重置。
A/B:otf 思源/白狐 × ttf/woff2 共 4 产物逐字节完全一致。
性能:subsetOTF 思源 min 0.266→0.235ms(-11.6%)。
基准测试全通过,SSIM 与基线一致。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 03:03:54 +08:00 |
|
崮生(子虚)
|
94f89ddbb8
|
perf(cff): collectSubrRefs 用 stackLen+lastVal 替代 stack:number[]
仅需栈长度(HSTEM 算 stemCount)与栈顶值(CALLSUBR/CALLGSUBR 取调用编号),
无需完整栈数组。消除每 operand 的 push(装箱)与 length=0 重置。
otf-思源8字 subsetOTF min 0.319→0.297ms(-7%),p50 0.401→0.390ms。
输出逐字节相同。与 rewriteCharstring/replaceDictOffsets 累计 otf-思源 -19%。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 02:53:41 +08:00 |
|
崮生(子虚)
|
efe24d9312
|
perf(cff): rewriteCharstring/replaceDictOffsets 用 Uint8Array+写指针替代 number[]
rewriteCharstring(18.7%) 与 replaceDictOffsets(7.8%) 原 out/chunks: number[] 逐字节 push
有装箱开销 + new Uint8Array(out) 二次遍历 / Array.from+spread 多次拷贝。改预分配 Uint8Array
+ 写指针 wp(CALLSUBR 截断即 wp=stackStart[]),encodeDictInt 内联到写入处消除临时 number[]。
otf-思源8字 subsetOTF min 0.352→0.322ms(-9%)、p50 0.483→0.421ms(-13%);
otf-白狐5字 min 0.058→0.055ms。输出逐字节完全相同(19188B/26536B A/B 一致)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 02:52:13 +08:00 |
|
崮生(子虚)
|
db33685475
|
perf(gsub): serializeSingleSubst 大 coverage 反转遍历,思源 -63%
思源 GSUB 56 lookup 中 4 个非空 type1 lookup(locl 地区变体)的 coverage
极大(lookup#43 = 8881 gid,#0=729,#42=636),但子集仅命中个位数。
原 serializeSingleSubst 用 readCoverageGids 全量展开 coverage 再逐个查
gidLookup(8881 次几乎全未命中),占 subsetGSUB 84%(0.184ms/call)。
改为遍历子集原始 gid(currentSortedSubsetGids,仅 19 项)+ coverageIndexOf
在 coverage 中二分定位下标(format2 下标对应 substituteGlyphIDs 数组),
O(subsetSize × log covCount) 替代 O(covCount)。与 reachable 侧
[[gsub-reachable-bigcov-byte-bsearch]] 同一类「展开全量消费极少」浪费。
仅当 covGidCount > subsetSize×4 且 >16 时启用反转,短 coverage(FiraCode
等)走原路径避免二分开销。导出 coverageIndexOf/coverageCount 供复用。
subsetGSUB 思源 min 0.204→0.075ms(-63%),otf-思源 woff2 avg 2.7→1.0ms。
输出逐字节一致(思源 GSUB 6560 bytes 差异 0),全字体 SSIM 不变。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 02:00:05 +08:00 |
|
崮生(子虚)
|
baa3a026ed
|
refactor(gsub): 去除 subtableAbsPositions 中间数组
优化329 已用 writeInt16At 直接回填,subtableAbsPositions 数组仅为
中间存储起点供回填使用,改为直接用 w.length(allEmpty)/ before(else)
计算相对 lookupStart 偏移。纯重构,输出字节逐字节一致。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-25 01:39:10 +08:00 |
|