perf(gsub): serialize type2/3/4 大 coverage 反转遍历 + 修越界 undefined bug

serializeMultipleSubst/serializeAlternateSubst/serializeLigatureSubst 原实现
readCoverageGids 全量展开 coverage 再逐个查 gidLookup,与 serializeSingleSubst
([[gsub-serialize-single-bigcov-reverse]] 早修)同类的 serialize 侧浪费。

probe 实测:白狐千字文 type3 coverage=1000、初夏 type3=329/type4=97、霞鹜 type4=94,
但子集仅命中个位数。CJK 字体(令东/思源 TTF+OTF)type2/3/4 全无调用。

优化:套用 serializeSingleSubst 的反转模式——coverageCount 判大小,coverage 远大于
子集(covGidCount > subsetLen*4 && >16)时遍历 currentSortedSubsetGids 用
coverageIndexOf 二分定位 coverage 下标 idx(即 set 偏移数组下标),替代全量展开。

附带修复原版越界 bug:原版 gidLookup[covGids[i]] 当 covGids[i] 超出 numGlyphs
(损坏 coverage,白狐实测含)返回 undefined,`undefined < 0` 为 false(NaN 比较)漏过,
产生 from=undefined 的非法 AlternateSet entry(emitCoverage 写无效 gid)。
两路径统一改 `!(fromNew >= 0)`(undefined>=0 也是 false → 跳过),与反转路径行为一致。
区别于 reachable 侧 readCoverageRemapped 早有 numGlyphs 越界保护(line 221-261)。

A/B:初夏/霞鹜千字文(type3/4 触发反转)字节与基准线 IDENTICAL;白狐千字文字节变小
(-12B ttf/-16B woff2,即去掉的非法 entry)。
基准测试新增「otf-白狐千字文」用例验证:SSIM=1.0000 ink=19735/19735(渲染像素级一致,
证明去掉的是浏览器本就忽略的无效 gid entry)。

性能 best-of N=2000:白狐千字文 min -5.4%/p10 -6.7%、初夏 -5.1%/-3.9%、霞鹜 -5.6%/-3.5%;
令东千字文(无 type2/3/4)持平。29 用例全 SSIM PASS。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-25 04:58:34 +08:00
parent 01b7ee432d
commit ae41a99e95
2 changed files with 147 additions and 48 deletions

View File

