fix(otf-subset): 保留 GSUB locl 变体 target 字形,思源 otf SSIM 0.9530→1.0000

otf 子集化路径此前完全跳过 probeGsubAndCmap/collectReachableGsubTargets,
subsetGids 仅含 cmap 查到的 codepoint→基础 gid。思源黑体等 CID 字体含 locl
feature,浏览器按中文 locale 渲染时 GSUB 把基础字形替换为地区变体(如
"天" gid A → CN 变体 gid A+1)。subsetGids 不含这些 target,子集 GSUB 重
映射后 target gid 指向不存在/错误字形,渲染出错误变体。

逐字 ink 对比定位(地/黄/洪/波一致,天/玄/宇/法/海多 ink、宙少 ink)正是
locl 变体丢失特征。subsetOTF 内部复用 collectReachableGsubTargets 收集 seed
经 GSUB 可达的 target gid 合并进 subsetGids,subsetCFF/FDSelect/charset 与
GSUB/GPOS 重映射统一基于扩展后的 subsetGids,变体字形正确保留。

实测思源 9 字 otf/woff2 输出均 ink 6724/6724 SSIM 1.0000(修复前 0.9530),
白狐 5 字保持 1.0000,其余用例无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-24 23:50:37 +08:00
parent 94e75ea2ba
commit 07432852cf

View File

@ -23,6 +23,7 @@
import { subsetCFF } from "./cff-subset.js";
import { subsetGSUB } from "./gsub-subset.js";
import { subsetGPOS } from "./gpos-subset.js";
import { collectReachableGsubTargets } from "./gsub-reachable.js";
/** sfnt 表目录条目 */
interface SfntTable {
@ -470,6 +471,32 @@ export function subsetOTF(fontBuffer: Uint8Array, codePoints: number[], keepGSUB
/** 子集字形集 + codepoint→新gid */
const { subsetGids, cpToNewGid } = buildSubsetGids(dv, cmap.offset, codePoints);
/** GSUB target locl/calt/liga
* CID locl feature localeGSUB
* "天"gid A CN gid A+1 subsetGids target
* GSUB target gid / 9 ink 75
* SSIM 0.9530"天玄宇法海" ink"宙" ink locl
* ttf collectReachableGsubTargets seed GSUB target gid subsetGids
* GSUB GSUB/ */
if (keepGSUB) {
const gsub = findTable(tables, "GSUB");
if (gsub) {
const gsubBytes = new Uint8Array(dv.buffer, dv.byteOffset + gsub.offset, gsub.length);
/** seed = 当前 subsetGids已含 .notdef + 各 codepoint 的 gid */
const seed = new Set<number>(subsetGids);
const reachable = collectReachableGsubTargets(gsubBytes, seed);
if (reachable.size > 0) {
/** 合并去重:新 gid 顺序追加在现有 subsetGids 之后(不破坏 cpToNewGid 已建立的映射) */
for (const gid of reachable) {
if (gid > 0 && !seed.has(gid)) {
seed.add(gid);
subsetGids.push(gid);
}
}
}
}
}
/** 子集 CFF */
const cffBytes = new Uint8Array(fontBuffer.buffer, fontBuffer.byteOffset + cff.offset, cff.length);
const newCFF = subsetCFF(cffBytes, subsetGids);