231 Commits

Author SHA1 Message Date
崮生(子虚)
09f434bbcf feat: 离线裁剪页面 + 增量预览 + DEV预览页 + tempUploads统计 + SDK缓存破坏 + UI优化
- 新增离线裁剪页面 /offline-subset(纯浏览器端字体裁剪)
- SDK subsetProvider 抽象,离线页面注入本地裁剪 provider
- 预览用增量裁剪(SDK自动去重),下载用全量裁剪
- 拖放区域支持点击和拖拽选择字体文件
- 新增 DEV 预览页 /__dev(仅 dev 模式路由可达)
- FontDebugPreview 从首页提取为独立页面
- tempUploads 统计字段(全链路+持久化)
- webfont-sdk.js 构建时戳缓存破坏
- 首页导航栏新增离线裁剪入口
- UploadSection 离线裁剪入口合并到游客上传卡片
- SDK 文档代码示例补充 HTML 用法
- 导航栏窄屏换行适配
2026-08-03 15:18:01 +08:00
崮生(子虚)
5a7c6a9913 fix: LLRT readdir 不兼容 withFileTypes 导致临时字体清理失效
根因:LLRT 的 fs.readdir 返回字符串数组而非 Dirent 对象,
entry.isFile() 抛 TypeError 被 catch 静默吞掉,
导致 evictIfNeeded 和 temp_cleaner 从未执行(线上堆积 108 个文件 992M)。

修复:
- node.ts: readdir 改为先拿文件名再 stat 判断
- temp_cleaner.ts: catch 块加日志不再静默吞错误

同时包含:
- font-config.json homepage 链接修复(删除 3 个 404,修正 2 个)
- FontDetail 字符预览改为可实时编辑
- 添加 scripts/bench.mjs 压测脚本
2026-08-01 08:03:33 +08:00
崮生(子虚)
1979d7c81e fix: FontMeta 接口补上 metaVersion 字段声明 2026-07-31 00:11:23 +08:00
崮生(子虚)
fa3fa15bf7 feat: font-meta 并发限制 + metaVersion 磁盘缓存失效 + 清理废弃参数
- font-meta 首次计算复用 withMemoryGate 并发闸门(与子集化共享内存预算)
- FontMeta 加 metaVersion 版本指纹,算法变更后 .meta.json 自动失效
- 删除 withMemoryGate/initMemoryGate 废弃的 _softLimitMB 参数
- 清理旧 .meta.json 缓存文件
2026-07-31 00:10:25 +08:00
崮生(子虚)
98c3c5a6e2 fix: 路由切换后滚动到顶部
详情页→列表→另一个详情页时滚动位置保留在前一页的偏移,
用 router.afterEach 统一处理,每次导航回到顶部。
2026-07-31 00:06:05 +08:00
崮生(子虚)
3e3e8899d5 fix: 修复字体详情页字体未应用——@font-face 替代 link stylesheet
根因:API 返回二进制 woff2 字体文件,但前端用 <link rel="stylesheet">
引用它,浏览器无法将 woff2 二进制作为 CSS 解析,导致字体完全未加载。

修复:
- 用 watchEffect + document.createElement('style') 动态注入 @font-face
  声明,通过 src: url(...) 引用 API 返回的 woff2 二进制
- 修正 usageCode 示例代码,展示正确的 @font-face 用法
- 添加 SSG/SSR 守卫(typeof document === 'undefined' 跳过)

验证:document.fonts.check() 返回 true,字体 status 为 loaded
2026-07-31 00:03:33 +08:00
崮生(子虚)
6421e7d059 fix: extractCodePoints 增加 glyph 轮廓验证 + 删除冗余正文预览卡片
- font_meta.ts: extractCodePoints 新增 createGlyphValidator,对 TrueType 字体
  检查 loca 表确认 glyph 有真实轮廓数据(非空),CFF 字体保持原逻辑
- lint 同步去掉 bodyTitle/bodyText(FontDetail 已移除正文预览卡片)
- FontDetail.vue: 删除正文预览区块(与大标题预览功能重叠),移除对应 computed
- allPreviewText 只合并 previewText + charsetPreview
2026-07-30 23:50:27 +08:00
崮生(子虚)
afbd4db77c fix: 字体加载合并所有预览文字(previewText+bodyTitle+bodyText+charsetPreview)
之前 <link> 只加载 previewText 的字符子集,导致 bodyTitle/bodyText/charsetPreview
区域显示 fallback serif 而非实际字体。现在 allPreviewText 合并所有预览文字请求子集。
2026-07-30 23:43:08 +08:00
崮生(子虚)
7638ce7e2d fix: 移除 FontDetail fallback 默认值 + lint 检查缺失字段 + 使用 CodeBlock 组件
- FontDetail.vue: 移除 DEFAULT_PREVIEW 等硬编码 fallback,config 缺字段时不渲染对应区块(避免显示字体不支持的字符)
- lint-font-config.ts: 新增检查 font/ 目录下所有字体是否都在 config 中有完整配置(previewText/bodyTitle/bodyText/charsetPreview 四个必填字段)
- FontDetail.vue: 使用方法代码块从 <pre><code> 改为 CodeBlock 组件(语法高亮)
2026-07-30 23:37:18 +08:00
崮生(子虚)
a095cd0590 feat: 详情页全面接入 font-config + 防闪烁
- font-config.json: 新增 bodyTitle/bodyText/charsetPreview 字段
  为每个字体配置通过 lint 检查的专属文案
- FontDetail.vue: 正文/字符预览使用 config 字段替代写死文案
  大标题/正文/字符预览加 v-if=meta 守卫,消除 hydrate 闪烁
  h1 标题使用 displayName
- lint-font-config.ts: 扩展检查范围至所有文本字段
- FontUserConfig 类型同步更新(后端+前端)
2026-07-30 23:30:40 +08:00
崮生(子虚)
9397e10aad feat: 字体列表/详情页接入 font-config 配置 + lint 脚本
前端:
- FontList.vue: 卡片使用 router-link 跳转;
  展示 displayName/description/previewText/tags;
  底部添加字体投稿联系文案
- FontDetail.vue: 预览文字使用 config.previewText 替代写死文案

工具:
- scripts/lint-font-config.ts: 检查 previewText 字符是否被字体支持
- package.json: 添加 lint:font-config 脚本
2026-07-30 23:23:30 +08:00
崮生(子虚)
23728ba02b refactor: 回退内存闸门为固定并发(保留分组调度) + 前端改进
后端:
- subset_queue.ts: 移除基于内存的门控(LLRT GC 不释放 RSS),
  回退到固定并发模型,保留分组调度优化(相同字体批处理重用缓存)
- config.ts: 恢复 subsetConcurrency / subsetQueueTimeoutSeconds,
  移除 subsetMemSoftLimitMB
- routes/subset.ts, app.ts, routes/config.ts: 同步恢复
- api.ts, README: 同步更新

前端:
- FontSelector.vue: 下拉项和触发器标记临时字体(橙色徽章)
- CodeBlock.vue: 新增轻量代码块组件,内置 CSS/HTML/JS 分词高亮(0依赖)
- Home.vue: 集成 CodeBlock 替换原始 <pre> 代码块
2026-07-30 23:11:54 +08:00
崮生(子虚)
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