88 Commits

Author SHA1 Message Date
崮生(子虚)
d793e22d92 perf(loca): TypedArray 批量读+内联翻转替代 DataView 循环,loca 解析快 39%~46%
loca 偏移是大端,原 DataView.getUint16/32 逐次调用含边界检查开销。改用
Uint16/32Array 直接 view 共享 buffer + 内联字节翻转,微基准实测:
- format0(Uint16):157→84μs(-46%)
- format1(Uint32):152→93μs(-39%)
(思源 30907 条 loca)

返回 Uint32Array 替代普通 Array,glyf.js 仅按下标 loca[index] 只读访问,
与 TypedArray 兼容(已确认 loca 仅被 glyf.js:22 读取、ttfreader.js:216 置 null)。

对齐保护:Uint16Array 需 2 字节对齐、Uint32Array 需 4 字节对齐,loca offset
来自表目录不保证对齐。检测 srcByteOff 对齐情况,未对齐回退 DataView 路径。

验证:基准测试 29 用例 SSIM 全部无回归;4 用例(思源千字文/标点/FiraCode/ttf)
sha256 输出字节逐字节等价。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:25:39 +08:00
崮生(子虚)
6f96e2b1dd perf(post): subset 模式跳过 glyph name 解析降级 format 3,CJK 子集化提速 45%~50%
subset 模式下 post glyph name 对 web 字体渲染无用(浏览器靠 cmap→gid→glyf 渲染,
glyph name 仅用于 PDF 嵌入/编辑器)。原实现 subset 模式保存 pascalString 区
readBytes(思源 278KB)+ view 引用,resolveGlyf 再按需读 nameIndex + 算 name,
实测 post 段占 Font.create 的 69%(思源 56 glyph 子集 1.19ms)。

优化:subset 模式直接降级 post.format=3,跳过 nameIndex 解析、pascalString
readBytes、resolveGlyf 按需 name 读取。resolveGlyf 的 `2 === format` 条件不成立
整段跳过;write/size 走 format 3(32 字节,无 nameIndex/names)。

收益(CJK Font.create 是 fontSubset 主体):
- 思源千字文中段 3.057→1.673ms(-45%)
- 思源标点 2.705→1.361ms(-50%)
- FiraCode 22.2→21.0ms(-5.5%,瓶颈在 GSUB 不在此)
体积:FiraCode 8724→8704B(post 无 name 更紧凑)

验证:基准测试 29 用例 SSIM 全部无回归(千字文 0.9986、思源标点 0.9977、
初夏纯标点 0.9920、FiraCode 0.9923、otf-思源 0.9310 均不变)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:16:56 +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
崮生(子虚)
8db337d27e chore(release): v1.9.1 — 修复 LLRT __filename 报错
重新发版,修复 v1.9.0 中 createRequire(import.meta.url) 导致 LLRT 运行时报错。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:22:48 +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
崮生(子虚)
c04914e255 perf(woff2): glyf triplet 变换无分支优化(优化302)
千字文 woff2(57168 点)profile 定位 transformGlyfAndLoca 热点:
- 坐标解码 50/50 不可预测分支(正负 delta 各半)→ sign 位无分支乘法
- abs 三元 → Math.abs(V8 编译为 neg 指令)
- curveBit 三元 → 位运算无分支

