feat(otf): 独立 OTF(CFF) 子集化器,修复思源等 CID 字体 OTS 拒绝与 gid 错位

fonteditor-core 对含 idRangeOffset 的 CID cmap 解析有 bug,产出 gid 错乱子集(SSIM 0.93~0.97)。
新增独立 otf-subset + cff-subset,直接重建 CFF/cmap/hmtx,透传 charstring。

核心修复:
1. cff-subset 透传各 FD 的 Local Subr INDEX(op 19 指向)+ patch op19 新偏移。
   原仅透传 Private DICT 字节未带 Local Subr INDEX,思源等字形用 callsubr 引用本地 subr,
   子集 op19 指向越界致 OTS "Failed to parse Top DICT Data" 拒绝加载。
2. otf-subset 接入 subsetGSUB/subsetGPOS 按 subsetGids 做 gid 重映射。
   原透传原始 GSUB/GPOS 字节 gid 未重映射,coverage 引用旧 gid 指向错字,
   致 shaping 把字符替换成 .notdef(思源 otf SSIM 0.623,洪/法等字变空白)。
   OTF 与 TTF 的 GSUB/GPOS 表结构相同,字节级子集化器可直接复用。

结果:思源 otf OTS 拒绝→可加载,SSIM 0.623→0.953,白狐 otf SSIM 1.0,
逐字 charstring 字节级与原始一致,渲染像素级一致(5 种独立验证 diff=0)。
所有非 otf 用例无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-07-24 22:48:14 +08:00
parent f4af6f7b68
commit bebb8503f4
5 changed files with 1327 additions and 10 deletions

View File

