崮生(子虚)
|
bebb8503f4
|
feat(otf): 独立 OTF(CFF) 子集化器,修复思源等 CID 字体 OTS 拒绝与 gid 错位
fonteditor-core 对含 idRangeOffset 的 CID cmap 解析有 bug,产出 gid 错乱子集(SSIM 0.93~0.97)。
新增独立 otf-subset + cff-subset,直接重建 CFF/cmap/hmtx,透传 charstring。
核心修复:
1. cff-subset 透传各 FD 的 Local Subr INDEX(op 19 指向)+ patch op19 新偏移。
原仅透传 Private DICT 字节未带 Local Subr INDEX,思源等字形用 callsubr 引用本地 subr,
子集 op19 指向越界致 OTS "Failed to parse Top DICT Data" 拒绝加载。
2. otf-subset 接入 subsetGSUB/subsetGPOS 按 subsetGids 做 gid 重映射。
原透传原始 GSUB/GPOS 字节 gid 未重映射,coverage 引用旧 gid 指向错字,
致 shaping 把字符替换成 .notdef(思源 otf SSIM 0.623,洪/法等字变空白)。
OTF 与 TTF 的 GSUB/GPOS 表结构相同,字节级子集化器可直接复用。
结果:思源 otf OTS 拒绝→可加载,SSIM 0.623→0.953,白狐 otf SSIM 1.0,
逐字 charstring 字节级与原始一致,渲染像素级一致(5 种独立验证 diff=0)。
所有非 otf 用例无回归。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 22:48:14 +08:00 |
|
崮生(子虚)
|
f4af6f7b68
|
perf(glyf): subset simple 字形剥离 hinting instructions
subset 模式下 simple 字形走原始字节快路径([[glyf-origbytes-fastpath]]),完整保留
原始 hinting instructions。但 web 渲染场景浏览器用 autohint,原始 instructions 不参与
渲染——实测剥离后所有基准用例 SSIM 与 ink 逐位一致(含最敏感的小字号用例)。
read 快路径定位 instructionLength(紧接 endPtsOfContours,OpenType 规范布局,无需展开
flags),存 _instrOff/_instrLen。write 快路径跳过 instructions 段并写 instructionLength=0,
sizeof 同步扣减字节长度。_instrOff=-1(无 instructions)走原整段拷贝路径。
收益(SSIM 全不变):
- FiraCode woff2 10776→8892(-17.5%),端到端 avg 1.92→1.68ms(-12.5%)
- 千字文 woff2 79784→79736,ttf 体积同步减小
- CJK 字体(初夏/霞鹜/思源)instructions 极少,体积持平
brotli 是 write 阶段硬下限,减小 glyf 原始字节直接减小 brotli 输入与输出。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 21:11:36 +08:00 |
|
崮生(子虚)
|
eb3ed06d41
|
perf(glyf): simple 字形 fast-path glyfObj 改对象字面量消除隐藏类迁移
glyf.read 的 simple 字形快路径原用 `var glyfObj = {}` 后逐个赋值 9 个属性
(xMin/yMin/xMax/yMax/_origBuf/_origOff/_origLen/_numContours/_totalPoints),
每个字形产生 9 次隐藏类迁移。改为对象字面量一次性创建所有属性,V8 分配稳定隐藏类
(无迁移),且 numberOfContours>0 / =0 统一形状(_numContours 直接用值,
_totalPoints 用三元表达式)。
千字文(129 simple 字形)Font.create best-of p50 约 -3%(多轮 0.1527→0.1479)。
5 字体 woff2 sha 逐字节一致,SSIM 全部不变(仅对象创建方式变化,属性集完全相同)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 20:44:55 +08:00 |
|
崮生(子虚)
|
c6316378f2
|
perf(cmap): presetCmap 模式 parse 提前返回跳过 subtable 解析
承接优化315(readWindowsAllCodes 复用 presetCmap),cmap.parse 在 presetCmap 存在时
直接返回 presetCmap,跳过整个 cmap 解析:子表目录扫描 + readSubTable 建 format4._lazySegs
/format12._lazyGroups + readWindowsAllCodes 调用。
presetCmap 模式下 subTables 的 _lazySegs/_lazyGroups 不再被 lookupFormat4/12 使用
(readWindowsAllCodes 已跳过),cmap.write/sizeof 用 ttf.cmap 的 keys 重建不依赖
subTables,故 format4/12 subtable 的 header 解析纯粹是无用功。
收益(Font.create best-of,叠加优化315的总收益):千字文思源 -8.4%、初夏纯标点 -6.2%、
FiraCode -5.0%、思源标点 -5.5%。千字文前段 woff2 基准 avg 4.9→3.7ms。6 字体 woff2 sha
逐字节一致,SSIM 全部不变。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 20:32:39 +08:00 |
|
崮生(子虚)
|
ce893976f1
|
perf(cmap): subset 模式复用 probe 的 codepoint→gid 映射跳过二分查找
Font.create 内 readWindowsAllCodes 在 subset 模式下对每个 subset 字符做
lookupFormat4/lookupFormat12 二分查找产出 codepoint→gid,这与之前 probeGsubAndCmap
的 probe.lookup(算法一致)完全重复。subset 模式下 format0/format14 不解析
(glyphIdArray/groups 为空),codes 仅来自 format4/12,故 probe.lookup 与
readWindowsAllCodes 产出严格等价。
fontSubset 在 probe.ok 时将 probe.lookup(Map)转为普通对象经 readOptions.presetCmap
注入,readWindowsAllCodes 检测到 presetCmap 直接返回,跳过 N 次(subset 长度)二分查找。
收益(best-of Font.create):千字文 -10.2%、初夏纯标点 -5.5%、FiraCode -3.7%
(与 subset 长度成正比,每 codepoint 省一次二分)。5 字体 woff2 sha 逐字节一致,
SSIM 全部不变。probe 不 ok(otf 或无 format4/12)时不注入,走原逻辑。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 20:25:03 +08:00 |
|
崮生(子虚)
|
8a7efea9d0
|
perf(gpos/gsub): read 零拷贝 subarray 替代 readBytes slice
GPOS/GSUB 表类 read 原用 reader.readBytes (= new Uint8Array(view.buffer, off, len).slice()),
.slice() 拷贝整个表字节(初夏 GPOS 68KB、GSUB 31KB)。V8 中 68KB slice 耗 ~40μs,
而 subarray 零拷贝仅 ~0.3μs(130× 差距)。
GPOS/GSUB 是原始字节透传:subsetGPOS/subsetGSUB 只读 OTReader + 写全新 OTWriter,
write 只 writeBytes 只读,optimize 不碰,整个 read→optimize→subset→write 生命周期
从不原地修改底层字节,故可安全返回共享 ArrayBuffer 的 subarray 视图。
初夏 full read 0.124→0.063ms (-49%),readBuffer 0.165→0.098ms。
全基准 SSIM 与输出字节完全不变。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 15:07:18 +08:00 |
|
崮生(子虚)
|
04bf5c6077
|
perf(os2): size 固定返回 96 跳过通用 struct 查表(version 恒 0x4)
|
2026-07-24 14:33:54 +08:00 |
|
崮生(子虚)
|
82c1e42108
|
perf(name): subset 字体 name 表白名单,仅保留渲染必需 6 个 nameId
subset 字体核心目的是小体积下载 + 正确渲染,但 name 表保留了全部 19 个 nameId
(含版权/商标/URL/版本/描述等元数据)。思源 8 字 ttf 8.8KB 里 name 占 3.9KB(44%),
多为这些 web 渲染无用的元数据,徒增 brotli 输入与下载体积。
size 阶段加 KEEP_NAME_IDS 白名单,只保留浏览器字体匹配/渲染必需的:
1 fontFamily / 2 fontSubFamily(CSS font-family 与 weight/style 匹配核心)
4 fullName / 6 postScriptName(全名与 PS 名)
16 preferredFamily / 17 preferredSubFamily(变体/typographic 匹配)
体积收益(SSIM 全部不变,渲染像素不受 name 内容影响):
思源 8 字 woff2 4448→2632B(-41%)、思源千字文中段 19100→17156B(-10%)
FiraCode 11636→10828B(-7%)、思源标点 4864→3048B(-37%)
令东千字文 -0.5%(name 占比本就小)。brotli 输入减小附带压缩提速。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 14:22:11 +08:00 |
|
崮生(子虚)
|
089ae7c0a7
|
perf(name): read 单遍扫描定 platform,消除全部中间 nameRecord 对象
原 read 先建 count 个 {platform,encoding,language,nameId,length,offset} 中间对象,
再两遍遍历它们。改为两趟直扫 view:
pass1 只读 platform/encoding/language 三字段比较定 platform(windows english 优先),
不读 nameId/length/offset、不建对象;
pass2 头开始只对匹配记录读 nameId/length/offset + readBytes + 解码,直接产出 names。
思源 56 条记录中间对象 56→0,view 读次数减半。字节 sha256 完全一致,
5 轮交替 readBuffer median 稳定 -3~-5%(对象分配本身 V8 近零成本,收益小但一致)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 12:53:04 +08:00 |
|
崮生(子虚)
|
ba17974e1d
|
perf(name): read 延迟 readBytes,按需只解码选中 platform 的记录
原 name.read 第一遍循环对全部 count 条记录都 readBytes(每次 slice 一份字节),
但最终第三循环只对匹配 platform+encoding+language 且 nameId 有用的记录解码字符串,
其余记录的 .name 字节从不被消费。思源 56 条记录只解码约 15 条,浪费 41 次 slice。
改为删除第一遍全量 readBytes,在第三循环里对匹配记录按需 readBytes,
N 次 slice 降到「有用记录数」次。字节 sha256 完全一致(思源/令东验证),
思源 readBuffer median 0.021→0.014ms(-34%),令东持平(name 表小本就无浪费)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 12:50:28 +08:00 |
|
崮生(子虚)
|
d6d94e201e
|
perf(os2): read 改用直接 DataView 批量读取
OS/2.read 原走 struct 通用 read(逐字段 switch 分发 + reader.read 调用),
与已优化的 write(优化176 直接 view)不对称。改为手写每个字段的 view 直读,
按 version(0/1/2+)决定读到哪,补默认值逻辑不变。
字段值完全一致(version/achVendID/sxHeight 验证),OS/2.read 微基准
思源 6.2→1.7μs(-73%)、令东 5.5→0.6μs(-89%),端到端约 -1%
(readBuffer 占比小),与 write 对称后可推广到其他 struct 通用 read 表。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 12:44:50 +08:00 |
|
崮生(子虚)
|
d575d55540
|
perf(name): 合并 UTF-8+UCS-2 编码 + ASCII 快路径
name 表 size 阶段对每条记录都要 UTF-8(mac)+ UCS-2(windows)各一份,
原两次独立编码各做一遍 stringify 扫描 + 编码循环。新增 toUTF8AndUCS2Bytes
合并为单次扫描:纯 ASCII(含拒绝 \0)走超快路径——UTF-8 直接取字节、
UCS-2 大端序 [0,ch],省掉多字节分支与两次扫描;非 ASCII 回退原逻辑。
字节 sha256 完全一致(思源/令东验证),思源 8 字 woff2 端到端 -7%
(name.size 占 dump 44%),令东千字文持平(name 占比 0.3% 在噪声内)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 12:38:40 +08:00 |
|
崮生(子虚)
|
bcd4574b3f
|
perf(woff2): triplet 编码无分支 abs + 符号位提取
transformGlyfAndLoca 的 y+triplet 循环每点原先 4 次三元判断(absDx/absDy/xSign/ySign),
中文字体 dx/dy 正负近似 50/50 为不可预测分支,致流水线冲刷。
改用 Int32 算术右移统一提取符号:
- dxSgn = dx >> 31(0 或 -1)
- absDx = (dx ^ dxSgn) - dxSgn(无分支 abs)
- xSignBit = dxSgn + 1(等价 dx >= 0 ? 1 : 0)
令东 74.6% 点走 case3,收益最大:woff2 端到端 2.197→2.078ms(-5.4%),
思源 0.662→0.653ms(-1.4%)。sha256 字节级一致,全基准 SSIM 零变化。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 12:08:48 +08:00 |
|
崮生(子虚)
|
60db8d226b
|
perf(woff2): triplet 编码 case2/3/4 去掉冗余的轴非零判定
case1 排除 dx===0、case2 排除 dy===0 后,走到 case3/case4 时两轴必非零,
故 case3/case4 的 'dx!==0 && dy!==0' 是冗余判定。令东 74.6% 点命中 case3,
每点省 2 次比较。case2 的 'dx!==0' 同理冗余(case1 未命中且 dy===0 则 dx 必非0)。
cmp 字节一致,逻辑等价已证明。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 11:55:24 +08:00 |
|
崮生(子虚)
|
72c6afef92
|
perf(woff2): 移除 simple glyph 的 bbox 计算,由解码端从轮廓点重建
WOFF2 规范:bboxBitmap bit=0 时解码端计算点的 min/max 重建 bbox,bit=1 读显式存储值。
原编码对 simple glyph 计算 calcBbox 并与原始 bbox 比对决定 bit——但置 1 时存的也是
calcBbox,故无论 bit 值解码端都得 calcBbox,该计算与比对无功能意义。
移除后 simple glyph 永远 bit=0(解码端自算),省掉 x/y 循环每点 3 次 bbox 比较 + bboxSet
判定 + Pass2 写入。复合 glyph 仍显式存 bbox(raw 数据剥离了 glyph 头,解码端无法重建)。
令东千字文 woff2 write 2.449→2.237ms,输出体积 -20B~60B,全基准 SSIM 零变化。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 11:51:29 +08:00 |
|
崮生(子虚)
|
f945e1aedb
|
perf(woff2): triplet 编码各分支内累加 gsbi,消除 TRIPLET_DATA_SIZES 查表
令东千字文 woff2 write -12%(2.785→2.449ms),输出字节完全一致(cmp sha256 验证)。
原 gsbi += TRIPLET_DATA_SIZES[flag & 0x7F] 每点查表(Uint8Array 索引+位运算),
改为各 case 分支内 adv=1/1/1/2/3/4 常量累加,省掉查表。思源 -4%。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 11:48:12 +08:00 |
|
崮生(子虚)
|
65e238cdca
|
perf(cmap): subset 模式 format4 延迟解析,跳过 4×segCount 数组构建
subset 模式下 format4 不再急切构建 endCode/startCode/idDelta/idRangeOffset
共 4 个 segCount 项数组,只记录各段在 view 的偏移;lookupFormat4 二分时
按需从 view 读(mid*2 偏移)。原 subset 模式仍执行 4×segCount 次 getUint16
+ 4 个数组分配,得意黑 segCount=3929 → 1.5 万次读,占 readSubTable 0.101ms。
延迟后 view 读次数 = O(S × log(segCount)),远小于 4×segCount。
lookupFormat4 新增 _lazySegs 分支,非延迟路径(全量模式)保持急切数组不变。
idDelta 仍用 getUint16 读(与原急切模式一致),依赖 % 0x10000 处理符号。
字节逐字节一致(令东/思源/初夏/得意黑/otf 全 cmp 通过),SSIM 全程不变。
cmap: 初夏 0.075→0.009ms(-88%),得意黑→0.004ms(-94%),思源 0.063→0.014(-78%)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 10:57:36 +08:00 |
|
崮生(子虚)
|
b7dbe3fe1d
|
perf(hmtx): subset 模式紧凑存储,消除 numGlyphs*2 大对象分配
subset 模式下 hmtx 改为按 subsetGids 顺序紧凑存储 Int32Array(S*2),
下标用 gi*2(subsetGids 内位置)替代 gid*2。原实现分配 Int32Array(numGlyphs*2),
CJK 字体 numGlyphs 达 3.5 万→284KB 大对象,V8 大对象分配+zero-fill 单次 ~54μs。
resolveGlyf subset 分支同步改用 gi*2 索引。OTF 路径不变(表顺序 hmtx 在 CFF 前,
subsetGids 未就绪,仍走全量数组)。
字节逐字节一致(令东/思源/初夏/得意黑/otf 全 cmp 通过),SSIM 全程不变。
初夏 hmtx 0.056→0.002ms(-96%),create -15%,fontSubset 总 -7%。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 10:52:30 +08:00 |
|
崮生(子虚)
|
a1c7312722
|
perf(glyf): _origGlyfRef 子对象展平为 _origBuf/_origOff/_origLen
subset 快路径每个 simple 字形原本分配 glyfObj + _origGlyfRef 两个对象,
展平后 _origGlyfRef 的 {buffer,byteOffset,length} 直接存为 glyfObj 的三个属性,
省掉每字一个子对象分配 + 下游解引用间接寻址。
改 glyf.js(声明+component 守卫)、write.js、sizeof.js、optimizettf.js。
千字文 ttf 0.246→0.234ms(-5%),ttf/woff2 字节输出 cmp 完全一致,SSIM 全部不变。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 09:31:43 +08:00 |
|
崮生(子虚)
|
6b28ba3d74
|
perf(glyf): subset 快路径删 endPtsOfContours 数组分配,直接读最后 endPt
优化310 快路径下 endPtsOfContours 数组仅用于算 _totalPoints(供 maxPoints metrics),
下游 optimizettf 快路径分支、sizeof/write 原始字节拷贝均不消费它。
直接读最后一个 endPt(vOff+10+(numContours-1)*2)算 _totalPoints,
省掉每个 simple 字形的 Array 分配 + numContours 次 getUint16 循环。
千字文 ttf 0.252→0.246ms,ttf 字节输出 cmp 完全一致,SSIM 全部不变。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 09:27:19 +08:00 |
|
崮生(子虚)
|
308df8ef4d
|
perf(woff2): transformGlyfAndLoca 合并 y 解码与 triplet 编码为单循环
原 3 次遍历(x 解码→y 解码→triplet 编码)+ yCoords Int32Array 中转;
现 2 次遍历(x 解码→y 解码+triplet 编码合并)。
关键洞察:triplet delta 语义 = TTF delta(都是相对前一点的差),
所以解码 y 的同时用已存 xCoords[i] 配对编码 triplet,无需 yCoords 数组。
bbox_y 的 min/max 在合并循环同步计算,bbox bitmap 判定拆为 x/y 两段。
千字文 woff2 2.87ms→2.59ms(-9.6%),字节输出与 clean 版 cmp 完全一致,
SSIM 全部不变。删除 _reuseYCoords 模块级缓冲区。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 09:23:06 +08:00 |
|
崮生(子虚)
|
ece6eeca2f
|
perf(buffer): toBuffer ArrayBuffer→Buffer 用 Buffer.from 零拷贝替代逐字节循环
千字文 ttf 0.396ms→0.252ms(-36%),woff2 不受影响(不走 node Buffer 路径)。
Writer 的 ArrayBuffer 精确分配为 ttfSize(offset=0),getBuffer 返回完整 buffer,
Buffer.from 共享底层内存无 over-exposure 风险。SSIM 全部不变。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 09:15:17 +08:00 |
|
崮生(子虚)
|
69dc5e343e
|
perf(glyf): simple 字形原始字节快路径,千字文 ttf -80%/woff2 -35%
子集化 simple 字形经 parse→optimizettf→write 往返后产出的 glyf 字节
与原始字体完全一致(实测 73/73),直接拷贝原始字节跳过三段往返。
- glyf.js subset read: simple 字形存 _origGlyfRef 引用 + 读 header/endPts
供 metrics,跳过坐标解码;compound 及 component 走完整 parse
- optimizettf: 检测 _origGlyfRef 只收集 metrics 跳过 ceilReduce
- sizeof: glyfSize = 原始字节长度
- write: fullView.set 原始字节段跳过 encode
额外保留原始 hinting instructions(原非 hinting 模式丢弃),渲染更精确。
SSIM 全部不变(千字文 0.9986/FiraCode 连字 0.9923/初夏明朝 0.9920)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 09:08:22 +08:00 |
|
崮生(子虚)
|
8f0964ce54
|
perf(parse): OS/2 表类按 format 缓存 + 清理 dead write,千字文 -5%
OS/2 read 原每次 struct.slice + _table.create 动态建类(_createClass +
Object.assign prototype),是 OS/2 parse 主开销(prof 稳态 ObjectAssign 4.2%)。
format 仅 0/1/2+ 三种,表类按 format 模块级缓存复用;去掉 Object.assign
(os2Fields, tbl),直接在 tbl 上补默认字段。
附带清理:ceilReduceAndSizeFromTypedArrays/Flat 的 _preXLen/_preYLen 是
全库零读取点的 dead write(subarray.length 已携带长度),删除;
optimizettf 后补 _yArr=null(悬挂内存,唯一消费者因 _xArr=null 守卫进不去)。
千字文 woff2 A/B:4.47→4.26ms(-5%),字节完全相同,SSIM 全部一致。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 08:51:44 +08:00 |
|
崮生(子虚)
|
78e8f798e8
|
perf(woff2): Brotli 质量 q3→q2,再省 8% 时间,体积仅 +0.09%
实测千字文 woff2 各 quality 端到端时间 + 体积(同文本):
q3=4.65ms/42712B q2=4.26ms/42752B q1=4.21ms/46432B q0=4.12ms/47240B
q2 是时间/体积最优拐点:比 q3 再省 8% 时间,体积仅 +40 字节(+0.09%)。
q1/q0 体积陡增 +8.7%/+10.6%(下载/解析代价),不划算。
基准测试全部 SSIM 与 ink 计数一致(FiraCode 0.9923、初夏明朝纯标点 0.9920)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 08:42:36 +08:00 |
|
崮生(子虚)
|
4fb4679636
|
perf(woff2): Brotli 质量 q6→q3,千字文 -26%/FiraCode -13%,SSIM 不变
WOFF2 是无损容器,Brotli 质量只影响压缩率不影响解码结果。
实测 glyf 变换数据:q6=3.66ms/45935B(54.0%) → q3=1.20ms/46168B(54.3%),
时间 -67%,体积仅 +0.5%(持平)。q3 是时间/体积最优点,再降体积显著增大。
端到端 A/B(esbuild bundle):千字文 woff2 6.31→4.65ms(-26%),
FiraCode woff2 11.80→10.27ms(-13%)。基准测试全部 SSIM 与 ink 计数完全一致。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 08:37:55 +08:00 |
|
崮生(子虚)
|
8fd11fc859
|
perf(woff2): calcTripletAndWrite inline 到主循环,消除每点函数调用
千字文 woff2(54350 点)transformGlyfAndLoca 的 calcTripletAndWrite 占 31.8% CPU
(V8 --prof 实测)。手动 inline 到 triplet 循环消除 54350 次/call 调用开销,
让 V8 做循环内寄存器分配与公共子表达式消除。原独立函数删除(避免死代码)。
输出字节逐字节完全一致(千字文/思源黑体/FiraCode 三组 IDENTICAL 验证)。
微基准 encodeTTFToWOFF2 千字文 4.84→4.38ms(-9.5%)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-24 07:27:09 +08:00 |
|
崮生(子虚)
|
d3d96d1735
|
perf(loca): subset 模式 Proxy 按需读,跳过全量 30907 项解析
subset 模式下 glyf.read 只访问 subsetGids 相关的 ~40 个 loca 项(含 compound
展开),原全量读 numGlyphs 项(思源 30907)+ 分配 Uint32Array(30907)=124KB 是纯浪费。
loca 表读取顺序在 glyf 之前,subsetGids 尚未构建,无法像 hmtx 那样按已知 gid 列表读。
改用 Proxy 拦截 glyf.read 的 loca[index] 访问,首次访问某 gid 时按需从 view 读并缓存。
实测 loca.read 从 profile 12.2% 降至 0%(思源标点),千字文 fontSubset -6.5%。
非 subset 路径保持原 TypedArray(全量访问下 Proxy trap 反慢)。基准 SSIM/ink/字节全无回归。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-23 20:09:18 +08:00 |
|
崮生(子虚)
|
b4f8d7a522
|
perf(hmtx): subset 模式按需读 subsetGids 项,跳过全量 30888 项解析
subset 模式下 glyf.read(表顺序在 hmtx 之前)已构建 subsetGids,
resolveGlyf 也只访问 subsetGids 对应位置。原实现全量解析 numOfLongHorMetrics
项(思源 30888)对小子集是纯浪费。改为只读 subsetGids 需要的项,
其余位置保持 0(不被访问)。
实测 hmtx.read 0.21→0.032ms(6.5×),基准 SSIM/ink/字节全无回归。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-23 19:46:42 +08:00 |
|
崮生(子虚)
|
bbc3d8b88a
|
perf(hmtx): LongHorMetric 段改 Uint16Array view 批量读+内联翻转
hmtx.read 原用 DataView.getUint16/getInt16 逐次调用读 LongHorMetric 段(每项 u16 advW + i16 lsb),
思源黑体 numOfLongHorMetrics=30888,占 fontSubset 约 8.7%(profile 仅次于 brotli/subsetGSUB)。
改用 Uint16Array view 共享 buffer 按下标读取后内联大端翻转(与 loca 优化同思路),实测快 2×
(198→98μs/call)。offset 需 2 字节对齐,未对齐回退 DataView。Last 段(lsb only)同样优化,
起始必然 2 字节对齐(前段是 4 字节倍数)。
基准测试 29 用例 SSIM/ink/输出字节全部无回归。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-07-23 19:10:18 +08:00 |
|
崮生(子虚)
|
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 |
|
崮生(子虚)
|
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 |
|
崮生(子虚)
|
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 |
|
崮生(子虚)
|
03107af2dd
|
添加注释
|
2026-04-11 22:51:53 +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 |
|
崮生(子虚)
|
0bfc9c9b27
|
1
|
2026-04-11 00:11:09 +08:00 |
|
崮生(子虚)
|
161bafc02a
|
实现 js 版 woff2 处理
|
2026-04-10 20:42:30 +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 |
|
崮生(子虚)
|
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 |
|
崮生(子虚)
|
909b91a8d5
|
优化
|
2026-04-09 10:27:17 +08:00 |
|
崮生(子虚)
|
acfe0ca022
|
优化 6.3ms / 2.3ms / 11.1ms - 全面超越之前最佳性能!
|
2026-04-09 10:25:17 +08:00 |
|