微基准 3.5-3.6x,端到端千字文 woff2 9.8→9.1ms。
体积与 SSIM 完全不变(无分支只改计算方式不改结果)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:07:57 +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
崮生(子虚)
4a6c7e64e1 glm2.5 优化,暂存 2026-06-13 20:19:57 +08:00
崮生(子虚)
13787dfd22 refactor: 重命名 Skill 为 chinese-web-font,聚焦中文字体应用
- skills/chinese-web-typography.md → skills/chinese-web-font.md
- 移除路线图(不写规划)
- 移除英文 README 中不存在的 Typography Presets 表格
- 统一文案:Typography Skill → Chinese Font Skill

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v20260613
2026-05-29 09:14:04 +08:00
崮生(子虚)
5dab3d6d93 更新文档 2026-05-28 22:50:02 +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
崮生(子虚)
106eb8ce7b fix: SDK 收集输入框字符时同时包含 placeholder 占位文本
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 21:39:47 +08:00
崮生(子虚)
7c7db7447a fix build 2026-04-30 21:34:48 +08:00
崮生(子虚)
76ac23ad5d fix build 2026-04-30 21:03:57 +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
崮生(子虚)
68df9db2da fix: 修复 StatsPanel 中 flex-wrap 属性的 JSX 语法错误
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 19:37:57 +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
崮生(子虚)
6e5b2c5fb7 chore: 升级至 v1.6.0,llrt 切换为 no-sdk 版本
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 13:32:07 +08:00
崮生(子虚)
258d9f9746 chore: 移除 skia-canvas 依赖,清理废弃验证脚本
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 17:30:09 +08:00
崮生(子虚)
03107af2dd 添加注释 2026-04-11 22:51:53 +08:00
崮生(子虚)
976a68f189 1 2026-04-11 17:54:01 +08:00
崮生(子虚)
bd3189b459 优化性能 2026-04-11 17:53:57 +08:00
崮生(子虚)
6ba093ff70 优化性能 2026-04-11 10:25:54 +08:00
崮生(子虚)
2f9eb3fd1a 支持 woff2 webfont@1.5.0 2026-04-11 00:17:27 +08:00
崮生(子虚)
0bfc9c9b27 1 2026-04-11 00:11:09 +08:00
崮生(子虚)
4ada92b6c5 更新基准测试 2026-04-10 20:57:37 +08:00
崮生(子虚)
161bafc02a 实现 js 版 woff2 处理 2026-04-10 20:42:30 +08:00
崮生(子虚)
e977cc9575 更新版本 2026-04-10 13:57:28 +08:00
崮生(子虚)
efc783cd86 更新依赖 2026-04-10 13:56:08 +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
崮生(子虚)
af0ab38cec 性能优化 2026-04-10 13:51:03 +08:00
崮生(子虚)
4990a0f61d 性能优化 2026-04-10 08:44:09 +08:00
崮生(子虚)
97f4d72e6a fix: 修复 OTF→TTF 转换字体在浏览器中渲染空白的问题
根因:optimizettf 中 maxPoints/maxContours 只在 TypedArray 格式(_xArr)
的字形分支中统计,OTF→TTF 转换的字形使用对象 contours 格式,
导致 maxp 表中这两个值为 0,浏览器据此跳过渲染。

同时包含 OTF 解析路径的多项性能优化和清理冗余测试文件。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 20:46:48 +08:00
崮生(子虚)
18e60fe940 优化性能 2026-04-09 16:47:41 +08:00
崮生(子虚)
3ce09a21a4 fix 切换字体没有变化 2026-04-09 12:06:31 +08:00
崮生(子虚)
84b50db7d9 更新版本 2026-04-09 12:02:49 +08:00
崮生(子虚)
55aa492ce0 新增 ttf 随机选择和提示 2026-04-09 12:02:37 +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
崮生(子虚)
6d34a5e53a feat: 标语随机使用字体展示,调整字号和位置
标语移至标题下方,每次加载随机选取字体列表中的一个字体渲染,
悬停显示字体名称,字号调至 24px。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 11:34:02 +08:00
崮生(子虚)
b7a91a3a11 fix: 修正 package.json exports 条件顺序,消除 tsup 构建警告
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 11:23:56 +08:00
崮生(子虚)
6f0e3d8e6b fix: 修复 name 表写入偏移错误导致浏览器无法渲染字体
name.js write 函数改用 view.setUint16 后未同步 writer.offset,
导致 name 字符串覆盖 header,name 表损坏。浏览器依赖 name 表
识别字体所以无法渲染,而 Node 端渲染不依赖 name 表所以基准
测试未检测到。

同时修复 cmap sizeof 动态计算记录头大小的问题。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 11:22:51 +08:00
崮生(子虚)
4acb1c1e5d docs: 添加品牌标语
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 10:47:48 +08:00