perf(cff): 跳过无 CALLSUBR 字形的 rewriteCharstring

collectSubrRefs 返回是否含 CALLSUBR,subsetCFF 据此标记每个字形。重建 CharStrings
时仅含 CALLSUBR 的字形走 rewriteCharstring(重映射 local subr 编号),无 CALLSUBR 的
直接透传原字节——含 CALLGSUBR 但无 CALLSUBR 的透传等价(global subr 不子集化,
operand 原样)。

思源千字文 241 字形仅 65(27%)含 callsubr,176 个无 callsubr 字形原先白白跑一遍
rewriteCharstring 完整字节扫描。subsetCFF 0.74→0.51ms(-30%)。

三 OTF case CFF 输出 sha 全 IDENTICAL(纯等价变换)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-25 07:17:12 +08:00
parent 7a76208558
commit d2b3b1939f
2 changed files with 19 additions and 21 deletions

View File

@ -1,18 +0,0 @@
import { readFileSync } from 'fs';
import { fontSubset } from './backend/font_util/font.js';
import supportMod from './vendor/fonteditor-core/lib/ttf/table/support.js';
const support = (supportMod as any).default || supportMod;
const hmtxT = support.hmtx;
const origRead = hmtxT.prototype.read;
hmtxT.prototype.read = function(reader: any, ttf: any) {
const r = origRead.call(this, reader, ttf);
if (ttf.subsetGids) {
let mx = 0;
for (const g of ttf.subsetGids) if (g>mx) mx=g;
console.log(`numGlyphs=${ttf.maxp.numGlyphs} subsetGids.len=${ttf.subsetGids.length} maxGid=${mx} alloc=${ttf.maxp.numGlyphs*2*4}B needed=${(mx+1)*2*4}B`);
}
return r;
};
const buf = readFileSync('font/令东齐伋复刻体.ttf');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
fontSubset(ab, '天地玄黄宇宙洪荒', {sourceType:'ttf',outType:'ttf'});

View File

@ -225,17 +225,22 @@ export function collectSubrRefs(
localCount: number,
localRefs: Set<number>,
gsubrRefs: Set<number>,
): void {
): boolean {
/**
* stackLen + lastVal stack: number[]
* HSTEM stemCountCALLSUBR/CALLGSUBR
* operand push stack.length=0
* lastVal stackLen>0 CALLSUBR operand push
* CALLSUBRlocal subr CALLSUBR charstring
* CALLGSUBR operand global subr rewrite
* gsubr subr charstring rewriteCharstring
*/
let p = start;
let stackLen = 0;
let lastVal = NaN;
let stemCount = 0;
/** 是否遇到 CALLSUBR决定该 charstring 是否需 rewriteCharstring */
let hasSubr = false;
while (p < end) {
const b0 = b[p++];
if (b0 === 255) {
@ -274,6 +279,7 @@ export function collectSubrRefs(
p += (stemCount + 7) >>> 3;
stackLen = 0;
} else if (b0 === T2_CALLSUBR) {
hasSubr = true;
if (Number.isInteger(lastVal)) {
const sn = lastVal + localBias;
if (sn >= 0 && sn < localCount) localRefs.add(sn);
@ -290,6 +296,7 @@ export function collectSubrRefs(
}
}
}
return hasSubr;
}
/**
@ -668,6 +675,12 @@ export function subsetCFF(cffBytes: Uint8Array, subsetGids: number[]): Uint8Arra
charStringRanges[gi] = { start: csDataStart + o0 - 1, end: csDataStart + o1 - 1 };
}
/** charstring CALLSUBRlocal subr
* CALLSUBR rewriteCharstring local subr CALLGSUBR
* CALLSUBR global subr operand collectSubrRefs
* CharStrings CALLSUBR rewrite 73% callsubr */
const gidHasSubr: Uint8Array = new Uint8Array(newSubsetNumGlyphs);
/** 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
@ -771,7 +784,10 @@ export function subsetCFF(cffBytes: Uint8Array, subsetGids: number[]): Uint8Arra
for (let i = 0; i < newSubsetNumGlyphs; i++) {
if (gidOrigFds[i] !== fd) continue;
const r = charStringRanges[i];
collectSubrRefs(b, r.start, r.end, info.localBias, idx.count, refs, dummyGsubrRefs);
/** 记录该字形 charstring 是否含 CALLSUBR决定后续是否 rewrite */
if (collectSubrRefs(b, r.start, r.end, info.localBias, idx.count, refs, dummyGsubrRefs)) {
gidHasSubr[i] = 1;
}
}
}
}
@ -840,7 +856,7 @@ export function subsetCFF(cffBytes: Uint8Array, subsetGids: number[]): Uint8Arra
for (let fi = 0; fi < usedFds.length; fi++) {
if (usedFds[fi] === origFd) { privInfo = fdInfos[fi].priv; break; }
}
if (privInfo && privInfo.localRemap) {
if (privInfo && privInfo.localRemap && gidHasSubr[gi]) {
const rewritten = rewriteCharstring(b, r.start, r.end, privInfo.localBias, privInfo.localRemap, privInfo.newLocalCount);
newCharStringObjects.push({ bytes: rewritten, start: 0, len: rewritten.length });
} else {