@ -0,0 +1,740 @@
/**
* CFF (Compact Font Format) OTF CFF
*
* fonteditor-core OTF otf2ttfobjectCFF glyf
* glyf CFF glyf
* OTF SSIM 0.93~0.97ink
*
* CFF CID-keyed charset/FDSelect/CharStrings/
* FDArray/Private subsetGids charstring charstring
* callsubr/callgsubr Global Subr INDEX FD Local Subr INDEX
* / OTF ink OTF
* SSIM 1.0
*
* CID-keyed CFFTop DICT ROS / FDArray / FDSelect CIDType 2 name-keyed
* charstring null
*
* @reference https://learn.microsoft.com/en-us/typography/opentype/spec/cff
*/
/** CFF INDEX 解析结果。offsets 为 1-basedobject i 的字节区间 = [dataStart+offsets[i]-1, dataStart+offsets[i+1]-1) */
interface CffIndex {
/** INDEX 起始偏移 */
start: number;
/** INDEX 内对象数量 */
count: number;
/** 偏移量字节宽度1~4 */
offSize: number;
/** (count+1) 个 1-based 偏移量 */
offsets: number[];
/** 数据区起始位置(紧接偏移量数组之后) */
dataStart: number;
/** INDEX 结束位置(= 最后一个 object 的尾) */
end: number;
}
/**
* CFF INDEX count + offSize + offsets + data
* @param b CFF
* @param pos INDEX
*/
function readIndex(b: Uint8Array, pos: number): CffIndex {
const count = (b[pos] << 8) | b[pos + 1];
/** count=0 的 INDEX 仅 2 字节 */
if (count === 0) return { start: pos, count: 0, offSize: 0, offsets: [], dataStart: pos + 2, end: pos + 2 };
const offSize = b[pos + 2];
let op = pos + 3;
const offsets: number[] = new Array(count + 1);
for (let i = 0; i <= count; i++) {
let v = 0;
for (let j = 0; j < offSize; j++) v = (v << 8) | b[op++];
offsets[i] = v;
}
/** offsets 是 1-basedobject i 的数据从 dataStart + offsets[i] - 1 开始 */
const dataStart = op;
return { start: pos, count, offSize, offsets, dataStart, end: dataStart + offsets[count] - 1 };
}
/** 取一个已解析 INDEX 的完整字节切片(含头部+偏移量+数据,[start, end) */
function getIndexBytes(b: Uint8Array, idx: CffIndex): Uint8Array {
return b.subarray(idx.start, idx.end);
}
/** CFF Top/Private DICT 解析结果:操作码键 → 操作数数组。双字节操作码 12,n 存为 (12<<8)|n */
type CffDict = Map<number, number[]>;
/**
* CFF DICT
* CFF §3.1
* 32~246 1 v-139
* 247~250 2 (v-247)*256+b+108
* 251~254 2 -(v-251)*256-b-108
* 28 2 int16
* 29 4 int32
* 12
* <31 12 单字节操作码
* @param b DICT
* @param start DICT
* @param end DICT
*/
function parseDict(b: Uint8Array, start: number, end: number): CffDict {
const dict: CffDict = new Map();
const operands: number[] = [];
let p = start;
while (p < end) {
const b0 = b[p++];
if (b0 <= 21) {
/** 操作码 */
let op = b0;
if (b0 === 12) op = (12 << 8) | b[p++];
dict.set(op, operands.splice(0, operands.length));
} else if (b0 === 28) {
operands.push(((b[p] << 24) | (b[p + 1] << 16)) >> 16);
p += 2;
} else if (b0 === 29) {
operands.push(((b[p] << 24) | (b[p + 1] << 16) | (b[p + 2] << 8) | b[p + 3]) | 0);
p += 4;
} else if (b0 >= 32 && b0 <= 246) {
operands.push(b0 - 139);
} else if (b0 >= 247 && b0 <= 250) {
operands.push((b0 - 247) * 256 + b[p] + 108);
p += 1;
} else if (b0 >= 251 && b0 <= 254) {
operands.push(-(b0 - 251) * 256 - b[p] - 108);
p += 1;
}
}
return dict;
}
/**
* DICT CFF §3.1
* @param v
*/
function encodeDictInt(v: number): number[] {
if (v >= -107 && v <= 107) return [v + 139];
if (v >= 108 && v <= 1131) {
const v0 = v - 108;
return [247 + (v0 >> 8), v0 & 0xff];
}
if (v >= -1131 && v <= -108) {
const v0 = -v - 108;
return [251 + (v0 >> 8), v0 & 0xff];
}
if (v >= -32768 && v <= 32767) return [28, (v >> 8) & 0xff, v & 0xff];
return [29, (v >>> 24) & 0xff, (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff];
}
/**
* CFF INDEXcount + offSize + (count+1)*offSize +
* offSize 1-based offset=1
* @param objects Uint8Array {start,len}
*/
function writeIndex(objects: { bytes: Uint8Array; start: number; len: number }[]): Uint8Array {
const count = objects.length;
if (count === 0) return new Uint8Array(2); /** count=0 的空 INDEX */
/** 累计数据长度,算最大 offset含末尾哨兵 */
let totalData = 0;
for (const o of objects) totalData += o.len;
const maxOffset = totalData + 1;
/** 选 offSize1~4 字节 */
let offSize = 1;
if (maxOffset > 0xffff) offSize = 4;
else if (maxOffset > 0xff) offSize = 2;
let tmp = maxOffset;
while (tmp > 0xff && offSize < 4) {
offSize++;
tmp >>>= 8;
}
const offsetsSize = (count + 1) * offSize;
const totalSize = 2 + 1 + offsetsSize + totalData;
const out = new Uint8Array(totalSize);
out[0] = (count >> 8) & 0xff;
out[1] = count & 0xff;
out[2] = offSize;
/** 写偏移量1-based大端 offSize 字节) */
let op = 3;
let acc = 1;
const writeOffset = (v: number) => {
for (let s = (offSize - 1) * 8; s >= 0; s -= 8) out[op++] = (v >>> s) & 0xff;
};
writeOffset(acc);
for (const o of objects) {
acc += o.len;
writeOffset(acc);
}
/** 写数据 */
let dp = 3 + offsetsSize;
for (const o of objects) {
out.set(o.bytes.subarray(o.start, o.start + o.len), dp);
dp += o.len;
}
return out;
}
/** CFF 操作码键(单字节直接用值,双字节用 (12<<8)|n */
const OP_charset = 15;
const OP_charStrings = 17;
const OP_Private = 18;
/** Private DICT 内Local Subr INDEX 相对 Private 起始的偏移 */
const OP_LocalSubr = 19;
const OP_FDArray = (12 << 8) | 36;
const OP_FDSelect = (12 << 8) | 37;
const OP_ROS = (12 << 8) | 30;
/** CFF FDSelect 格式 3 的单个 range首 glyph index + FD index */
interface FdSelectRange {
first: number;
fd: number;
}
/**
* FDSelect gid FD index
* 0format(1) + numGlyphs×uint8 count
* 3format(1) + nRanges(u16) + ranges[3]×nRanges + sentinel(u16)
* @param b CFF
* @param fdSelectOff FDSelect
* @param numGlyphs
*/
function parseFDSelect(b: Uint8Array, fdSelectOff: number, numGlyphs: 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 */
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;
}
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;
}
/**
* FDSelect FD FD 0 FD 3 ranges
* 0format(1) + numGlyphs×uint8
* 3format(1) + nRanges(u16) + ranges[first(u16), fd(u8)]×nRanges + sentinel(u16)
* FD 3 CID OTS format 0
* @param gidToFd gid FD gid 0
*/
function encodeFDSelect(gidToFd: number[]): Uint8Array {
/** 单 FD所有字形同一个 FD用格式 0 */
let singleFd = gidToFd.length > 0 ? gidToFd[0] : 0;
let isSingle = gidToFd.length > 0;
for (const fd of gidToFd) {
if (fd !== singleFd) { isSingle = false; break; }
}
if (isSingle) {
const out = new Uint8Array(1 + gidToFd.length);
out[0] = 0;
for (let i = 0; i < gidToFd.length; i++) out[1 + i] = gidToFd[i];
return out;
}
/** 多 FD格式 3 ranges。连续相同 FD 的 gid 合并为一个 range。 */
const ranges: FdSelectRange[] = [];
let curFirst = 0;
let curFd = gidToFd[0];
for (let i = 1; i < gidToFd.length; i++) {
if (gidToFd[i] !== curFd) {
ranges.push({ first: curFirst, fd: curFd });
curFirst = i;
curFd = gidToFd[i];
}
}
ranges.push({ first: curFirst, fd: curFd });
const nRanges = ranges.length;
const out = new Uint8Array(3 + nRanges * 3 + 2);
out[0] = 3; /** format 3 */
out[1] = (nRanges >> 8) & 0xff;
out[2] = nRanges & 0xff;
let p = 3;
for (const r of ranges) {
out[p] = (r.first >> 8) & 0xff;
out[p + 1] = r.first & 0xff;
out[p + 2] = r.fd;
p += 3;
}
/** sentinel = numGlyphs最后一个 range 之后的第一个 gid */
const sentinel = gidToFd.length;
out[p] = (sentinel >> 8) & 0xff;
out[p + 1] = sentinel & 0xff;
return out;
}
/**
* CID CFF
*
*
* 1. Header / Name INDEX / Top DICT INDEX / String INDEX / Global Subr INDEX Top DICT offset
* 2. subsetGids charset / FDSelect / CharStrings INDEX
* 3. FD FDArray FD DICT + Private+ FDSelect FD
* 4. patch Top DICT charset / charStrings / FDArray / FDSelect offset
*
* @param cffBytes CFF
* @param subsetGids gid 0 = .notdef gid =
* @returns CFF CID null
*/
export function subsetCFF(cffBytes: Uint8Array, subsetGids: number[]): Uint8Array | null {
const b = cffBytes;
/** Header: major(1) minor(1) hdrSize(1) offSize(1) */
const hdrSize = b[2];
/** Name INDEX 紧接 Header */
const nameIndex = readIndex(b, hdrSize);
/** Top DICT INDEX 紧接 Name INDEX */
const topDictIndex = readIndex(b, nameIndex.end);
if (topDictIndex.count < 1) return null;
/** Top DICT 数据 */
const topDictDataStart = topDictIndex.dataStart + topDictIndex.offsets[0] - 1;
const topDictDataEnd = topDictIndex.dataStart + topDictIndex.offsets[1] - 1;
const topDict = parseDict(b, topDictDataStart, topDictDataEnd);
/** 非 CID 字体(无 ROS走 name-keyed 结构,当前不支持 */
if (!topDict.has(OP_ROS)) return null;
/** String INDEX 紧接 Top DICT INDEXGlobal Subr INDEX 紧接其后 */
const stringIndex = readIndex(b, topDictIndex.end);
const globalSubrIndex = readIndex(b, stringIndex.end);
/** Top DICT 中各结构表的绝对偏移(相对 CFF 起始) */
const charStringsOff = topDict.get(OP_charStrings)?.[0];
const charsetOff = topDict.get(OP_charset)?.[0];
const fdArrayOff = topDict.get(OP_FDArray)?.[0];
const fdSelectOff = topDict.get(OP_FDSelect)?.[0];
if (charStringsOff === undefined || charsetOff === undefined || fdArrayOff === undefined || fdSelectOff === undefined) {
return null;
}
const charStringsIndex = readIndex(b, charStringsOff);
/** 子集字形数 = CharStrings 数(含 .notdef */
const numGlyphs = charStringsIndex.count;
if (subsetGids.length === 0) return null;
/** .notdefgid 0必须保留且 subsetGids[0] 应为 0 */
const newSubsetGids = subsetGids[0] === 0 ? subsetGids : [0, ...subsetGids];
/** 重建 CharStrings INDEX按 newSubsetGids 顺序透传原 charstring 字节 */
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;
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);
const newSubsetNumGlyphs = newSubsetGids.length;
const newCharsetBody: number[] = [];
for (let i = 1; i < newSubsetNumGlyphs; i++) {
/** newSubsetGids[i] 是原始 gid取其原 CID */
newCharsetBody.push(origCids[newSubsetGids[i]] ?? 0);
}
const newCharset = encodeCharsetFormat0(newCharsetBody);
/** FDSelect原始 gid→FD index重映射为新 gid→新 FD index */
const origFds = parseFDSelect(b, fdSelectOff, numGlyphs);
/** 收集命中的 FD保持首次出现顺序构建 原FD→新FD 映射 */
const fdRemap = new Map<number, number>();
const usedFds: number[] = [];
for (const gid of newSubsetGids) {
const fd = origFds[gid] ?? 0;
if (!fdRemap.has(fd)) {
fdRemap.set(fd, usedFds.length);
usedFds.push(fd);
}
}
/** FDArray FD DICTpatch Private [len, offset] Private
* Private offset Private Local Subr INDEXop 19
* INDEX Private patch op 19
* op 19 /OTS Local Subr "Failed to parse Top DICT Data"
* charstring callsubr subr + bias INDEX */
const fdArrayIndex = readIndex(b, fdArrayOff);
/** 每个 usedFd 对应的 (FD DICT 原字节, Private 信息) */
interface PrivInfo {
/** Private 段在原 CFF 的绝对偏移(-1 表示无 Private */
origOff: number;
/** Private 段长度(仅 DICT 字节,不含 Local Subr INDEX */
len: number;
/** Local Subr INDEX 原始字节(无则 null。思源等 CID 字体字形通过 callsubr 引用本地 subr */
localSubr: Uint8Array | null;
}
interface FdInfo { dictBytes: Uint8Array; priv: PrivInfo; }
/** 原始 Private 段去重:相同 origOff 的 Private 共享同一份(含其 Local Subr */
const privSegCache = new Map<number, PrivInfo>();
const fdInfos: FdInfo[] = [];
for (const fd of usedFds) {
const s = fdArrayIndex.dataStart + fdArrayIndex.offsets[fd] - 1;
const e = fdArrayIndex.dataStart + fdArrayIndex.offsets[fd + 1] - 1;
const dictBytes = b.subarray(s, e);
const fdDict = parseDict(b, s, e);
const priv = fdDict.get(OP_Private);
/** 无 Private 的 FD极罕见原样透传 */
if (!priv || priv.length < 2) {
fdInfos.push({ dictBytes, priv: { origOff: -1, len: 0, localSubr: null } });
continue;
}
const privLen = priv[0];
const privOrigOff = priv[1];
let info = privSegCache.get(privOrigOff);
if (!info) {
/** 解析 Private DICT查 op 19Local Subr INDEX 相对 Private 起始的偏移) */
const privDict = parseDict(b, privOrigOff, privOrigOff + privLen);
const subrRel = privDict.get(OP_LocalSubr)?.[0];
let localSubr: Uint8Array | null = null;
if (subrRel !== undefined) {
/** Local Subr INDEX 紧接 Private DICT 字节之后(绝对偏移 = privOrigOff + subrRel */
const subrIdx = readIndex(b, privOrigOff + subrRel);
localSubr = b.subarray(subrIdx.start, subrIdx.end);
}
info = { origOff: privOrigOff, len: privLen, localSubr };
privSegCache.set(privOrigOff, info);
}
fdInfos.push({ dictBytes, priv: info });
}
/** FDSelect gid FD FD 01+numGlyphs
* FD 3 ranges CID OTS */
const newGidToFd: number[] = new Array(newSubsetNumGlyphs);
for (let i = 0; i < newSubsetNumGlyphs; i++) {
const origFd = origFds[newSubsetGids[i]] ?? 0;
newGidToFd[i] = fdRemap.get(origFd) ?? 0;
}
const newFdSelectBody = encodeFDSelect(newGidToFd);
/** CFFHeader + Name INDEX + Top DICT INDEX + String INDEX + Global Subr INDEX
* + charset + charStrings + FDArray + FDSelect + Private
* Top DICT offset patch */
const headerNameBytes = combineBytes([b.subarray(0, hdrSize), getIndexBytes(b, nameIndex)]);
const stringSeg = getIndexBytes(b, stringIndex);
const globalSubrSeg = getIndexBytes(b, globalSubrIndex);
/** Top DICT 原始字节(待 patch offset 后替换) */
const topDictBytes = b.subarray(topDictDataStart, topDictDataEnd);
/** 新结构段FDArray 段依赖 patch迭代中确定 */
const charsetSeg = newCharset;
const charStringsSeg = newCharStrings;
const fdSelectSeg = newFdSelectBody;
/** Top DICT patch + FD DICT patch + Private op19 patch
* headerName + TopDICT INDEX + String INDEX + GlobalSubr INDEX + charset + charStrings
* + FDArray INDEX + FDSelect + Private = Private DICT + Local Subr INDEX */
let topDictLen = topDictDataEnd - topDictDataStart;
let fdArrayTotalLen = 0;
/** 去重后的唯一 Private 段(按首次出现顺序),用于计算偏移与最终拼接 */
const uniquePrivInfos: PrivInfo[] = [];
const privOrigToUniqueIdx = new Map<number, number>();
for (const info of fdInfos) {
if (info.priv.origOff >= 0 && !privOrigToUniqueIdx.has(info.priv.origOff)) {
privOrigToUniqueIdx.set(info.priv.origOff, uniquePrivInfos.length);
uniquePrivInfos.push(info.priv);
}
}
/** 各唯一 Private 段 patch 后的 DICT 字节 + op19 新值迭代收敛op19 = patchedDICT.length */
let patchedPrivSegs: { dict: Uint8Array; subr: Uint8Array | null }[] = [];
let privSegsTotalLen = 0;
let patchedTopDict: Uint8Array = topDictBytes;
let newFdArrayBytes: Uint8Array = new Uint8Array(0);
for (let iter = 0; iter < 8; iter++) {
/** 先 patch 各唯一 Private DICTop19 指向新 DICT 长度Local Subr 紧跟其后) */
const curPatchedPriv: { dict: Uint8Array; subr: Uint8Array | null }[] = [];
for (const pi of uniquePrivInfos) {
const patchedPriv = patchPrivateDict(b, pi.origOff, pi.len, pi.localSubr !== null);
curPatchedPriv.push({ dict: patchedPriv, subr: pi.localSubr });
}
/** patched 私有段总长(含各自 Local Subr */
let curPrivTotal = 0;
for (const pp of curPatchedPriv) {
curPrivTotal += pp.dict.length;
if (pp.subr) curPrivTotal += pp.subr.length;
}
/** Top DICT INDEX 总长 = count(2)+offSize(1)+(count+1)*offSize + topDictLencount=1 */
const tdOffSize = patchedTopDictOffSize(topDictLen + 1);
const tdIdxTotalLen = 2 + 1 + 2 * tdOffSize + topDictLen;
const stringOff = headerNameBytes.length + tdIdxTotalLen;
const gsubrOff = stringOff + stringSeg.length;
const charsetOff = gsubrOff + globalSubrSeg.length;
const charStringsOff = charsetOff + charsetSeg.length;
const fdArrayOff = charStringsOff + charStringsSeg.length;
const fdSelectOff = fdArrayOff + fdArrayTotalLen;
const privateOff = fdSelectOff + fdSelectSeg.length;
/** 各唯一 Private 段在新 CFF 中的绝对偏移(顺序拼接,起始 privateOff */
let pAcc = privateOff;
const origToNewPrivOff = new Map<number, number>();
for (const [origOff, uid] of privOrigToUniqueIdx) {
origToNewPrivOff.set(origOff, pAcc);
pAcc += curPatchedPriv[uid].dict.length;
if (curPatchedPriv[uid].subr) pAcc += curPatchedPriv[uid].subr!.length;
}
/** patch FD DICT Private [len, offset]len = patchedDICT Local Subr
* offset = Private CFF Private len DICT Local Subr */
const patchedFdObjects: { bytes: Uint8Array; start: number; len: number }[] = [];
for (const info of fdInfos) {
if (info.priv.origOff < 0) {
patchedFdObjects.push({ bytes: info.dictBytes, start: 0, len: info.dictBytes.length });
} else {
const uid = privOrigToUniqueIdx.get(info.priv.origOff)!;
const newPrivOff = origToNewPrivOff.get(info.priv.origOff)!;
const newPrivLen = curPatchedPriv[uid].dict.length;
const patched = patchFdDictPrivate(info.dictBytes, newPrivLen, newPrivOff);
patchedFdObjects.push({ bytes: patched, start: 0, len: patched.length });
}
}
const candidateFdArray = writeIndex(patchedFdObjects);
/** patch Top DICT */
const candidateTopDict = replaceDictOffsets(topDictBytes, new Map<number, number>([
[OP_charset, charsetOff],
[OP_charStrings, charStringsOff],
[OP_FDArray, fdArrayOff],
[OP_FDSelect, fdSelectOff],
]));
/** 收敛判定topDictLen、fdArrayTotalLen、privSegsTotalLen 三者都不变 */
const tdConverged = candidateTopDict.length === topDictLen;
const fdConverged = candidateFdArray.length === fdArrayTotalLen;
const privConverged = curPrivTotal === privSegsTotalLen;
patchedTopDict = candidateTopDict;
newFdArrayBytes = candidateFdArray;
patchedPrivSegs = curPatchedPriv;
topDictLen = candidateTopDict.length;
fdArrayTotalLen = candidateFdArray.length;
privSegsTotalLen = curPrivTotal;
if (tdConverged && fdConverged && privConverged) break;
}
/** 组装 Top DICT INDEXcount=1 */
const newTopDictIndex = writeIndex([{ bytes: patchedTopDict, start: 0, len: patchedTopDict.length }]);
/** 拼接所有 Private 段(每段 = patched DICT + Local Subr INDEX */
const privParts: Uint8Array[] = [];
for (const pp of patchedPrivSegs) {
privParts.push(pp.dict);
if (pp.subr) privParts.push(pp.subr);
}
const newPrivateSeg = combineBytes(privParts);
/** Header+Name + TopDICT INDEX + String INDEX + GlobalSubr INDEX + charset + charStrings
* + FDArray INDEX + FDSelect + Private */
return combineBytes([headerNameBytes, newTopDictIndex, stringSeg, globalSubrSeg, charsetSeg, charStringsSeg, newFdArrayBytes, fdSelectSeg, newPrivateSeg]);
}
/** 计算 Top DICT INDEX 的 offSize容纳 topDictDataLen+1 的最小字节数1~4 */
function patchedTopDictOffSize(maxOffset: number): number {
if (maxOffset > 0xffff) return 4;
if (maxOffset > 0xff) return 2;
return 1;
}
/**
* patch FD DICT Private [length, offset]
* DICT 18Private [privLen, newPrivOff]
*
* @param dictBytes FD DICT
* @param privLen Private DICT Local Subr INDEX
* @param newPrivOff Private CFF
*/
function patchFdDictPrivate(dictBytes: Uint8Array, privLen: number, newPrivOff: number): Uint8Array {
/** 按操作码分段,找到 Private18替换其两个操作数 */
const chunks: Uint8Array[] = [];
let p = 0;
let operandStart = 0;
const len = dictBytes.length;
while (p < len) {
const b0 = dictBytes[p++];
if (b0 <= 21) {
let op = b0;
if (b0 === 12) op = (12 << 8) | dictBytes[p++];
if (op === OP_Private) {
/** 替换:编码 [privLen, newPrivOff] + 操作码 18 */
const enc1 = encodeDictInt(privLen);
const enc2 = encodeDictInt(newPrivOff);
const combined = new Uint8Array(enc1.length + enc2.length + 1);
combined.set(enc1, 0);
combined.set(enc2, enc1.length);
combined[enc1.length + enc2.length] = 18;
chunks.push(combined);
} else {
/** 保留原操作数 + 操作码 */
chunks.push(dictBytes.subarray(operandStart, p));
}
operandStart = p;
} else if (b0 === 28) {
p += 2;
} else if (b0 === 29) {
p += 4;
} else if (b0 >= 247 && b0 <= 254) {
p += 1;
}
}
return combineBytes(chunks);
}
/**
* patch Private DICT Local Subr op 19使 DICT
* Local Subr INDEX Private DICT op 19 = patched DICT
* op 19 DICT patch
* Local SubrhasSubr=false Private
* @param b CFF
* @param privOrigOff Private DICT CFF
* @param privLen Private DICT
* @param hasSubr Local Subr INDEXop 19
*/
function patchPrivateDict(b: Uint8Array, privOrigOff: number, privLen: number, hasSubr: boolean): Uint8Array {
/** 无 Local SubrDICT 原样透传offset 不需改Private 内无跨段引用) */
if (!hasSubr) return b.subarray(privOrigOff, privOrigOff + privLen);
/** 把 op 19 的操作数替换为 patchedDICT.length。迭代至 op19 编码长度稳定。 */
let cur = b.subarray(privOrigOff, privOrigOff + privLen);
for (let iter = 0; iter < 4; iter++) {
const patched = replaceDictOffsets(cur, new Map<number, number>([[OP_LocalSubr, cur.length]]));
if (patched.length === cur.length) return patched;
cur = patched;
}
return cur;
}
/** 拼接多个字节切片 */
function combineBytes(parts: Uint8Array[]): Uint8Array {
let total = 0;
for (const p of parts) total += p.length;
const out = new Uint8Array(total);
let off = 0;
for (const p of parts) {
out.set(p, off);
off += p.length;
}
return out;
}
/**
* 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)]
* @param b CFF
* @param charsetOff charset
* @param numGlyphs
*/
function readCharsetCIDs(b: Uint8Array, charsetOff: number, numGlyphs: number): number[] {
const cids = new Array<number>(numGlyphs).fill(0);
cids[0] = 0; /** .notdef */
const fmt = b[charsetOff];
let p = charsetOff + 1;
let gid = 1;
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 nLeft = b[p + 2];
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;
}
/** 编码 charset 格式 0format(1) + CIDs.length×CID(u16) */
function encodeCharsetFormat0(cids: number[]): Uint8Array {
const out = new Uint8Array(1 + cids.length * 2);
out[0] = 0;
for (let i = 0; i < cids.length; i++) {
out[1 + i * 2] = (cids[i] >> 8) & 0xff;
out[1 + i * 2 + 1] = cids[i] & 0xff;
}
return out;
}
/**
* patch Top DICT charset / charStrings / FDArray / FDSelect offset
* patch DICT DICT
* patch offset 3 1~2
*
* @param topDictBytes Top DICT
* @param newTopDictDataOff Top DICT CFF
* @param origTopDictLen Top DICT
* @param charsetSeg / charStringsSeg / fdArraySeg / fdSelectSeg Top DICT INDEX
*/
/**
* DICT offset
* DICT +
* @param dictBytes DICT
* @param replacements offset
*/
function replaceDictOffsets(dictBytes: Uint8Array, replacements: Map<number, number>): Uint8Array {
/** 先分段:按操作码切,重组 */
const chunks: number[][] = [];
let p = 0;
let operandStart = 0;
const len = dictBytes.length;
while (p < len) {
const b0 = dictBytes[p++];
if (b0 <= 21) {
let op = b0;
if (b0 === 12) op = (12 << 8) | dictBytes[p++];
if (replacements.has(op)) {
/** 替换:新操作数 + 操作码 */
const newVal = replacements.get(op)!;
const encoded = encodeDictInt(newVal);
if (op >= 256) {
chunks.push([...encoded, 12, op & 0xff]);
} else {
chunks.push([...encoded, op]);
}
} else {
/** 保留原操作数 + 操作码 */
chunks.push(Array.from(dictBytes.subarray(operandStart, p)));
}
operandStart = p;
} else if (b0 === 28) {
p += 2;
} else if (b0 === 29) {
p += 4;
} else if (b0 >= 247 && b0 <= 254) {
p += 1;
}
/** 32~246 单字节,无后续 */
}
/** 拼接 */
let total = 0;
for (const c of chunks) total += c.length;
const out = new Uint8Array(total);
let off = 0;
for (const c of chunks) {
for (const v of c) out[off++] = v;
}
return out;
}

