perf(gpos): ScriptList/FeatureList 整块拷贝跳过逐字段序列化

两表不含 glyphId(仅引用 lookup index),绝大多数字体其子表紧凑排列在
[listOff, listOff+span) 内、与下一表无物理交错,字节块本身即合法表。
新增 scriptListSpan/featureListSpan 计算跨度并校验自洽,满足则 subarray
整块拷贝,否则降级 serializeScriptList/serializeFeatureList。

收益:初夏 subsetGPOS 0.270→0.226ms,纯标点 0.240→0.165(-31%),
思源 0.071→0.036(-49%)。顺带保留 fontTools 的 FeatureTable 去重
(serialize 重排丢失致初夏 FeatureList 1840→3728B),输出 GPOS 字节
初夏 12064→10176(-16%)、思源 1194→978(-18%)。全基准 SSIM 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-24 11:16:21 +08:00
parent 8c311768f6
commit ccc2e995c8
2 changed files with 93 additions and 3 deletions

View File

@ -14,7 +14,7 @@
* @reference https://learn.microsoft.com/en-us/typography/opentype/spec/gpos
*/
import { OTWriter as Writer, OTReader as Reader, serializeScriptList, serializeFeatureList } from "./ot-bytes.js";
import { OTWriter as Writer, OTReader as Reader, serializeScriptList, serializeFeatureList, scriptListSpan, featureListSpan } from "./ot-bytes.js";
/** ValueFormat 位掩码 */
const VF_X_PLACEMENT = 0x0001;
@ -180,8 +180,24 @@ export function subsetGPOS(
* - MarkBase/Context gid
* gid coverage gid
*/
const scriptListBytes = serializeScriptList(r, scriptListOff);
const featureListBytes = serializeFeatureList(r, featureListOff);
/** ScriptList / FeatureList
* glyphId lookup index [listOff, listOff+span)
* ScriptList/FeatureList subarray
* serializeScriptList/serializeFeatureList u16 GPOS 0.087ms
* fontTools LangSys/FeatureTable serialize FL 18403728B
* span serialize */
const slSpan = scriptListSpan(r, scriptListOff);
const slContiguous = slSpan >= 0 && scriptListOff + slSpan <= featureListOff;
const scriptListBytes = slContiguous
? gposBytes.subarray(scriptListOff, scriptListOff + slSpan)
: serializeScriptList(r, scriptListOff);
const flSpan = featureListSpan(r, featureListOff);
const flContiguous = flSpan >= 0 && featureListOff + flSpan <= lookupListOff;
const featureListBytes = flContiguous
? gposBytes.subarray(featureListOff, featureListOff + flSpan)
: serializeFeatureList(r, featureListOff);
/** span 函数可能因越界读取 set errorFlagclear 后后续 serializeSubtable 的越界检测仍有效 */
r.clearError();
/** ScriptList/FeatureList 解析失败(异常表)则整体降级返回 null调用方保留原始 GPOS 字节) */
if (!scriptListBytes || !featureListBytes) return null;

View File

@ -159,6 +159,80 @@ export class OTReader {
}
}
/**
* ScriptList listAbs
*
* ScriptList glyphIdScriptTable/LangSys listAbs
* // GPOS GSUB ScriptList
* [listAbs, listAbs+span) FeatureList/LookupList
* ScriptList fontTools LangSys
* serializeScriptList OTF 5381066B
*
* ScriptTable/LangSys span -1
* errorFlag serializeScriptListspan
* span
*
* @param r
* @param listAbs ScriptList
* @returns ScriptList 0 -1
*/
export function scriptListSpan(r: OTReader, listAbs: number): number {
r.clearError();
const scriptCount = r.u16(listAbs);
/** span = listAbs 起,覆盖 ScriptRecord 数组 + 所有 ScriptTable 及其 LangSys 的结束位置 */
let span = 2 + scriptCount * 6;
for (let i = 0; i < scriptCount; i++) {
/** ScriptRecord 偏移相对 listAbs */
const scriptRel = r.u16(listAbs + 2 + i * 6 + 4);
const scriptAbs = listAbs + scriptRel;
const defaultLangSysOff = r.u16(scriptAbs);
const langSysCount = r.u16(scriptAbs + 2);
/** ScriptTable 头defaultLangSysOff(2) + langSysCount(2) + LangSysRecord[langSysCount](6) */
span = Math.max(span, scriptRel + 4 + langSysCount * 6);
/** defaultLangSys 表lookupOrder(2)+reqFeatureIdx(2)+featureIdxCount(2)+indices */
if (defaultLangSysOff !== 0) {
const dlAbs = scriptAbs + defaultLangSysOff;
const dlRel = scriptRel + defaultLangSysOff;
const fic = r.u16(dlAbs + 4);
span = Math.max(span, dlRel + 6 + fic * 2);
}
/** 各 LangSys 表 */
for (let li = 0; li < langSysCount; li++) {
const lsRel = r.u16(scriptAbs + 4 + li * 6 + 4);
const lsAbs = scriptAbs + lsRel;
const fic = r.u16(lsAbs + 4);
span = Math.max(span, scriptRel + lsRel + 6 + fic * 2);
}
}
return r.errorFlag ? -1 : span;
}
/**
* FeatureList listAbs
*
* FeatureList glyphIdFeatureTable listAbsfontTools
* FeatureTable FeatureRecord FeatureTable GPOS 296 feature
* 1 FeatureTableserializeFeatureList feature 296
* 18403728B FeatureTable [listAbs, listAbs+span)
*
*
* @returns FeatureList 0 -1
*/
export function featureListSpan(r: OTReader, listAbs: number): number {
r.clearError();
const featureCount = r.u16(listAbs);
let span = 2 + featureCount * 6;
for (let i = 0; i < featureCount; i++) {
/** FeatureRecord 偏移相对 listAbs */
const ftRel = r.u16(listAbs + 2 + i * 6 + 4);
const ftAbs = listAbs + ftRel;
const lookupIndexCount = r.u16(ftAbs + 2);
/** FeatureTable: featureParamsOff(2) + lookupIndexCount(2) + indices */
span = Math.max(span, ftRel + 4 + lookupIndexCount * 2);
}
return r.errorFlag ? -1 : span;
}
/**
* ScriptListGPOS/GSUB
*