perf(gpos): subsetGPOS 全模块 origToNew(Map)→gidLookup(Int32Array)

套用 gsub 侧已验证的 Int32Array gidLookup 模式到 GPOS:
- subsetGPOS 入口构建 gidLookup(动态容量 maxOrigGid+1,fill -1)
- remapGid 的 Map.get(7处)+ subtableCoverageHits 的 Map.has 改数组索引
- 涉及函数签名全部 origToNew:Map → gidLookup:GidLookup

A/B 11 字体字节全 IDENTICAL;基准测试 SSIM 全通过。

性能 best-of N=2000:
- 思源千字文中段 min -2.6%
- 思源标点 min -2.5%
- 千字文96 min -1.2%
- FiraCode 持平(GSUB 已用 gidLookup,GPOS 增量小)

关键:gidLookup[越界] 返回 undefined(非 -1),GPOS coverage 常引用
大于子集 maxGid 的 gid,remapGid 须 origGid < length 保护(否则
undefined < 0 = false 误判有效,同 gsub type234 undefined bug)。
容量必须动态,65536 固定容量 fill 在小 GPOS 用例倒贴(篆体 +12.5%)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-25 05:36:49 +08:00
parent ae41a99e95
commit 0cfd9851df

View File

@ -123,6 +123,17 @@ export function subsetGPOS(
const dv = new DataView(gposBytes.buffer, gposBytes.byteOffset, gposBytes.byteLength);
const r = new Reader(dv);
/**
* 335gid gid Int32Array =gid=gid-1=
* origToNew.get/hasMap ~9ns vs ~1ns
* coverage/ClassDef/PairPos CJK
* subsetGids gid 65536 fill
* GPOS coverage maxGid gidremapGid -1 */
let maxOrigGid = 0;
for (const g of origToNew.keys()) if (g > maxOrigGid) maxOrigGid = g;
const gidLookup: GidLookup = new Int32Array(maxOrigGid + 1).fill(-1);
for (const [g, n] of origToNew) gidLookup[g] = n;
/** ---- GPOS Header ---- */
const major = dv.getUint16(0, false);
const minor = dv.getUint16(2, false);
@ -185,7 +196,7 @@ export function subsetGPOS(
const hits: boolean[] = [];
if (lk.supported) {
for (let j = 0; j < lk.subtableAbsOffs.length; j++) {
hits.push(subtableCoverageHits(r, lk.subtableAbsOffs[j], origToNew));
hits.push(subtableCoverageHits(r, lk.subtableAbsOffs[j], gidLookup));
}
}
subtableHits.push(hits);
@ -315,7 +326,7 @@ export function subsetGPOS(
for (let j = 0; j < lk.subtableAbsOffs.length; j++) {
subtableAbsPositions[j] = w.length;
r.clearError();
const ok = serializeSubtable(w, r, lk.subtableAbsOffs[j], lk.effectiveType, origToNew, hits[j]);
const ok = serializeSubtable(w, r, lk.subtableAbsOffs[j], lk.effectiveType, gidLookup, hits[j]);
/** 越界读取(异常表)也降级为保留原始 GPOS 字节(调用方安全降级) */
if (!ok || r.errorFlag) return null;
w.writeInt16At(subtableSlotsStart + j * 2, subtableAbsPositions[j] - lookupStart);
@ -359,7 +370,7 @@ function serializeSubtable(
r: Reader,
subAbs: number,
type: number,
origToNew: Map<number, number>,
gidLookup: GidLookup,
/** 预扫描结果:该 subtable coverage 是否含任意子集字形。false 时 coverage 必空,直接写空 subtable 跳过全量解析 */
coverageHit: boolean,
): boolean {
@ -370,15 +381,22 @@ function serializeSubtable(
writeEmptyPosSubtable(w, type);
return true;
}
if (type === LT_SINGLE_POS) return serializeSinglePos(w, r, subAbs, origToNew);
if (type === LT_PAIR_POS) return serializePairPos(w, r, subAbs, origToNew);
if (type === LT_SINGLE_POS) return serializeSinglePos(w, r, subAbs, gidLookup);
if (type === LT_PAIR_POS) return serializePairPos(w, r, subAbs, gidLookup);
return false;
}
/** 重映射原 gid不在子集中返回 -1 */
function remapGid(origGid: number, origToNew: Map<number, number>): number {
const ng = origToNew.get(origGid);
return ng === undefined ? -1 : ng;
/** gid gid =gid=gid-1= Map.get
* GPOS coverage/ClassDef/PairPos CJK */
type GidLookup = Int32Array;
/** gid -1
* 335gidLookupInt32Array ~1ns origToNew.getMap.get ~9ns
* gidLookup[g] >= 0 origToNew.has(g) gidg >= lengthGPOS coverage
* maxGid gid -1gidLookup[] undefined`undefined < 0` false
* [[gsub-serialize-type234-no-hotspot]] undefined bug */
function remapGid(origGid: number, gidLookup: GidLookup): number {
return origGid < gidLookup.length ? gidLookup[origGid] : -1;
}
/**
@ -386,7 +404,7 @@ function remapGid(origGid: number, origToNew: Map<number, number>): number {
* lookup subtable coverageOff +2SinglePos/PairPos format
* coverage gid origToNew true readCoverageGids +
*/
function subtableCoverageHits(r: Reader, subAbs: number, origToNew: Map<number, number>): boolean {
function subtableCoverageHits(r: Reader, subAbs: number, gidLookup: GidLookup): boolean {
const dv = r.dv;
const len = dv.byteLength;
const coverageOff = r.u16(subAbs + 2);
@ -398,7 +416,7 @@ function subtableCoverageHits(r: Reader, subAbs: number, origToNew: Map<number,
const base = covAbs + 4;
if (base + count * 2 > len) return false;
for (let i = 0; i < count; i++) {
if (origToNew.has(dv.getUint16(base + i * 2, false))) return true;
if (gidLookup[dv.getUint16(base + i * 2, false)] >= 0) return true;
}
return false;
}
@ -411,7 +429,7 @@ function subtableCoverageHits(r: Reader, subAbs: number, origToNew: Map<number,
const end = dv.getUint16(p + 2, false);
/** range 内逐 gid 查子集range 通常很短CJK 标点压缩覆盖分散),逐个可接受且命中即退出 */
for (let g = start; g <= end; g++) {
if (origToNew.has(g)) return true;
if (gidLookup[g] >= 0) return true;
}
p += 6;
}
@ -583,7 +601,7 @@ function emitClassDefFromClassMap(w: Writer, classMap: Map<number, number>): boo
function readClassDefMap(
r: Reader,
classDefAbs: number,
origToNew: Map<number, number>,
gidLookup: GidLookup,
): Map<number, number> | null {
const out = new Map<number, number>();
const dv = r.dv;
@ -592,7 +610,7 @@ function readClassDefMap(
const startGlyph = dv.getUint16(classDefAbs + 2, false);
const glyphCount = dv.getUint16(classDefAbs + 4, false);
for (let i = 0; i < glyphCount; i++) {
const ng = remapGid(startGlyph + i, origToNew);
const ng = remapGid(startGlyph + i, gidLookup);
if (ng < 0) continue;
out.set(ng, dv.getUint16(classDefAbs + 6 + i * 2, false));
}
@ -604,7 +622,7 @@ function readClassDefMap(
const end = dv.getUint16(recOff + 2, false);
const cls = dv.getUint16(recOff + 4, false);
for (let g = start; g <= end; g++) {
const ng = remapGid(g, origToNew);
const ng = remapGid(g, gidLookup);
if (ng >= 0) out.set(ng, cls);
}
}
@ -623,7 +641,7 @@ function serializeSinglePos(
w: Writer,
r: Reader,
subAbs: number,
origToNew: Map<number, number>,
gidLookup: GidLookup,
): boolean {
const fmt = r.u16(subAbs);
const coverageOff = r.u16(subAbs + 2);
@ -637,7 +655,7 @@ function serializeSinglePos(
const sz = vrCount(valueFormat);
const kept: Array<{ newGid: number; valueAbs: number }> = [];
for (let idx = 0; idx < origGids.length; idx++) {
const ng = remapGid(origGids[idx], origToNew);
const ng = remapGid(origGids[idx], gidLookup);
if (ng < 0) continue;
if (fmt === 1) {
/** format1 所有 glyph 共用 subAbs + 6 处的 ValueRecord */
@ -681,17 +699,17 @@ function serializePairPos(
w: Writer,
r: Reader,
subAbs: number,
origToNew: Map<number, number>,
gidLookup: GidLookup,
): boolean {
const fmt = r.u16(subAbs);
const coverageOff = r.u16(subAbs + 2);
const valueFormat1 = r.u16(subAbs + 4);
const valueFormat2 = r.u16(subAbs + 6);
if (fmt === 1) {
return serializePairPosFormat1(w, r, subAbs, coverageOff, valueFormat1, valueFormat2, origToNew);
return serializePairPosFormat1(w, r, subAbs, coverageOff, valueFormat1, valueFormat2, gidLookup);
}
if (fmt === 2) {
return serializePairPosFormat2(w, r, subAbs, coverageOff, valueFormat1, valueFormat2, origToNew);
return serializePairPosFormat2(w, r, subAbs, coverageOff, valueFormat1, valueFormat2, gidLookup);
}
return false;
}
@ -707,7 +725,7 @@ function serializePairPosFormat1(
coverageOff: number,
valueFormat1: number,
valueFormat2: number,
origToNew: Map<number, number>,
gidLookup: GidLookup,
): boolean {
const firstGids = readCoverageGids(r, subAbs + coverageOff);
if (!firstGids) return false;
@ -719,7 +737,7 @@ function serializePairPosFormat1(
/** 重建每个保留 firstGlyph 的 PairSet */
const rebuilt: Array<{ newFirstGid: number; pairSetBytes: Uint8Array }> = [];
for (let idx = 0; idx < pairSetCount; idx++) {
const newFirstGid = remapGid(firstGids[idx], origToNew);
const newFirstGid = remapGid(firstGids[idx], gidLookup);
if (newFirstGid < 0) continue;
const pairSetOff = subAbs + dv.getUint16(subAbs + 10 + idx * 2, false);
const pairValueCount = dv.getUint16(pairSetOff, false);
@ -727,7 +745,7 @@ function serializePairPosFormat1(
const keptSeconds: Array<{ newSecondGid: number; recAbs: number }> = [];
for (let p = 0; p < pairValueCount; p++) {
const recAbs = pairSetOff + 2 + p * (2 + vr1 * 2 + vr2 * 2);
const newSecondGid = remapGid(dv.getUint16(recAbs, false), origToNew);
const newSecondGid = remapGid(dv.getUint16(recAbs, false), gidLookup);
if (newSecondGid < 0) continue;
keptSeconds.push({ newSecondGid, recAbs });
}
@ -785,15 +803,15 @@ function serializePairPosFormat2(
coverageOff: number,
valueFormat1: number,
valueFormat2: number,
origToNew: Map<number, number>,
gidLookup: GidLookup,
): boolean {
const classDef1Off = r.u16(subAbs + 8);
const classDef2Off = r.u16(subAbs + 10);
const class2Count = r.u16(subAbs + 14);
/** 解析原始 classDef → 新gid -> 原classclassDef 未出现的 gid 默认 class 0 */
const gidToClass1 = readClassDefMap(r, subAbs + classDef1Off, origToNew);
const gidToClass2 = readClassDefMap(r, subAbs + classDef2Off, origToNew);
const gidToClass1 = readClassDefMap(r, subAbs + classDef1Off, gidLookup);
const gidToClass2 = readClassDefMap(r, subAbs + classDef2Off, gidLookup);
if (!gidToClass1 || !gidToClass2) return false;
/** 收集实际命中的 classclass 0 永远保留) */
@ -827,7 +845,7 @@ function serializePairPosFormat2(
if (!covGids) return false;
const newCovGids: number[] = [];
for (const g of covGids) {
const ng = remapGid(g, origToNew);
const ng = remapGid(g, gidLookup);
if (ng >= 0) newCovGids.push(ng);
}
newCovGids.sort((a, b) => a - b);