View File

@ -4,6 +4,8 @@ import { subsetGPOS } from "./gpos-subset.js";
import { subsetGSUB } from "./gsub-subset.js";
import { collectReachableGsubTargets } from "./gsub-reachable.js";
import { probeGsubAndCmap } from "./gsub-probe.js";
import { subsetOTF } from "./otf-subset.js";
import { encodeTTFToWOFF2 } from "../../vendor/fonteditor-core/woff2/woff2-encode.js";
/** 优化291: TextEncoder 模块级单例 */
const textEncoder = new TextEncoder();
@ -129,6 +131,23 @@ export const fontSubset = (
): Uint8Array => {
const codePoints = textToCodePoints(subString);
/** OTFCFF OTF fonteditor-core idRangeOffset CID cmap bug
* gid SSIM 0.93~0.97subsetOTF CFF/cmap/hmtx charstring
* OTF SSIM 1.0outType=woff2 woff2 OTF
* WOFF2 glyf/loca CFF brotli outType=otf/ttf OTF
* GSUB/GPOS gid */
if (option.sourceType === "otf") {
const fontU8 = new Uint8Array(fontBuffer);
const otfBytes = subsetOTF(fontU8, codePoints, true);
if (otfBytes !== null) {
if (option.outType === "woff2") {
return encodeTTFToWOFF2(otfBytes);
}
return otfBytes;
}
/** subsetOTF 不支持(非 CID CFF 等)降级到原 fonteditor 路径 */
}
/** GSUB target glyph GSUB probe codepoint
* GSUB target glyph unicode FiraCode greater_equal.liga
* extraSubsetGids 使 target

View File

@ -0,0 +1,542 @@
/**
* OTF (OpenType with CFF)
*
* fonteditor-core OTF otf2ttfobjectCFF glyf cmap
* idRangeOffset format4 bug_lazySegs graphIdArrayIndexOffset
* CID-keyed OTF OTF cmap gid
* SSIM 0.93~0.97
*
* OTF fonteditor otf Font.create
* 1. sfnt
* 2. cmapformat4 + format12 codepoint gid fonteditor bug
* 3. subsetCFF CFF charstring
* 4. cmap codepoint gidformat4
* 5. hmtx/vmtx gid metrics
* 6. maxpnumGlyphs = postformat3 glyph name nameId
* 7. head/hhea/vhea/OS/2/VORG gid patch numGlyphs
* 8. GSUB/GPOS gsub-subset/gpos-subset
* 9. OTF sfntOTTO + checkSumAdjustment
*
* @reference https://learn.microsoft.com/en-us/typography/opentype/spec/
*/
import { subsetCFF } from "./cff-subset.js";
import { subsetGSUB } from "./gsub-subset.js";
import { subsetGPOS } from "./gpos-subset.js";
/** sfnt 表目录条目 */
interface SfntTable {
/** 4 字节 tag 字符串 */
tag: string;
/** 原始字节偏移 */
offset: number;
/** 原始字节长度 */
length: number;
}
/** 解析 sfnt 表目录 */
function readSfntTables(dv: DataView): SfntTable[] {
const numTables = dv.getUint16(4, false);
const tables: SfntTable[] = [];
for (let i = 0; i < numTables; i++) {
const off = 12 + i * 16;
const tag = String.fromCharCode(dv.getUint8(off), dv.getUint8(off + 1), dv.getUint8(off + 2), dv.getUint8(off + 3));
tables.push({ tag, offset: dv.getUint32(off + 8, false), length: dv.getUint32(off + 12, false) });
}
return tables;
}
/** 按 tag 查表,返回字节切片偏移/长度 */
function findTable(tables: SfntTable[], tag: string): SfntTable | undefined {
for (const t of tables) if (t.tag === tag) return t;
return undefined;
}
/** format4 cmap idRangeOffset
* gid0 / .notdef */
function lookupFormat4(dv: DataView, fmt4Off: number, unicode: number): number {
const segCountX2 = dv.getUint16(fmt4Off + 6, false);
const segCount = segCountX2 >>> 1;
const endCodeBase = fmt4Off + 14;
const startCodeBase = endCodeBase + segCount * 2 + 2;
const idDeltaBase = startCodeBase + segCount * 2;
const idRangeOffsetBase = idDeltaBase + segCount * 2;
let lo = 0;
let hi = segCount - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
const start = dv.getUint16(startCodeBase + mid * 2, false);
const end = dv.getUint16(endCodeBase + mid * 2, false);
if (unicode < start) {
hi = mid - 1;
} else if (unicode > end) {
lo = mid + 1;
} else {
const idDelta = dv.getInt16(idDeltaBase + mid * 2, false);
const idRangeOffset = dv.getUint16(idRangeOffsetBase + mid * 2, false);
if (idRangeOffset === 0) return ((unicode + idDelta) & 0xffff) & 0xffff;
/** idRangeOffset 是相对「idRangeOffset 字段自身地址」的字节偏移OpenType 规范) */
const idRangeOffsetAddr = idRangeOffsetBase + mid * 2;
const gidAddr = idRangeOffsetAddr + idRangeOffset + (unicode - start) * 2;
const gid = dv.getUint16(gidAddr, false);
if (gid === 0) return 0;
return (gid + idDelta) & 0xffff;
}
}
return 0;
}
/** format12 cmap 查找(补充平面 + BMP */
function lookupFormat12(dv: DataView, fmt12Off: number, unicode: number): number {
const numGroups = dv.getUint32(fmt12Off + 12, false);
/** groups 起始 = fmt12Off + 16每组 12 字节startCharCode(4) endCharCode(4) startGlyphID(4) */
let lo = 0;
let hi = numGroups - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
const gOff = fmt12Off + 16 + mid * 12;
const start = dv.getUint32(gOff, false);
const end = dv.getUint32(gOff + 4, false);
if (unicode < start) {
hi = mid - 1;
} else if (unicode > end) {
lo = mid + 1;
} else {
const startGlyphID = dv.getUint32(gOff + 8, false);
return startGlyphID + (unicode - start);
}
}
return 0;
}
/** 选 cmap 子表format12p3e10优先format4p3e1次之。返回各自相对 cmap 起始的偏移(-1 无) */
function selectCmapSubtables(dv: DataView, cmapOff: number): { fmt4Off: number; fmt12Off: number } {
const numSub = dv.getUint16(cmapOff + 2, false);
let fmt4Off = -1;
let fmt12Off = -1;
for (let i = 0; i < numSub; i++) {
const r = cmapOff + 4 + i * 8;
const pid = dv.getUint16(r, false);
const eid = dv.getUint16(r + 2, false);
const suboff = cmapOff + dv.getUint32(r + 4, false);
const fmt = dv.getUint16(suboff, false);
if (pid === 3 && eid === 10 && fmt === 12 && fmt12Off < 0) fmt12Off = suboff;
else if (pid === 3 && eid === 1 && fmt === 4 && fmt4Off < 0) fmt4Off = suboff;
else if (pid === 0 && fmt === 12 && fmt12Off < 0) fmt12Off = suboff;
else if (pid === 0 && fmt === 4 && fmt4Off < 0) fmt4Off = suboff;
}
return { fmt4Off, fmt12Off };
}
/**
* OTF codepoint gid .notdef
* readWindowsAllCodes BMP format4 format12 format12
* newSubsetGids[0, ... gid] codepointgid
*/
function buildSubsetGids(
dv: DataView,
cmapOff: number,
codePoints: number[],
): { subsetGids: number[]; cpToNewGid: Map<number, number> } {
const { fmt4Off, fmt12Off } = selectCmapSubtables(dv, cmapOff);
/** origGid 集合(保持插入顺序,新 gid = 数组 index */
const subsetGids: number[] = [0];
const seenGid = new Set<number>([0]);
const cpToNewGid = new Map<number, number>();
for (const cp of codePoints) {
if (cpToNewGid.has(cp)) continue;
let gid = 0;
if (cp < 0x10000 && fmt4Off >= 0) gid = lookupFormat4(dv, fmt4Off, cp);
if (gid === 0 && fmt12Off >= 0) gid = lookupFormat12(dv, fmt12Off, cp);
if (gid === 0) continue; /** 字体无此字 */
if (!seenGid.has(gid)) {
seenGid.add(gid);
subsetGids.push(gid);
}
cpToNewGid.set(cp, subsetGids.indexOf(gid));
}
return { subsetGids, cpToNewGid };
}
/** cmap format4 codepoint gid
* BMP format4 format12 otf BMP
* cpToNewGid */
function buildSubsetCmap(cpToNewGid: Map<number, number>): Uint8Array {
/** 收集 BMP 映射,按 codepoint 排序 */
const entries: { cp: number; gid: number }[] = [];
for (const [cp, gid] of cpToNewGid) {
if (cp < 0x10000 && cp > 0) entries.push({ cp, gid });
}
entries.sort((a, b) => a.cp - b.cp);
/** format4 segment codepoint gid delta
* delta = (gid - cp) delta cp */
interface Seg { start: number; end: number; delta: number; }
const segs: Seg[] = [];
for (const e of entries) {
const last = segs[segs.length - 1];
/** 与上一段连续cp 递增 1 且保持同一 delta则扩展 */
if (last && e.cp === last.end + 1 && (e.gid - e.cp) === ((last.delta << 16) >> 16)) {
last.end = e.cp;
} else {
segs.push({ start: e.cp, end: e.cp, delta: (e.gid - e.cp) & 0xffff });
}
}
/** 末尾哨兵段 0xFFFF→gid 0delta=1 使 0xFFFF+1=0 mod 0x10000 */
segs.push({ start: 0xffff, end: 0xffff, delta: 1 });
const segCount = segs.length;
const segCountX2 = segCount * 2;
const searchRange = (1 << (31 - Math.clz32(segCount))) * 2;
const entrySelector = 31 - Math.clz32(segCount);
const rangeShift = segCountX2 - searchRange;
/** format4 format(2) length(2) language(2) segCountX2(2) searchRange(2) entrySelector(2) rangeShift(2)
* + endCode[segCount] + reservedPad(2) + startCode[segCount] + idDelta[segCount] + idRangeOffset[segCount] */
const bodySize = 14 + segCount * 8 + 2;
/** cmap header: version(2) + numSubtables(2) + 子表记录(8) + format4 body */
const headerSize = 4 + 8;
const fmt4Length = bodySize;
const out = new Uint8Array(headerSize + fmt4Length);
const dv = new DataView(out.buffer);
/** cmap header */
dv.setUint16(0, 0, false); /** version */
dv.setUint16(2, 1, false); /** numSubtables */
/** 子表记录pid=3 eid=1 offset=12相对 cmap 起始) */
dv.setUint16(4, 3, false);
dv.setUint16(6, 1, false);
dv.setUint32(8, 12, false);
/** format4 body at offset 12 */
const b = 12;
dv.setUint16(b, 4, false); /** format */
dv.setUint16(b + 2, fmt4Length, false); /** length */
dv.setUint16(b + 4, 0, false); /** language */
dv.setUint16(b + 6, segCountX2, false);
dv.setUint16(b + 8, searchRange, false);
dv.setUint16(b + 10, entrySelector, false);
dv.setUint16(b + 12, rangeShift, false);
const endCodeBase = b + 14;
for (let i = 0; i < segCount; i++) dv.setUint16(endCodeBase + i * 2, segs[i].end, false);
dv.setUint16(endCodeBase + segCount * 2, 0, false); /** reservedPad */
const startCodeBase = endCodeBase + segCount * 2 + 2;
for (let i = 0; i < segCount; i++) dv.setUint16(startCodeBase + i * 2, segs[i].start, false);
const idDeltaBase = startCodeBase + segCount * 2;
for (let i = 0; i < segCount; i++) dv.setInt16(idDeltaBase + i * 2, (segs[i].delta << 16) >> 16, false);
const idRangeOffsetBase = idDeltaBase + segCount * 2;
for (let i = 0; i < segCount; i++) dv.setUint16(idRangeOffsetBase + i * 2, 0, false);
return out;
}
/** hmtx/vmtx gid metricsadvanceWidth + lsb/topSideBearing
* numberOfHMetrics */
function buildSubsetMetrics(
srcDv: DataView,
metricsOff: number,
numberOfHMetrics: number,
subsetGids: number[],
): Uint8Array {
/** 原始记录:前 numberOfHMetrics 个完整advance+lsb其余仅 lsbadvance 复用最后一个) */
const lastAdv = numberOfHMetrics > 0 ? srcDv.getUint16(metricsOff + (numberOfHMetrics - 1) * 4, false) : 0;
const out = new Uint8Array(subsetGids.length * 4);
const dv = new DataView(out.buffer);
for (let i = 0; i < subsetGids.length; i++) {
const gid = subsetGids[i];
let adv: number;
let sb: number;
if (gid < numberOfHMetrics) {
adv = srcDv.getUint16(metricsOff + gid * 4, false);
sb = srcDv.getInt16(metricsOff + gid * 4 + 2, false);
} else {
adv = lastAdv;
/** lsb 数组起始 = metricsOff + numberOfHMetrics*4每项 2 字节 */
const lsbArrOff = metricsOff + numberOfHMetrics * 4;
sb = srcDv.getInt16(lsbArrOff + (gid - numberOfHMetrics) * 2, false);
}
dv.setUint16(i * 4, adv, false);
dv.setInt16(i * 4 + 2, sb, false);
}
return out;
}
/** 重建 maxpOTF 版本 0.5version(4) + numGlyphs(2) */
function buildSubsetMaxp(subsetCount: number): Uint8Array {
const out = new Uint8Array(6);
const dv = new DataView(out.buffer);
dv.setUint32(0, 0x00005000, false); /** version 0.5 */
dv.setUint16(4, subsetCount, false);
return out;
}
/** 重建 postformat3无 glyph 名最省。version=3.0 */
function buildSubsetPost(): Uint8Array {
/** post format3 = version(4)=0x00030000 + italicAngle(4) + underlinePosition(2) + underlineThickness(2)
* + isFixedPitch(4) + minMemType42(4) + maxMemType42(4) + minMemType1(4) + maxMemType1(4) = 32 */
const out = new Uint8Array(32);
const dv = new DataView(out.buffer);
dv.setUint32(0, 0x00030000, false);
return out;
}
/** OS/2 patch usFirstCharIndex/usLastCharIndex codepoint min/max
* CID OS/2 65535/0OTS usFirst<=usLast
* cmap codepoint BMP codepoint uint16 */
function buildSubsetOS2(srcDv: DataView, off: number, len: number, codePoints: number[]): Uint8Array {
const out = new Uint8Array(srcDv.buffer, srcDv.byteOffset + off, len).slice();
const dv = new DataView(out.buffer);
let minCp = 0xffff;
let maxCp = 0;
for (const cp of codePoints) {
if (cp > 0 && cp < 0x10000) {
if (cp < minCp) minCp = cp;
if (cp > maxCp) maxCp = cp;
}
}
if (minCp > maxCp) { minCp = 0; maxCp = 0xffff; }
dv.setUint16(64, minCp, false);
dv.setUint16(66, maxCp, false);
return out;
}
/** name nameId1/2/4/6/16/17
* nameId name */
function buildSubsetName(srcDv: DataView, nameOff: number): Uint8Array {
const count = srcDv.getUint16(nameOff + 2, false);
const stringOff = nameOff + srcDv.getUint16(nameOff + 4, false);
/** 保留的 nameId 白名单(与 ttf 子集化 [[name-subset-whitelist]] 一致) */
const KEEP = new Set([1, 2, 4, 6, 16, 17]);
/** 收集保留记录(优先 p3e1回退任意 */
const records: { platformID: number; encodingID: number; languageID: number; nameID: number; bytes: Uint8Array }[] = [];
const seenNameId = new Set<number>();
/** 先扫 p3e1Windows UTF-16再补其他 platform */
for (let pass = 0; pass < 2; pass++) {
for (let i = 0; i < count; i++) {
const r = nameOff + 6 + i * 12;
const platformID = srcDv.getUint16(r, false);
const encodingID = srcDv.getUint16(r + 2, false);
const languageID = srcDv.getUint16(r + 4, false);
const nameID = srcDv.getUint16(r + 6, false);
if (!KEEP.has(nameID)) continue;
const isWin = platformID === 3 && encodingID === 1;
if (pass === 0 && !isWin) continue;
if (pass === 1 && seenNameId.has(nameID)) continue;
if (pass === 0 && seenNameId.has(nameID)) continue;
const length = srcDv.getUint16(r + 8, false);
const offset = srcDv.getUint16(r + 10, false);
records.push({ platformID, encodingID, languageID, nameID, bytes: new Uint8Array(srcDv.buffer, srcDv.byteOffset + stringOff + offset, length) });
seenNameId.add(nameID);
}
}
/** 重建 name 表 */
const headerSize = 6 + records.length * 12;
let stringSize = 0;
for (const rec of records) stringSize += rec.bytes.length;
const out = new Uint8Array(headerSize + stringSize);
const dv = new DataView(out.buffer);
dv.setUint16(0, 0, false); /** format */
dv.setUint16(2, records.length, false);
dv.setUint16(4, headerSize, false); /** stringOffset */
let strAcc = 0;
for (let i = 0; i < records.length; i++) {
const rec = records[i];
const r = 6 + i * 12;
dv.setUint16(r, rec.platformID, false);
dv.setUint16(r + 2, rec.encodingID, false);
dv.setUint16(r + 4, rec.languageID, false);
dv.setUint16(r + 6, rec.nameID, false);
dv.setUint16(r + 8, rec.bytes.length, false);
dv.setUint16(r + 10, strAcc, false);
out.set(rec.bytes, headerSize + strAcc);
strAcc += rec.bytes.length;
}
return out;
}
/** head patch numGlyphs head numGlyphs checkSumAdjustment
* head checkSumAdjustment sfnt */
function passthroughHead(srcDv: DataView, off: number, len: number): Uint8Array {
const out = new Uint8Array(srcDv.buffer, srcDv.byteOffset + off, len).slice();
const dv = new DataView(out.buffer);
dv.setUint32(8, 0, false); /** checkSumAdjustment = 0封装时回填 */
return out;
}
/** 计算表 4 字节对齐后的 checksumOpenType 规范:表长度按 4 字节边界补齐算 sum */
function calcTableChecksum(bytes: Uint8Array): number {
const n = bytes.length;
const padded = (n + 3) & ~3;
let sum = 0;
for (let i = 0; i < padded; i += 4) {
const b0 = bytes[i] || 0;
const b1 = bytes[i + 1] || 0;
const b2 = bytes[i + 2] || 0;
const b3 = bytes[i + 3] || 0;
sum = (sum + ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3)) >>> 0;
}
return sum >>> 0;
}
/**
* OTF sfntOTTO + 4 + head.checkSumAdjustment
* @param tables (tag bytes) tag 便
*/
function assembleSfnt(tables: { tag: string; bytes: Uint8Array }[]): Uint8Array {
/** 按 tag 升序sfnt 规范要求,浏览器按 tag 二分查找表) */
tables.sort((a, b) => (a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0));
const numTables = tables.length;
const entrySelector = numTables > 0 ? 31 - Math.clz32(numTables) : 0;
const searchRange = (1 << entrySelector) * 16;
const rangeShift = numTables * 16 - searchRange;
/** 目录区 = 12sfnt 头)+ numTables*16表记录 */
const dirSize = 12 + numTables * 16;
/** 各表 4 字节对齐后的总数据长度 */
const paddedLens = tables.map((t) => (t.bytes.length + 3) & ~3);
let dataTotal = 0;
for (const pl of paddedLens) dataTotal += pl;
const out = new Uint8Array(dirSize + dataTotal);
const dv = new DataView(out.buffer);
/** sfnt 头OTTO 签名 */
out[0] = 0x4f; out[1] = 0x54; out[2] = 0x54; out[3] = 0x4f;
dv.setUint16(4, numTables, false);
dv.setUint16(6, searchRange, false);
dv.setUint16(8, entrySelector, false);
dv.setUint16(10, rangeShift, false);
/** 表记录 + 数据 */
let dataOff = dirSize;
const headIdx = tables.findIndex((t) => t.tag === "head");
for (let i = 0; i < numTables; i++) {
const r = 12 + i * 16;
out[r] = tables[i].tag.charCodeAt(0);
out[r + 1] = tables[i].tag.charCodeAt(1);
out[r + 2] = tables[i].tag.charCodeAt(2);
out[r + 3] = tables[i].tag.charCodeAt(3);
const checksum = calcTableChecksum(tables[i].bytes);
dv.setUint32(r + 4, checksum, false);
dv.setUint32(r + 8, dataOff, false);
dv.setUint32(r + 12, tables[i].bytes.length, false);
out.set(tables[i].bytes, dataOff);
dataOff += paddedLens[i];
}
/** head.checkSumAdjustment = 0xB1B0AFBA - 全文件 sum */
if (headIdx >= 0) {
const headRecOff = 12 + headIdx * 16;
const headDataOff = dv.getUint32(headRecOff + 8, false);
/** 先算全文件 checksumhead.checkSumAdjustment 此时为 0 */
let wholeSum = 0;
const wholePadded = (out.length + 3) & ~3;
for (let i = 0; i < wholePadded; i += 4) {
const b0 = out[i] || 0;
const b1 = out[i + 1] || 0;
const b2 = out[i + 2] || 0;
const b3 = out[i + 3] || 0;
wholeSum = (wholeSum + ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3)) >>> 0;
}
const adjustment = (0xb1b0afba - wholeSum) >>> 0;
dv.setUint32(headDataOff + 8, adjustment, false);
}
return out;
}
/**
* OTF
* @param fontBuffer OTF
* @param codePoints
* @param keepGSUB GSUB/GPOS/
* @returns OTF CFF null
*/
export function subsetOTF(fontBuffer: Uint8Array, codePoints: number[], keepGSUB: boolean): Uint8Array | null {
const dv = new DataView(fontBuffer.buffer, fontBuffer.byteOffset, fontBuffer.byteLength);
const tables = readSfntTables(dv);
const cff = findTable(tables, "CFF ");
const cmap = findTable(tables, "cmap");
const hmtx = findTable(tables, "hmtx");
const hhea = findTable(tables, "hhea");
const head = findTable(tables, "head");
const maxp = findTable(tables, "maxp");
if (!cff || !cmap || !hmtx || !hhea || !head || !maxp) return null;
/** 子集字形集 + codepoint→新gid */
const { subsetGids, cpToNewGid } = buildSubsetGids(dv, cmap.offset, codePoints);
/** 子集 CFF */
const cffBytes = new Uint8Array(fontBuffer.buffer, fontBuffer.byteOffset + cff.offset, cff.length);
const newCFF = subsetCFF(cffBytes, subsetGids);
if (!newCFF) return null;
/** 子集 cmap */
const newCmap = buildSubsetCmap(cpToNewGid);
/** numberOfHMetricshhea +34*/
const numberOfHMetrics = dv.getUint16(hhea.offset + 34, false);
/** 子集 hmtx */
const newHmtx = buildSubsetMetrics(dv, hmtx.offset, numberOfHMetrics, subsetGids);
/** 组装各表 */
const outTables: { tag: string; bytes: Uint8Array }[] = [];
outTables.push({ tag: "CFF ", bytes: newCFF });
outTables.push({ tag: "cmap", bytes: newCmap });
outTables.push({ tag: "hmtx", bytes: newHmtx });
outTables.push({ tag: "maxp", bytes: buildSubsetMaxp(subsetGids.length) });
outTables.push({ tag: "post", bytes: buildSubsetPost() });
/** head 透传patch checkSumAdjustment 占位) */
outTables.push({ tag: "head", bytes: passthroughHead(dv, head.offset, head.length) });
/** hhea 透传 + patch numberOfHMetrics = subsetGids.length */
{
const hheaBytes = new Uint8Array(dv.buffer, dv.byteOffset + hhea.offset, hhea.length).slice();
const hheaDv = new DataView(hheaBytes.buffer);
hheaDv.setUint16(34, subsetGids.length, false);
outTables.push({ tag: "hhea", bytes: hheaBytes });
}
/** OS/2 透传 + patch usFirstCharIndex/usLastCharIndex */
const os2 = findTable(tables, "OS/2");
if (os2) outTables.push({ tag: "OS/2", bytes: buildSubsetOS2(dv, os2.offset, os2.length, codePoints) });
/** name 子集 */
const name = findTable(tables, "name");
if (name) outTables.push({ tag: "name", bytes: buildSubsetName(dv, name.offset) });
/** vhea/vmtx 透传 + 重建(垂直排版 metricsCJK 需要) */
const vhea = findTable(tables, "vhea");
const vmtx = findTable(tables, "vmtx");
if (vhea && vmtx) {
const numOfLongVerMetrics = dv.getUint16(vhea.offset + 34, false);
const newVmtx = buildSubsetMetrics(dv, vmtx.offset, numOfLongVerMetrics, subsetGids);
outTables.push({ tag: "vmtx", bytes: newVmtx });
const vheaBytes = new Uint8Array(dv.buffer, dv.byteOffset + vhea.offset, vhea.length).slice();
const vheaDv = new DataView(vheaBytes.buffer);
vheaDv.setUint16(34, subsetGids.length, false);
outTables.push({ tag: "vhea", bytes: vheaBytes });
}
/** GSUB/GPOS keepGSUB ttf gsub-subset/gpos-subset
* subsetGids gidgid glyphIdOTF TTF GSUB/GPOS
* OpenType glyf vs CFF
* gid coverage gid
* shaping .notdef/ otf GSUB SSIM 0.623/
* keepGSUB=false / */
if (keepGSUB) {
/** origToNewsubsetGids[新gid] = 原gid反转得 原gid→新gid */
const origToNew = new Map<number, number>();
for (let newGid = 0; newGid < subsetGids.length; newGid++) origToNew.set(subsetGids[newGid], newGid);
const gsub = findTable(tables, "GSUB");
if (gsub) {
const gsubBytes = new Uint8Array(dv.buffer, dv.byteOffset + gsub.offset, gsub.length);
outTables.push({ tag: "GSUB", bytes: subsetGSUB(gsubBytes, origToNew) });
}
const gpos = findTable(tables, "GPOS");
if (gpos) {
const gposBytes = new Uint8Array(dv.buffer, dv.byteOffset + gpos.offset, gpos.length);
/** subsetGPOS 可能对不支持的版本返回 null此时丢弃该表无 GPOS 仅丢失 kerning */
const newGpos = subsetGPOS(gposBytes, origToNew);
if (newGpos) outTables.push({ tag: "GPOS", bytes: newGpos });
}
}
return assembleSfnt(outTables);
}

