From 07432852cfb5f6f50e2c441d7c39c72910e1a08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Fri, 24 Jul 2026 23:50:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(otf-subset):=20=E4=BF=9D=E7=95=99=20GSUB=20?= =?UTF-8?q?locl=20=E5=8F=98=E4=BD=93=20target=20=E5=AD=97=E5=BD=A2?= =?UTF-8?q?=EF=BC=8C=E6=80=9D=E6=BA=90=20otf=20SSIM=200.9530=E2=86=921.000?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- backend/font_util/otf-subset.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/backend/font_util/otf-subset.ts b/backend/font_util/otf-subset.ts index 5c860e0..dbe2d5c 100644 --- a/backend/font_util/otf-subset.ts +++ b/backend/font_util/otf-subset.ts @@ -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:浏览器按系统 locale(中文)渲染时,GSUB 把基础字形 + * 替换为地区变体字形(如"天"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(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);