perf(cff-subset): 三处全量解析改按需查询,思源 subsetCFF -77%

CID CFF 子集化原先对 numGlyphs(思源 65535) 全量解析三处,子集仅 11 字时是纯浪费:
1. readIndex(CharStrings) 遍历 65536 个 offset(0.435ms)→ 直接读 count+offSize,
   按 subsetGid 随机读 2 个 offset 取字节区间。
2. readCharsetCIDs 填充 65535 CID 数组 → lookupCharsetCID 遍历 range 查单个 gid
   (思源 format2 约 6 千 range,省去 65535 项填充;range 起始 gid 累积计算)。
3. parseFDSelect 填充 65535 FD 数组 → lookupFDSelect format3 二分 range 查单个 gid。

subsetCFF 1.577→0.357ms(-77%);subsetOTF 思源 3.56→1.89ms(-47%),基准 avg 5.5→3.7ms。
字节级零回归(思源/白狐子集字节与优化前 cmp 完全一致),SSIM 不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-24 23:05:54 +08:00
parent 67439be486
commit e37593a552

View File

@ -194,35 +194,31 @@ interface FdSelectRange {
}
/**
* FDSelect gid FD index
* 0format(1) + numGlyphs×uint8 count
* 3format(1) + nRanges(u16) + ranges[3]×nRanges + sentinel(u16)
* gid FD index parseFDSelect
* newSubsetGids FD numGlyphs( 65535)
* 0format(1) + numGlyphs×uint8 gid
* 3format(1) + nRanges(u16) + ranges[first(u16),fd(u8)]×nRanges + sentinel(u16)
* first<=gid rangerange [first, range.first)
* @param b CFF
* @param fdSelectOff FDSelect
* @param numGlyphs
* @param gid gid
*/
function parseFDSelect(b: Uint8Array, fdSelectOff: number, numGlyphs: number): number[] {
function lookupFDSelect(b: Uint8Array, fdSelectOff: number, gid: number): number {
const fmt = b[fdSelectOff];
const fds = new Array<number>(numGlyphs).fill(0);
if (fmt === 0) {
for (let i = 0; i < numGlyphs; i++) fds[i] = b[fdSelectOff + 1 + i];
return fds;
}
/** format 3 */
if (fmt === 0) return b[fdSelectOff + 1 + gid];
/** format 3ranges 起始 = fdSelectOff + 3每 range 3 字节 */
const nRanges = (b[fdSelectOff + 1] << 8) | b[fdSelectOff + 2];
const ranges: FdSelectRange[] = [];
let p = fdSelectOff + 3;
for (let i = 0; i < nRanges; i++) {
const first = (b[p] << 8) | b[p + 1];
const fd = b[p + 2];
ranges.push({ first, fd });
p += 3;
const rangesStart = fdSelectOff + 3;
/** 二分:找最大 i 使 ranges[i].first <= gid返回该 range 的 fd */
let lo = 0;
let hi = nRanges - 1;
while (lo < hi) {
const mid = (lo + hi + 1) >> 1;
const first = (b[rangesStart + mid * 3] << 8) | b[rangesStart + mid * 3 + 1];
if (first <= gid) lo = mid;
else hi = mid - 1;
}
for (let i = 0; i < nRanges; i++) {
const next = i + 1 < nRanges ? ranges[i + 1].first : numGlyphs;
for (let g = ranges[i].first; g < next; g++) fds[g] = ranges[i].fd;
}
return fds;
return b[rangesStart + lo * 3 + 2];
}
/**
@ -321,43 +317,54 @@ export function subsetCFF(cffBytes: Uint8Array, subsetGids: number[]): Uint8Arra
return null;
}
const charStringsIndex = readIndex(b, charStringsOff);
/** 子集字形数 = CharStrings 数(含 .notdef */
const numGlyphs = charStringsIndex.count;
/** CharStrings INDEX count + offSize 65535 offset 0.4ms
* newSubsetGids gid offset */
const csCount = (b[charStringsOff] << 8) | b[charStringsOff + 1];
const csOffSize = b[charStringsOff + 2];
/** offset 数组起始(紧跟 count+offSize 3 字节dataStart = 偏移数组尾 + 1 */
const csOffArrStart = charStringsOff + 3;
const csDataStart = csOffArrStart + (csCount + 1) * csOffSize;
if (subsetGids.length === 0) return null;
/** .notdefgid 0必须保留且 subsetGids[0] 应为 0 */
const newSubsetGids = subsetGids[0] === 0 ? subsetGids : [0, ...subsetGids];
/** 重建 CharStrings INDEX按 newSubsetGids 顺序透传原 charstring 字节 */
/** CharStrings INDEX newSubsetGids charstring
* gid 2 offset offset */
const newCharStringObjects: { bytes: Uint8Array; start: number; len: number }[] = [];
for (const gid of newSubsetGids) {
const s = charStringsIndex.dataStart + charStringsIndex.offsets[gid] - 1;
const e = charStringsIndex.dataStart + charStringsIndex.offsets[gid + 1] - 1;
/** 读 offset[gid] 与 offset[gid+1]offSize 字节大端) */
let o0 = 0;
let o1 = 0;
const p0 = csOffArrStart + gid * csOffSize;
const p1 = csOffArrStart + (gid + 1) * csOffSize;
for (let j = 0; j < csOffSize; j++) o0 = (o0 << 8) | b[p0 + j];
for (let j = 0; j < csOffSize; j++) o1 = (o1 << 8) | b[p1 + j];
const s = csDataStart + o0 - 1;
const e = csDataStart + o1 - 1;
newCharStringObjects.push({ bytes: b, start: s, len: e - s });
}
const newCharStrings = writeIndex(newCharStringObjects);
/** charsetCID-keyed charset gidCID 0/1/2 newSubsetGids CID
* CID 0 .notdefgid 0 charset charset 0
* format(1) + (numGlyphs-1)×CID(u16)charset gid 0 CID 0 */
const origCids = readCharsetCIDs(b, charsetOff, numGlyphs);
* format(1) + (numGlyphs-1)×CID(u16)charset gid 0 CID 0
* CIDlookupCharsetCID range gid 65535 */
const newSubsetNumGlyphs = newSubsetGids.length;
const newCharsetBody: number[] = [];
for (let i = 1; i < newSubsetNumGlyphs; i++) {
/** newSubsetGids[i] 是原始 gid取其原 CID */
newCharsetBody.push(origCids[newSubsetGids[i]] ?? 0);
newCharsetBody.push(lookupCharsetCID(b, charsetOff, newSubsetGids[i]));
}
const newCharset = encodeCharsetFormat0(newCharsetBody);
/** FDSelect原始 gid→FD index重映射为新 gid→新 FD index */
const origFds = parseFDSelect(b, fdSelectOff, numGlyphs);
/** FDSelect按需查每个 subsetGid 的原 FDlookupFDSelect 二分 range不全量展开 numGlyphs。 */
/** 收集命中的 FD保持首次出现顺序构建 原FD→新FD 映射 */
const fdRemap = new Map<number, number>();
const usedFds: number[] = [];
for (const gid of newSubsetGids) {
const fd = origFds[gid] ?? 0;
const fd = lookupFDSelect(b, fdSelectOff, gid);
if (!fdRemap.has(fd)) {
fdRemap.set(fd, usedFds.length);
usedFds.push(fd);
@ -417,7 +424,7 @@ export function subsetCFF(cffBytes: Uint8Array, subsetGids: number[]): Uint8Arra
* FD 3 ranges CID OTS */
const newGidToFd: number[] = new Array(newSubsetNumGlyphs);
for (let i = 0; i < newSubsetNumGlyphs; i++) {
const origFd = origFds[newSubsetGids[i]] ?? 0;
const origFd = lookupFDSelect(b, fdSelectOff, newSubsetGids[i]);
newGidToFd[i] = fdRemap.get(origFd) ?? 0;
}
const newFdSelectBody = encodeFDSelect(newGidToFd);
@ -630,41 +637,46 @@ function combineBytes(parts: Uint8Array[]): Uint8Array {
}
/**
* charset gid CIDgid 0 CID 0
* 0format(1) + (numGlyphs-1)×CID(u16)
* 1format(1) + ranges[first(2), nLeft(1)]
* 2format(1) + ranges[first(2), nLeft(2)]
* gid CID readCharsetCIDs
* newSubsetGids CID numGlyphs( 65535)
* gid 0 CID 0.notdef
* 0format(1) + (numGlyphs-1)×CID(u16) gid charset gid 0
* 1/2 range gid range first gidCID = first CID + (gid - range.first)
* range numGlyphs format2 6 range vs 65535 gid 65535
* @param b CFF
* @param charsetOff charset
* @param numGlyphs
* @param gid gid>0gid 0 0
*/
function readCharsetCIDs(b: Uint8Array, charsetOff: number, numGlyphs: number): number[] {
const cids = new Array<number>(numGlyphs).fill(0);
cids[0] = 0; /** .notdef */
function lookupCharsetCID(b: Uint8Array, charsetOff: number, gid: number): number {
if (gid === 0) return 0;
const fmt = b[charsetOff];
let p = charsetOff + 1;
let gid = 1;
/** 格式 0format(1) + (numGlyphs-1)×CID(u16) 紧排gid i(>0) 的 CID 在 (i-1)*2 */
if (fmt === 0) {
while (gid < numGlyphs) {
cids[gid++] = (b[p] << 8) | b[p + 1];
p += 2;
}
} else if (fmt === 1) {
while (gid < numGlyphs) {
const first = (b[p] << 8) | b[p + 1];
const o = charsetOff + 1 + (gid - 1) * 2;
return (b[o] << 8) | b[o + 1];
}
/** 1/2range[ firstCID, nLeft ]range gid gid 1
* range nLeft+1 gid CID = firstCID + gid gid range
* range numGlyphs format2 6 range vs 65535 gid 65535 */
let p = charsetOff + 1;
let rangeFirstGid = 1;
if (fmt === 1) {
for (;;) {
const firstCID = (b[p] << 8) | b[p + 1];
const nLeft = b[p + 2];
if (gid >= rangeFirstGid && gid <= rangeFirstGid + nLeft) return firstCID + (gid - rangeFirstGid);
rangeFirstGid += nLeft + 1;
p += 3;
for (let i = 0; i <= nLeft && gid < numGlyphs; i++) cids[gid++] = first + i;
}
} else if (fmt === 2) {
while (gid < numGlyphs) {
const first = (b[p] << 8) | b[p + 1];
const nLeft = (b[p + 2] << 8) | b[p + 3];
p += 4;
for (let i = 0; i <= nLeft && gid < numGlyphs; i++) cids[gid++] = first + i;
}
}
return cids;
/** fmt === 2 */
for (;;) {
const firstCID = (b[p] << 8) | b[p + 1];
const nLeft = (b[p + 2] << 8) | b[p + 3];
if (gid >= rangeFirstGid && gid <= rangeFirstGid + nLeft) return firstCID + (gid - rangeFirstGid);
rangeFirstGid += nLeft + 1;
p += 4;
}
}
/** 编码 charset 格式 0format(1) + CIDs.length×CID(u16) */