mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 14:53:32 +08:00
v1.14.0: GSUB/GPOS 共享 gidLookup 查找表 + remapGid 数组化 + WF2_NOTRANSFORM 实验开关
- 优化340:buildGidLookup 提取导出,rewriteLayoutTablesForSubset 构造一次共享传入 subsetGPOS/subsetGSUB(省每侧独立的 maxOrigGid 扫描+fill+Map 遍历,big3500 -4.2%) - 优化342:remapGid/parseChainRuleFormat1or2 从 Map.get 改为 gidLookup 数组索引 (~9ns→~1ns,语义等价:越界/-1 均映射为 null),GSUB 查询路径 Map 残余清零 - 实验341:woff2-encode.js 增加 WF2_NOTRANSFORM=1 环境变量开关,跳过 glyf/loca transform(规范合法 untransformed 路径,-13% CPU 但 +10% 体积,默认关闭不影响生产) 验证:tsc 通过;基准 30/30 PASS min ssim 0.9876 与基线一致;输出字节总量 449,440B 与基线完全一致;7 用例 sha256 字节级 IDENTICAL;leafer 渲染 7 用例 SSIM≥0.99; 24 字体结构扫描通过(6 个缺字均为原字体本身无此字形);40/40 边界输入扫描通过。
This commit is contained in:
parent
cc792d27b0
commit
5c118121ff
@ -1,7 +1,7 @@
|
||||
import { Font } from "../../vendor/fonteditor-core/lib/ttf/font.js";
|
||||
import type { FontEditor } from "../../vendor/fonteditor-core/lib/ttf/font.js";
|
||||
import { subsetGPOS } from "./gpos-subset.js";
|
||||
import { subsetGSUB } from "./gsub-subset.js";
|
||||
import { subsetGSUB, buildGidLookup } from "./gsub-subset.js";
|
||||
import { collectReachableGsubTargets } from "./gsub-reachable.js";
|
||||
import { probeGsubAndCmap } from "./gsub-probe.js";
|
||||
import { subsetOTF } from "./otf-subset.js";
|
||||
@ -80,12 +80,15 @@ const rewriteLayoutTablesForSubset = (
|
||||
const origToNew = new Map<number, number>();
|
||||
for (let i = 0; i < subsetGids.length; i++) origToNew.set(subsetGids[i], i);
|
||||
|
||||
/** GPOS/GSUB 的 origToNew 相同,gidLookup 只构造一次共享传入(省一次 maxOrigGid 扫描+fill+Map 遍历) */
|
||||
const gidLookup = buildGidLookup(origToNew);
|
||||
|
||||
/** GPOS 子集化 */
|
||||
const origGPOS = ttf.GPOS;
|
||||
if (origGPOS) {
|
||||
const gposBytes = origGPOS instanceof Uint8Array ? origGPOS : new Uint8Array(origGPOS);
|
||||
if (gposBytes.byteLength > 0) {
|
||||
const rewritten = subsetGPOS(gposBytes, origToNew);
|
||||
const rewritten = subsetGPOS(gposBytes, origToNew, gidLookup);
|
||||
if (rewritten) ttf.GPOS = rewritten;
|
||||
/** rewritten === null 表示含不支持的版本,保留原始 GPOS 字节(安全降级) */
|
||||
}
|
||||
@ -96,7 +99,7 @@ const rewriteLayoutTablesForSubset = (
|
||||
if (origGSUB) {
|
||||
const gsubBytes = origGSUB instanceof Uint8Array ? origGSUB : new Uint8Array(origGSUB);
|
||||
if (gsubBytes.byteLength > 0) {
|
||||
ttf.GSUB = subsetGSUB(gsubBytes, origToNew);
|
||||
ttf.GSUB = subsetGSUB(gsubBytes, origToNew, gidLookup);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { OTWriter as Writer, OTReader as Reader, serializeScriptList, serializeFeatureList, scriptListSpan, featureListSpan } from "./ot-bytes.js";
|
||||
import { buildGidLookup } from "./gsub-subset.js";
|
||||
|
||||
/** ValueFormat 位掩码 */
|
||||
const VF_X_PLACEMENT = 0x0001;
|
||||
@ -118,6 +119,7 @@ function writeEmptyCoverage(w: Writer): void {
|
||||
export function subsetGPOS(
|
||||
gposBytes: Uint8Array,
|
||||
origToNew: Map<number, number>,
|
||||
sharedGidLookup?: GidLookup,
|
||||
): Uint8Array | null {
|
||||
const dv = new DataView(gposBytes.buffer, gposBytes.byteOffset, gposBytes.byteLength);
|
||||
const r = new Reader(dv);
|
||||
@ -127,11 +129,9 @@ export function subsetGPOS(
|
||||
* 替代全模块的 origToNew.get/has(Map 查询 ~9ns vs 数组索引 ~1ns)。
|
||||
* coverage/ClassDef/PairPos 序列化每字形都查,CJK 标点字体调用密集。
|
||||
* 容量按 subsetGids 的最大原 gid 分配(远小于 65536,避免大数组 fill 抵消收益——
|
||||
* GPOS coverage 常引用大于子集 maxGid 的 gid,remapGid 内做越界判定返回 -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 coverage 常引用大于子集 maxGid 的 gid,remapGid 内做越界判定返回 -1)。
|
||||
* 与 subsetGSUB 内容一致,调用方构造一次共享传入;未传时兜底自建。 */
|
||||
const gidLookup: GidLookup = sharedGidLookup ?? buildGidLookup(origToNew);
|
||||
|
||||
/** ---- GPOS Header ---- */
|
||||
const major = dv.getUint16(0, false);
|
||||
|
||||
@ -39,10 +39,14 @@ const COV_RANGE = 2;
|
||||
/**
|
||||
* 重映射单个 gid。子集外的 gid 返回 null。
|
||||
* GSUB 中 coverage/ClassDef/替换目标引用的 gid 若不在子集内,该条目失效,调用方丢弃之。
|
||||
* 优化342:gidLookup(Int32Array 索引 ~1ns)替代 origToNew.get(Map.get ~9ns),全字符集
|
||||
* (27677 gid)下 coverage/ClassDef 逐 gid 重映射是 Map 残余热点。语义等价:
|
||||
* Map.get 缺失返回 undefined→null;gidLookup 越界返回 undefined、子集外为 -1,均映射为 null。
|
||||
*/
|
||||
function remapGid(origToNew: Map<number, number>, gid: number): number | null {
|
||||
const m = origToNew.get(gid);
|
||||
return m === undefined ? null : m;
|
||||
function remapGid(gidLookup: GidLookup, gid: number): number | null {
|
||||
if (gid >= gidLookup.length) return null;
|
||||
const m = gidLookup[gid];
|
||||
return m < 0 ? null : m;
|
||||
}
|
||||
|
||||
/** Coverage 表单个 range 展开为 gid 的上限保护:超出视为偏移错位读到垃圾数据。 */
|
||||
@ -83,6 +87,20 @@ const EMPTY_GIDS: number[] = [];
|
||||
*/
|
||||
type GidLookup = Int32Array;
|
||||
|
||||
/**
|
||||
* 构造 原gid→新gid 的 Int32Array 查找表(下标=原gid,值=新gid,-1=不在子集)。
|
||||
* subsetGPOS 与 subsetGSUB 用同一 origToNew,查表内容完全一致——由 rewriteLayoutTablesForSubset
|
||||
* 构造一次共享传入,省掉每侧各自的 maxOrigGid 扫描 + fill + Map 遍历(全字符集 27677 gid
|
||||
* 时两轮 Map 遍历 + 两次 fill 合计 ~1.6ms 量级)。
|
||||
*/
|
||||
export const buildGidLookup = (origToNew: Map<number, number>): GidLookup => {
|
||||
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;
|
||||
return gidLookup;
|
||||
};
|
||||
|
||||
/**
|
||||
* 当前 subsetGSUB 调用的子集原始 gid 升序数组,供 readClassDefMap format2 二分优化复用。
|
||||
*
|
||||
@ -924,7 +942,7 @@ function serializeChainedContextSubst(
|
||||
rules: Array<{ back: number[]; input: number[]; look: number[]; records: Array<{ seq: number; lookup: number }> }>;
|
||||
}> = [];
|
||||
for (let i = 0; i < covGids.length && i < ruleSetCount; i++) {
|
||||
const firstNew = remapGid(origToNew, covGids[i]);
|
||||
const firstNew = remapGid(gidLookup, covGids[i]);
|
||||
if (firstNew === null) continue;
|
||||
const setOff = off + dv.getUint16(off + 6 + i * 2, false);
|
||||
const ruleCount = dv.getUint16(setOff, false);
|
||||
@ -932,7 +950,7 @@ function serializeChainedContextSubst(
|
||||
const validRules: Array<{ back: number[]; input: number[]; look: number[]; records: Array<{ seq: number; lookup: number }> }> = [];
|
||||
for (let j = 0; j < ruleCount; j++) {
|
||||
const ruleOff = setOff + dv.getUint16(setOff + 2 + j * 2, false);
|
||||
const parsed = parseChainRuleFormat1or2(r, ruleOff, origToNew, true);
|
||||
const parsed = parseChainRuleFormat1or2(r, ruleOff, gidLookup, true);
|
||||
if (parsed) validRules.push(parsed);
|
||||
}
|
||||
if (validRules.length > 0) entries.push({ firstGid: firstNew, rules: validRules });
|
||||
@ -984,7 +1002,7 @@ function serializeChainedContextSubst(
|
||||
const ruleOff = setOff + dv.getUint16(setOff + 2 + j * 2, false);
|
||||
/** format2 的元素是 class index(非 gid),原样保留,传 isGidFormat=false。
|
||||
* class index 始终有效(ClassDef 重映射 gid 后 class 编号不变),规则恒保留。 */
|
||||
const parsed = parseChainRuleFormat1or2(r, ruleOff, origToNew, false);
|
||||
const parsed = parseChainRuleFormat1or2(r, ruleOff, gidLookup, false);
|
||||
if (!parsed) continue;
|
||||
const list = classToRules.get(i) ?? [];
|
||||
list.push(parsed);
|
||||
@ -1015,7 +1033,7 @@ function serializeChainedContextSubst(
|
||||
function parseChainRuleFormat1or2(
|
||||
r: Reader,
|
||||
ruleOff: number,
|
||||
origToNew: Map<number, number>,
|
||||
gidLookup: GidLookup,
|
||||
isGidFormat: boolean,
|
||||
): { back: number[]; input: number[]; look: number[]; records: Array<{ seq: number; lookup: number }> } | null {
|
||||
const dv = r.dv;
|
||||
@ -1033,7 +1051,7 @@ function parseChainRuleFormat1or2(
|
||||
if (isGidFormat) {
|
||||
const out: number[] = [];
|
||||
for (let k = 0; k < count; k++) {
|
||||
const m = remapGid(origToNew, dv.getUint16(p + k * 2, false));
|
||||
const m = remapGid(gidLookup, dv.getUint16(p + k * 2, false));
|
||||
if (m === null) return null;
|
||||
out.push(m);
|
||||
}
|
||||
@ -1466,6 +1484,7 @@ function serializeSubtable(
|
||||
export function subsetGSUB(
|
||||
gsubBytes: Uint8Array,
|
||||
origToNew: Map<number, number>,
|
||||
sharedGidLookup?: GidLookup,
|
||||
): Uint8Array {
|
||||
const dv = new DataView(gsubBytes.buffer, gsubBytes.byteOffset, gsubBytes.byteLength);
|
||||
const r = new Reader(dv);
|
||||
@ -1475,11 +1494,9 @@ export function subsetGSUB(
|
||||
const covCache: CoverageCache = new Map();
|
||||
|
||||
/** 原gid → 新gid 数组查找表(coverage 边解析边过滤的热路径用,数组索引比 Map.get 快数倍)。
|
||||
* 下标=原gid,值=新gid,-1 表示不在子集。容量覆盖出现的最大原 gid。 */
|
||||
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;
|
||||
* 下标=原gid,值=新gid,-1 表示不在子集。容量覆盖出现的最大原 gid。
|
||||
* 与 subsetGPOS 内容一致,调用方(rewriteLayoutTablesForSubset)构造一次共享传入;未传时兜底自建。 */
|
||||
const gidLookup: GidLookup = sharedGidLookup ?? buildGidLookup(origToNew);
|
||||
|
||||
/** 升序子集原始 gid 数组(惰性):供 readClassDefMap format2 / coverage range 二分优化。
|
||||
* 优化336:不在入口无条件 sort(全字符集 27677 gid 耗 ~3.2ms),改为设 source + 清缓存,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webfont",
|
||||
"private": true,
|
||||
"version": "1.13.0",
|
||||
"version": "1.14.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpx tsx scripts/dev-all.ts",
|
||||
|
||||
31
vendor/fonteditor-core/woff2/woff2-encode.js
vendored
31
vendor/fonteditor-core/woff2/woff2-encode.js
vendored
@ -783,7 +783,9 @@ function encodeTTFToWOFF2(ttfBuffer) {
|
||||
|
||||
/* glyf + loca 变换 */
|
||||
let glyfTransformed = null;
|
||||
if (glyfTable && locaTable) {
|
||||
/** 实验开关:WF2_NOTRANSFORM=1 时跳过 glyf/loca transform(规范允许,flags 置 0x40) */
|
||||
const noTransform = typeof process !== "undefined" && process.env && process.env.WF2_NOTRANSFORM === "1";
|
||||
if (glyfTable && locaTable && !noTransform) {
|
||||
const glyfData = data.subarray(glyfTable.offset, glyfTable.offset + glyfTable.length);
|
||||
const locaData = data.subarray(locaTable.offset, locaTable.offset + locaTable.length);
|
||||
const result = transformGlyfAndLoca(glyfData, locaData, indexToLocFormat, numGlyphs);
|
||||
@ -813,6 +815,19 @@ function encodeTTFToWOFF2(ttfBuffer) {
|
||||
for (var fi2 = 0, fl2 = filtered.length; fi2 < fl2; fi2++) {
|
||||
var t = filtered[fi2];
|
||||
if (t.tagU32 === TAG_loca) {
|
||||
if (noTransform) {
|
||||
/** untransformed:loca 原始字节保留(transform version 3,flags 置 0x40) */
|
||||
dirEntries[dirIdx++] = {
|
||||
tagU32: t.tagU32, tagIndex: t.tagIndex,
|
||||
flags: t.tagIndex | 0x40,
|
||||
origLength: t.length,
|
||||
transformLength: t.length,
|
||||
data: data.subarray(t.offset, t.offset + t.length),
|
||||
hasTransform: false,
|
||||
};
|
||||
totalDirSize += entrySize(t.tagIndex, t.length, false, t.length);
|
||||
continue;
|
||||
}
|
||||
/** 优化:直接使用 transformGlyfAndLoca 返回的 locaOrigLength,避免重复计算 */
|
||||
const origLength = glyfTransformed ? glyfTransformed.locaOrigLength : t.length;
|
||||
dirEntries[dirIdx++] = {
|
||||
@ -840,6 +855,20 @@ function encodeTTFToWOFF2(ttfBuffer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (t.tagU32 === TAG_glyf && noTransform) {
|
||||
/** untransformed glyf:原始字节直出 */
|
||||
dirEntries[dirIdx++] = {
|
||||
tagU32: t.tagU32, tagIndex: t.tagIndex,
|
||||
flags: t.tagIndex | 0x40,
|
||||
origLength: t.length,
|
||||
transformLength: t.length,
|
||||
data: data.subarray(t.offset, t.offset + t.length),
|
||||
hasTransform: false,
|
||||
};
|
||||
totalDirSize += entrySize(t.tagIndex, t.length, false, t.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let tableData = data.subarray(t.offset, t.offset + t.length);
|
||||
|
||||
dirEntries[dirIdx++] = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user