240 Commits

Author SHA1 Message Date
崮生(子虚)
64b8bcdbe8 chore: 恢复 doc 下技术截图跟踪,gitignore 收窄为仅 doc/*.md 2026-08-15 16:21:45 +08:00
崮生(子虚)
bbc5d6f8b1 feat(demo): 全 CDN 化——leafer-ui/export/插件/SDK 全走 jsDelivr,服务地址本地/线上自适应 2026-08-15 16:18:49 +08:00
崮生(子虚)
ec5740c266 chore(packages): 补齐两包 npm 发布元数据(LICENSE/README/types 路径修正/author/repository) 2026-08-15 16:09:30 +08:00
崮生(子虚)
8d53425483 chore: 移除 doc/ 目录跟踪(商业化文档不入库) 2026-08-15 16:06:33 +08:00
崮生(子虚)
a0c2b4aafc feat: webfont-sdk Node 模式 + leafer 版基准测试(双方案并存)
- webfont-sdk: 新增 Node 模式(node-registry),GlobalFonts 注册 + 每 chunk 唯一 family + 逗号链回退
- leafer-x-webfont: fontFamily 链写回(Node 端),scan 链归一化防套娃、rewriteFamily 跳过链形态防抹回
- 修复 pnpm 双实例 bug: @napi-rs/canvas 版本对齐 1.0.6(根与 SDK 同版本共享 .pnpm 条目)
- 基准测试重构: 用例表抽取为 基准测试用例.ts 共享,SSIM 计算收敛到 scripts/ssim.ts
- 新增 基准测试_leafer.test.ts: @leafer-ui/node + Skia 渲染验证(无浏览器),ttf 输出用例 SSIM=1.0000;woff2 用例仅计时(Skia 不吃 woff2,容器验证由 puppeteer 版守护)
- Node 端集成测试 scripts/test-leafer-node.mts(插件链路 SSIM=1.0000)
2026-08-15 15:51:42 +08:00
崮生(子虚)
0693521716 feat: SDK 抽取为 webfont-sdk npm 包 + leafer-x-webfont 插件(浏览器验证通过)
- packages/webfont-sdk:TS 重写,IncrementalEngine 核心(去重/并发池/失败字符记忆/retryFailed)
  - CSS 模式(WebFont)API 与旧 public/webfont-sdk.js 完全兼容
  - FontFace 模式(WebFontCanvas)供 Canvas 场景,unicodeRange 注册 + onChunk 回调
  - tsdown 双产物:dist/(ESM+dts)+ dist-iife/(script 直引)
  - scripts/sync-public.mjs 构建后同步到 public/webfont-sdk.js
  - 修复 banner 内嵌套块注释导致的语法错误(/* 重绘 */ 提前闭合 banner)
- packages/leafer-x-webfont:LeaferJS 插件,依赖 webfont-sdk
  - 监听 property.change + layout.end 防抖扫描 Text 节点聚合字符
  - 新旧 API 兼容(on_/on__、off_/off__)
  - 注册后自动改写 fontFamily 为合法 CSS 名(去 .ttf 后缀)
  - demo/index.html:海报场景(标题/副标题/字体切换/导出 PNG)本地 vendor 验证通过
- backend/app.ts:fs 适配层改为 top-level await import(修复 dev 模式 mkdir 崩溃)
- doc/:商业化思路一/二评估 + 主动拓客文档
2026-08-15 13:07:32 +08:00
崮生(子虚)
fe61bace16 fix: 修复所有 TypeScript 类型错误
- 修复 fonteditor-core 模块声明(添加 FontEditor 命名空间、FontType 类型、SubsetOptions 接口)
- 修复 llrt.ts 中 mkdir 和 rm 方法的返回类型问题
- 修复 tcp_server.ts 中 chunk 类型问题(添加 Buffer 类型检查和转换)
- 修复 font_detail.ts 中缺失的 ORIGIN 导入
- 修复 subset.ts 中 subsetQueueTimeoutSeconds 的导入
- 为 Font 类添加 optimize() 和 write() 方法声明
2026-08-15 07:38:57 +08:00
崮生(子虚)
dc1ee66fe3 修复 temp-cleaner 在 LLRT 上的 unlink 兼容性问题
- 修改 interface.ts: 接受 options.unlink 和 options.rm 作为 FS 适配器的可配置方法
- 修改 llrt.ts: 使用箭头函数 (path) => fs.rm(path) 避免 this 绑定问题
- 修改 app.ts: 通过 __RUNTIME__ 条件编译,仅在 LLRT 运行时引入 llrt.ts
- 修改 tsdown.config.ts: format 从字符串数组改为字符串类型,添加 as const
- 修改 tsconfig.node.json: 添加 node 类型支持
- 新增 backend/global.d.ts: 声明 __RUNTIME__ 全局变量类型
- 修改 .gitignore: 添加 dist_backend_node 目录
- 修改 build-backend.ts: 支持 Node.js 版本构建输出到 dist_backend_node

修复问题:
- LLRT fs/promises 没有 unlink 方法,只有 rm 方法
- CJS 格式不支持 top-level await,改用 IIFE + require
- 条件编译避免 Node.js 版本包含不必要的 LLRT 适配代码
2026-08-15 07:02:22 +08:00
崮生(子虚)
d16aa46227 feat: 离线裁剪统计 + 企业服务板块 + SDK文档独立页(/docs) + 示例改用observeFont 2026-08-06 08:57:48 +08:00
崮生(子虚)
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