@ -528,22 +528,48 @@ function serializeMultipleSubst(
const dv = r.dv;
const covOff = off + r.u16(off + 2);
const seqCount = r.u16(off + 4);
const covGids = readCoverageGids(r, covOff);
/** 逐 coverage 字形读取其 sequence重映射后保留有效项 */
const entries: Array<{ from: number; seq: number[] }> = [];
for (let i = 0; i < covGids.length && i < seqCount; i++) {
const fromNew = gidLookup[covGids[i]];
if (fromNew < 0) continue;
const seqOff = off + dv.getUint16(off + 6 + i * 2, false);
const glyphCount = dv.getUint16(seqOff, false);
const newSeq: number[] = [];
for (let k = 0; k < glyphCount; k++) {
const g = gidLookup[dv.getUint16(seqOff + 2 + k * 2, false)];
if (g >= 0) newSeq.push(g);
/** 反转遍历快路径(同 serializeLigatureSubst / serializeSingleSubstcoverage
* gid coverage idx SequenceTable */
const subsetGids = currentSortedSubsetGids;
const covGidCount = coverageCount(r, covOff);
const useReverse = covGidCount > subsetGids.length * 4 && covGidCount > 16;
if (useReverse) {
for (const g of subsetGids) {
const idx = coverageIndexOf(r, covOff, g);
if (idx < 0 || idx >= seqCount) continue;
const fromNew = gidLookup[g];
if (!(fromNew >= 0)) continue;
const seqOff = off + dv.getUint16(off + 6 + idx * 2, false);
const glyphCount = dv.getUint16(seqOff, false);
const newSeq: number[] = [];
for (let k = 0; k < glyphCount; k++) {
const gg = gidLookup[dv.getUint16(seqOff + 2 + k * 2, false)];
if (gg >= 0) newSeq.push(gg);
}
/** 序列至少 1 个目标 gid 才有意义 */
if (newSeq.length > 0) entries.push({ from: fromNew, seq: newSeq });
}
} else {
const covGids = readCoverageGids(r, covOff);
for (let i = 0; i < covGids.length && i < seqCount; i++) {
const fromNew = gidLookup[covGids[i]];
/** fromNew === undefinedcovGids[i] 越界,损坏 coverage也跳过与反转路径行为一致 */
if (!(fromNew >= 0)) continue;
const seqOff = off + dv.getUint16(off + 6 + i * 2, false);
const glyphCount = dv.getUint16(seqOff, false);
const newSeq: number[] = [];
for (let k = 0; k < glyphCount; k++) {
const g = gidLookup[dv.getUint16(seqOff + 2 + k * 2, false)];
if (g >= 0) newSeq.push(g);
}
/** 序列至少 1 个目标 gid 才有意义 */
if (newSeq.length > 0) entries.push({ from: fromNew, seq: newSeq });
}
/** 序列至少 1 个目标 gid 才有意义 */
if (newSeq.length > 0) entries.push({ from: fromNew, seq: newSeq });
}
if (entries.length === 0) return false;
/** 按 from gid 升序排序,使 coverageemitCoverage 强制升序)与 sequence 数组保持下标配对 */
@ -579,20 +605,46 @@ function serializeAlternateSubst(
const dv = r.dv;
const covOff = off + r.u16(off + 2);
const altCount = r.u16(off + 4);
const covGids = readCoverageGids(r, covOff);
const entries: Array<{ from: number; alts: number[] }> = [];
for (let i = 0; i < covGids.length && i < altCount; i++) {
const fromNew = gidLookup[covGids[i]];
if (fromNew < 0) continue;
const altOff = off + dv.getUint16(off + 6 + i * 2, false);
const cnt = dv.getUint16(altOff, false);
const newAlts: number[] = [];
for (let k = 0; k < cnt; k++) {
const g = gidLookup[dv.getUint16(altOff + 2 + k * 2, false)];
if (g >= 0) newAlts.push(g);
/** 反转遍历快路径(同 serializeMultipleSubst / serializeLigatureSubstcoverage
* gid coverage idx AlternateSet */
const subsetGids = currentSortedSubsetGids;
const covGidCount = coverageCount(r, covOff);
const useReverse = covGidCount > subsetGids.length * 4 && covGidCount > 16;
if (useReverse) {
for (const g of subsetGids) {
const idx = coverageIndexOf(r, covOff, g);
if (idx < 0 || idx >= altCount) continue;
const fromNew = gidLookup[g];
if (!(fromNew >= 0)) continue;
const altOff = off + dv.getUint16(off + 6 + idx * 2, false);
const cnt = dv.getUint16(altOff, false);
const newAlts: number[] = [];
for (let k = 0; k < cnt; k++) {
const gg = gidLookup[dv.getUint16(altOff + 2 + k * 2, false)];
if (gg >= 0) newAlts.push(gg);
}
if (newAlts.length > 0) entries.push({ from: fromNew, alts: newAlts });
}
} else {
const covGids = readCoverageGids(r, covOff);
for (let i = 0; i < covGids.length && i < altCount; i++) {
const fromNew = gidLookup[covGids[i]];
/** fromNew === undefinedcovGids[i] coverage gid numGlyphs
* subsetGids `fromNew < 0` undefinedNaN false */
if (!(fromNew >= 0)) continue;
const altOff = off + dv.getUint16(off + 6 + i * 2, false);
const cnt = dv.getUint16(altOff, false);
const newAlts: number[] = [];
for (let k = 0; k < cnt; k++) {
const g = gidLookup[dv.getUint16(altOff + 2 + k * 2, false)];
if (g >= 0) newAlts.push(g);
}
if (newAlts.length > 0) entries.push({ from: fromNew, alts: newAlts });
}
if (newAlts.length > 0) entries.push({ from: fromNew, alts: newAlts });
}
if (entries.length === 0) return false;
/** 按 from gid 升序排序,使 coverageemitCoverage 强制升序)与 alternate 数组保持下标配对 */
@ -629,37 +681,81 @@ function serializeLigatureSubst(
const dv = r.dv;
const covOff = off + r.u16(off + 2);
const setCount = r.u16(off + 4);
const covGids = readCoverageGids(r, covOff);
/** 每个 coverage 字形收集有效 ligature 列表 */
const entries: Array<{ from: number; ligs: Array<{ comp: number[]; lig: number }> }> = [];
for (let i = 0; i < covGids.length && i < setCount; i++) {
/** gidLookup[origGid] = gid -1 Map.get ~2×
* serializeLigatureSubst ligature remapGid CJK ligature */
const fromNew = gidLookup[covGids[i]];
if (fromNew < 0) continue;
const setOff = off + dv.getUint16(off + 6 + i * 2, false);
const ligCount = dv.getUint16(setOff, false);
const newLigs: Array<{ comp: number[]; lig: number }> = [];
for (let j = 0; j < ligCount; j++) {
const ligOff = setOff + dv.getUint16(setOff + 2 + j * 2, false);
const compCount = dv.getUint16(ligOff, false);
const ligNew = gidLookup[dv.getUint16(ligOff + 2, false)];
if (ligNew < 0) continue;
/** components 从第 2 字形开始(第 1 字形即 coverage 字形compCount 含第 1 字形 */
const compNew: number[] = [fromNew];
let ok = true;
for (let k = 0; k < compCount - 1; k++) {
const c = gidLookup[dv.getUint16(ligOff + 4 + k * 2, false)];
if (c < 0) {
ok = false;
break;
/** LigatureSubst coverage ~ gid/ type4
* readCoverageGids gidLookup
* gidcurrentSortedSubsetGids coverageIndexOf coverage idx
* idx LigatureSet off + 6 + idx * 2 serializeSingleSubst
* [[gsub-serialize-single-bigcov-reverse]] coverage */
const subsetGids = currentSortedSubsetGids;
const covGidCount = coverageCount(r, covOff);
const useReverse = covGidCount > subsetGids.length * 4 && covGidCount > 16;
if (useReverse) {
for (const g of subsetGids) {
const idx = coverageIndexOf(r, covOff, g);
if (idx < 0 || idx >= setCount) continue;
/** gidLookup[origGid] = gid -1 Map.get ~2×
* serializeLigatureSubst ligature remapGid CJK ligature */
const fromNew = gidLookup[g];
if (!(fromNew >= 0)) continue;
const setOff = off + dv.getUint16(off + 6 + idx * 2, false);
const ligCount = dv.getUint16(setOff, false);
const newLigs: Array<{ comp: number[]; lig: number }> = [];
for (let j = 0; j < ligCount; j++) {
const ligOff = setOff + dv.getUint16(setOff + 2 + j * 2, false);
const compCount = dv.getUint16(ligOff, false);
const ligNew = gidLookup[dv.getUint16(ligOff + 2, false)];
if (ligNew < 0) continue;
/** components 从第 2 字形开始(第 1 字形即 coverage 字形compCount 含第 1 字形 */
const compNew: number[] = [fromNew];
let ok = true;
for (let k = 0; k < compCount - 1; k++) {
const c = gidLookup[dv.getUint16(ligOff + 4 + k * 2, false)];
if (c < 0) {
ok = false;
break;
}
compNew.push(c);
}
compNew.push(c);
if (ok) newLigs.push({ comp: compNew, lig: ligNew });
}
if (ok) newLigs.push({ comp: compNew, lig: ligNew });
if (newLigs.length > 0) entries.push({ from: fromNew, ligs: newLigs });
}
} else {
const covGids = readCoverageGids(r, covOff);
for (let i = 0; i < covGids.length && i < setCount; i++) {
/** gidLookup[origGid] = gid -1 Map.get ~2×
* serializeLigatureSubst ligature remapGid CJK ligature */
const fromNew = gidLookup[covGids[i]];
/** fromNew === undefinedcovGids[i] 越界,损坏 coverage也跳过与反转路径行为一致 */
if (!(fromNew >= 0)) continue;
const setOff = off + dv.getUint16(off + 6 + i * 2, false);
const ligCount = dv.getUint16(setOff, false);
const newLigs: Array<{ comp: number[]; lig: number }> = [];
for (let j = 0; j < ligCount; j++) {
const ligOff = setOff + dv.getUint16(setOff + 2 + j * 2, false);
const compCount = dv.getUint16(ligOff, false);
const ligNew = gidLookup[dv.getUint16(ligOff + 2, false)];
if (ligNew < 0) continue;
/** components 从第 2 字形开始(第 1 字形即 coverage 字形compCount 含第 1 字形 */
const compNew: number[] = [fromNew];
let ok = true;
for (let k = 0; k < compCount - 1; k++) {
const c = gidLookup[dv.getUint16(ligOff + 4 + k * 2, false)];
if (c < 0) {
ok = false;
break;
}
compNew.push(c);
}
if (ok) newLigs.push({ comp: compNew, lig: ligNew });
}
if (newLigs.length > 0) entries.push({ from: fromNew, ligs: newLigs });
}
if (newLigs.length > 0) entries.push({ from: fromNew, ligs: newLigs });
}
if (entries.length === 0) return false;
/** 按 from gid 升序排序,使 coverageemitCoverage 强制升序)与 ligature set 数组保持下标配对 */

View File

@ -271,6 +271,9 @@ const testCases = [
{ label: "otf-五个汉字", fontPath: "font/temp/BaiHuOTFJiaoYuHanZi-2.otf", fontName: "白狐教育汉字", text: "天地黄宇宙法海波", sourceType: "otf" as const, outType: "woff2" as const, fullFormat: "opentype" },
{ label: "otf-思源黑体", fontPath: "font/temp/SourceHanSans-Regular.otf", fontName: "思源黑体", text: "天地玄黄宇宙洪法海波", sourceType: "otf" as const, outType: "ttf" as const, fullFormat: "opentype" },
{ label: "otf-思源黑体", fontPath: "font/temp/SourceHanSans-Regular.otf", fontName: "思源黑体", text: "天地玄黄宇宙洪法海波", sourceType: "otf" as const, outType: "woff2" as const, fullFormat: "opentype" },
/** type3 AlternateSubst coverage gid +
* serializeAlternateSubst coverage gid coverage undefined bug */
{ label: "otf-白狐千字文", fontPath: "font/temp/BaiHuOTFJiaoYuHanZi-2.otf", fontName: "白狐教育汉字", text: "天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳", sourceType: "otf" as const, outType: "woff2" as const, fullFormat: "opentype" },
/** ===== 小字号渲染size=24守护 SSIM 在小字号下不退化) ===== */
{ label: "小字号-8字", fontPath: "font/令东齐伋复刻体.ttf", fontName: "令东齐伋复刻体", text: "天地玄黄宇宙洪荒", sourceType: "ttf" as const, outType: "woff2" as const, fullFormat: "truetype", fontSize: 24 },