View File

@ -0,0 +1,3 @@
/** WOFF2 sfnt ttf otf WOFF2
* glyf/loca transform CFF brotli */
export function encodeTTFToWOFF2(sfntBuffer: Uint8Array): Uint8Array;

View File

@ -79,6 +79,7 @@ function createFontServer(): Promise<{ server: Server; port: number }> {
if (!buf) buf = fontStore.get(fontKey + ".otf");
if (!buf) buf = fontStore.get(fontKey + ".woff2");
if (buf) {
console.log(`[fontsrv] GET ${fontKey} -> ${buf.length} bytes`);
const ext = fontKey.endsWith(".woff2") ? "font/woff2" : fontKey.endsWith(".otf") ? "font/opentype" : "font/ttf";
res.writeHead(200, { "Content-Type": ext, "Content-Length": buf.length, "Cache-Control": "public, max-age=31536000, immutable" });
res.end(buf);
@ -365,7 +366,9 @@ for (const tc of testCases) {
const t1 = performance.now();
times.push(t1 - t0);
lastSize = subsetBuf.byteLength;
if (i === 0) lastBuffer = subsetBuf;
if (i === 0) {
lastBuffer = subsetBuf;
}
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
@ -383,9 +386,11 @@ for (const tc of testCases) {
/**
* maxp maxPoints/maxContours 0
* 0 /fallback
* OTFTTF optimizettf _flatContours
* OTFTTF optimizettf _flatContours
* OTFCFF maxp version 0.5 numGlyphs maxPoints/maxContours
*
*/
if (tc.outType !== "woff2") {
if (tc.outType !== "woff2" && tc.sourceType !== "otf") {
const ttfView = new DataView(lastBuffer.buffer, lastBuffer.byteOffset, lastBuffer.byteLength);
const numTbl = ttfView.getUint16(4, false);
for (let ti = 0; ti < numTbl; ti++) {
@ -403,8 +408,14 @@ for (const tc of testCases) {
}
}
/** 注册子集字体 */
const subsetKey = `subset_${safeLabel}.${tc.outType}`;
/**
* otf subsetOTF CFF outType=ttf/woff2 .otf
* HTTP Content-Type font/ttf truetype CFF
* woff2 .woff2 magic */
const subsetExt = tc.sourceType === "otf"
? (tc.outType === "woff2" ? "woff2" : "otf")
: tc.outType;
const subsetKey = `subset_${safeLabel}.${subsetExt}`;
fontStore.set(subsetKey, Buffer.from(lastBuffer));
/** 完整字体 key */
@ -417,12 +428,14 @@ for (const tc of testCases) {
const fullResult = await renderTextViaBrowser(page, baseUrl, fullKey, tc.text, renderSize, tc.fullFormat);
/**
* format outType
* woff2 format("truetype") Chrome GPOS
* CJK GPOS lookup
* outType format GPOS layout
* format
* otf subsetOTF CFF outType=otf woff2 format("opentype")
* woff2 CFF truetype
* ttf glyf woff2 woff2 ttf truetype
*/
const subsetFormat = tc.outType === "woff2" ? "woff2" : "truetype";
const subsetFormat = tc.sourceType === "otf"
? (tc.outType === "woff2" ? "woff2" : "opentype")
: (tc.outType === "woff2" ? "woff2" : "truetype");
const subsetResult = await renderTextViaBrowser(page, baseUrl, subsetKey, tc.text, renderSize, subsetFormat);
await writeFile(`${BENCHMARK_DIR}/screenshots/${safeLabel}_full.png`, fullResult.screenshot);