web-font/backend/font_util/cff-subset.ts
崮生(子虚) efe24d9312 perf(cff): rewriteCharstring/replaceDictOffsets 用 Uint8Array+写指针替代 number[]
rewriteCharstring(18.7%) 与 replaceDictOffsets(7.8%) 原 out/chunks: number[] 逐字节 push
有装箱开销 + new Uint8Array(out) 二次遍历 / Array.from+spread 多次拷贝。改预分配 Uint8Array
+ 写指针 wp(CALLSUBR 截断即 wp=stackStart[]),encodeDictInt 内联到写入处消除临时 number[]。

otf-思源8字 subsetOTF min 0.352→0.322ms(-9%)、p50 0.483→0.421ms(-13%);
otf-白狐5字 min 0.058→0.055ms。输出逐字节完全相同(19188B/26536B A/B 一致)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 02:52:13 +08:00

1186 lines
51 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 轻量头部:只解析 count/offSize/dataStart不解析 (count+1) 个 offset 数组。
* Local Subr INDEX 引用收集只需按 subr 编号读个别 offset思源 FD12 26550 subr 仅引用十几个),
* 全量 readIndex 解析 26551 个 offset 占 subsetCFF 一半耗时,按需读消除之。 */
interface IndexHeader {
/** INDEX 起始偏移 */
start: number;
/** INDEX 内对象数量 */
count: number;
/** 偏移量字节宽度1~4 */
offSize: number;
/** 偏移量数组起始(紧接 count/offSize 之后) */
offBase: number;
/** 数据区起始(紧接偏移量数组之后 = offBase + (count+1)*offSize */
dataStart: number;
/** INDEX 结束位置(= 最后一个 object 的尾) */
end: number;
}
/** 只读 INDEX 头部count + offSize + dataStart + end不解析 offset 数组。
* end 需读第 count 个 offset位于 offBase + count*offSize。 */
function readIndexHeader(b: Uint8Array, pos: number): IndexHeader {
const count = (b[pos] << 8) | b[pos + 1];
/** count=0 的 INDEX 仅 2 字节 */
if (count === 0) return { start: pos, count: 0, offSize: 0, offBase: pos + 2, dataStart: pos + 2, end: pos + 2 };
const offSize = b[pos + 2];
const offBase = pos + 3;
const dataStart = offBase + (count + 1) * offSize;
/** 读第 count 个 offset哨兵= 总数据长 + 1算 end */
let lastOff = 0;
let op = offBase + count * offSize;
for (let j = 0; j < offSize; j++) lastOff = (lastOff << 8) | b[op++];
return { start: pos, count, offSize, offBase, dataStart, end: dataStart + lastOff - 1 };
}
/** 按 object 编号 i 读取 INDEX 的第 i 个 offset1-based大端 offSize 字节)。
* object i 的数据区间 = [dataStart + offset(i) - 1, dataStart + offset(i+1) - 1)。 */
function readIndexOffset(b: Uint8Array, h: IndexHeader, i: number): number {
let v = 0;
let op = h.offBase + i * h.offSize;
for (let j = 0; j < h.offSize; j++) v = (v << 8) | b[op++];
return v;
}
/** 只取 INDEX 的字节范围 [start, end),不解析中间 offset 数组。
* Local/Global Subr INDEX 透传时只需整体字节切片,全量解析 count+1 个 offset 是纯浪费
* (思源等大字体的 Local Subr 可达数千 subrreadIndex 全量解析占 subsetCFF 主要耗时)。
* end = dataStart + offsets[count] - 1仅读第 count 个 offset位于 pos+3+count*offSize即可。 */
function indexByteRange(b: Uint8Array, pos: number): { start: number; end: number } {
const count = (b[pos] << 8) | b[pos + 1];
/** count=0 的 INDEX 仅 2 字节 */
if (count === 0) return { start: pos, end: pos + 2 };
const offSize = b[pos + 2];
/** dataStart = pos + 3count+offSize 头)+ (count+1)*offSizeoffset 数组) */
const dataStart = pos + 3 + (count + 1) * offSize;
/** 第 count 个 offset 位于 offset 数组末尾pos+3 + count*offSize读 offSize 字节大端 */
let lastOff = 0;
const lp = pos + 3 + count * offSize;
for (let j = 0; j < offSize; j++) lastOff = (lastOff << 8) | b[lp + j];
return { start: pos, end: dataStart + lastOff - 1 };
}
/** 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];
}
/** Type 2 charstring 操作码Adobe Type 2 Charstring Format。 */
const T2_CALLSUBR = 10;
const T2_RETURN = 11;
const T2_ENDCHAR = 14;
const T2_HSTEM = 1;
const T2_VSTEM = 3;
const T2_HSTEMHM = 18;
const T2_VSTEMHM = 23;
const T2_HINTMASK = 19;
const T2_CNTRMASK = 20;
const T2_CALLGSUBR = 29;
/** 计算 subr INDEX 的 biasCFF 规范:调用编号 = 实际 subr 索引 + bias
* nSubrs < 1240 → 107< 33900 → 1131否则 32768。 */
function subrBias(nSubrs: number): number {
if (nSubrs < 1240) return 107;
if (nSubrs < 33900) return 1131;
return 32768;
}
/**
* 扫描单个 Type 2 charstring/subr 字节,收集其引用的 local subr 与 global subr 编号。
* 采用栈模拟operand 依次入栈,遇 callsubr(10)/callgsubr(29) 取栈顶为调用编号(减 bias 得实际索引)。
*
* hintmask(19)/cntrmask(20) 后跟 ceil(stemCount/8) 字节掩码,必须跳过——否则掩码字节会被
* 误判为操作码致扫描错位、漏收引用(思源 charstring 大量使用 hint
* stemCount 由 stem 类操作码hstem/vstem/hstemhm/vstemhm的栈深度累加每 stem = 2 operand
*
* @param b CFF 字节
* @param start charstring/subr 起始偏移
* @param end charstring/subr 结束偏移(不含)
* @param localBias 该 charstring 所属 FD 的 local subr bias
* @param localCount local subr 总数(越界保护)
* @param localRefs 输出:收集到的 local subr 实际索引
* @param gsubrRefs 输出:收集到的 global subr 实际索引
*/
export function collectSubrRefs(
b: Uint8Array,
start: number,
end: number,
localBias: number,
localCount: number,
localRefs: Set<number>,
gsubrRefs: Set<number>,
): void {
let p = start;
const stack: number[] = [];
let stemCount = 0;
while (p < end) {
const b0 = b[p++];
if (b0 === 255) {
/** fixed point坐标4 字节,不入栈编号判定 */
stack.push(NaN);
p += 4;
} else if (b0 === 28) {
stack.push(((b[p] << 24) | (b[p + 1] << 16)) >> 16);
p += 2;
} else if (b0 === 29) {
stack.push(((b[p] << 24) | (b[p + 1] << 16) | (b[p + 2] << 8) | b[p + 3]) | 0);
p += 4;
} else if (b0 >= 32 && b0 <= 246) {
stack.push(b0 - 139);
} else if (b0 >= 247 && b0 <= 250) {
stack.push((b0 - 247) * 256 + b[p] + 108);
p += 1;
} else if (b0 >= 251 && b0 <= 254) {
stack.push(-(b0 - 251) * 256 - b[p] - 108);
p += 1;
} else {
/** 操作码b0 <= 27含 12 双字节) */
if (b0 === 12) {
p += 1;
stack.length = 0;
} else if (b0 === T2_HSTEM || b0 === T2_VSTEM || b0 === T2_HSTEMHM || b0 === T2_VSTEMHM) {
stemCount += stack.length >> 1;
stack.length = 0;
} else if (b0 === T2_HINTMASK || b0 === T2_CNTRMASK) {
p += (stemCount + 7) >>> 3;
stack.length = 0;
} else if (b0 === T2_CALLSUBR) {
const arg = stack[stack.length - 1];
if (Number.isInteger(arg)) {
const sn = arg + localBias;
if (sn >= 0 && sn < localCount) localRefs.add(sn);
}
stack.length = 0;
} else if (b0 === T2_CALLGSUBR) {
const arg = stack[stack.length - 1];
if (Number.isInteger(arg)) gsubrRefs.add(arg);
stack.length = 0;
} else if (b0 === T2_ENDCHAR) {
break;
} else {
/** 其余操作码(运动/曲线等)消费栈 */
stack.length = 0;
}
}
}
}
/**
* 重写 Type 2 charstring/subr把 callsubr/callgsubr 的调用编号 operand 重映射到子集编号。
* 其余字节原样保留坐标、操作码、hintmask 掩码等)。
*
* 实现逐字节复制到输出operand 入栈时记录其在输出的起始位置;遇 callsubr/callgsubr 时
* 截断输出回栈顶 operand 起始处写入新编号编码operand 编码长度可能变化,故必须重建流而非原地改)。
*
* @param b CFF 字节
* @param start charstring 起始偏移
* @param end charstring 结束偏移
* @param localBias 原 local subr bias解析 operand 用)
* @param localRemap 旧 local subr 索引 → 新索引(-1 表示未保留,不该出现于引用 charstring
* @param newLocalCount 新 local subr 总数(算新 bias
* @returns 重写后的字节。global subr 不子集化callgsubr operand 原样保留bias 不变)。
*/
export function rewriteCharstring(
b: Uint8Array,
start: number,
end: number,
localBias: number,
localRemap: Map<number, number>,
newLocalCount: number,
): Uint8Array {
const newLocalBias = subrBias(newLocalCount);
/**
* 优化:预分配 Uint8Array + 写指针 wp 替代 number[] + push。
* 原 out: number[] 逐字节 push 有装箱开销,且最终 new Uint8Array(out) 要二次遍历转换。
* Uint8Array 直接写字节wp 模拟 lengthCALLSUBR 截断即 wp = stackStart[...])。
* 容量上界:(end-start) 是原始字节长度CALLSUBR 重写时新 operand 编码1~5 字节)
* 可能比原 operand1~5 字节)长,最坏每个 operand 多 4 字节。operand 数 ≤ end-start
* 故 (end-start)*2 + 16 是安全上界(远超实际,仅预分配不写入多余字节)。
*/
const cap = ((end - start) << 1) + 16;
const out = new Uint8Array(cap);
let wp = 0;
/** 栈:记录每个 operand 在输出中的起始 wp便于截断重写。值为原始解析值。 */
const stackStart: number[] = [];
const stackVal: number[] = [];
let stemCount = 0;
let p = start;
while (p < end) {
const b0 = b[p++];
if (b0 === 255) {
stackStart.push(wp);
stackVal.push(NaN);
out[wp] = 255; out[wp + 1] = b[p]; out[wp + 2] = b[p + 1]; out[wp + 3] = b[p + 2]; out[wp + 4] = b[p + 3];
wp += 5;
p += 4;
} else if (b0 === 28) {
stackStart.push(wp);
stackVal.push(((b[p] << 24) | (b[p + 1] << 16)) >> 16);
out[wp] = 28; out[wp + 1] = b[p]; out[wp + 2] = b[p + 1];
wp += 3;
p += 2;
} else if (b0 === 29) {
stackStart.push(wp);
stackVal.push(((b[p] << 24) | (b[p + 1] << 16) | (b[p + 2] << 8) | b[p + 3]) | 0);
out[wp] = 29; out[wp + 1] = b[p]; out[wp + 2] = b[p + 1]; out[wp + 3] = b[p + 2]; out[wp + 4] = b[p + 3];
wp += 5;
p += 4;
} else if (b0 >= 32 && b0 <= 246) {
stackStart.push(wp);
stackVal.push(b0 - 139);
out[wp++] = b0;
} else if (b0 >= 247 && b0 <= 250) {
stackStart.push(wp);
stackVal.push((b0 - 247) * 256 + b[p] + 108);
out[wp] = b0; out[wp + 1] = b[p];
wp += 2;
p += 1;
} else if (b0 >= 251 && b0 <= 254) {
stackStart.push(wp);
stackVal.push(-(b0 - 251) * 256 - b[p] - 108);
out[wp] = b0; out[wp + 1] = b[p];
wp += 2;
p += 1;
} else {
/** 操作码 */
if (b0 === 12) {
out[wp] = 12; out[wp + 1] = b[p];
wp += 2;
p += 1;
stackStart.length = 0;
stackVal.length = 0;
} else if (b0 === T2_HSTEM || b0 === T2_VSTEM || b0 === T2_HSTEMHM || b0 === T2_VSTEMHM) {
stemCount += stackVal.length >> 1;
out[wp++] = b0;
stackStart.length = 0;
stackVal.length = 0;
} else if (b0 === T2_HINTMASK || b0 === T2_CNTRMASK) {
out[wp++] = b0;
const maskBytes = (stemCount + 7) >>> 3;
out.set(b.subarray(p, p + maskBytes), wp);
wp += maskBytes;
p += maskBytes;
stackStart.length = 0;
stackVal.length = 0;
} else if (b0 === T2_CALLSUBR) {
const arg = stackVal[stackVal.length - 1];
const oldSn = Number.isInteger(arg) ? arg + localBias : -1;
const newSn = localRemap.get(oldSn);
if (newSn === undefined) {
/** subr 未保留(理论上引用 charstring 必命中)——保留原 operand 保底 */
out[wp++] = T2_CALLSUBR;
} else {
/** 截断到栈顶 operand 起始,写入新编号编码(直接写 Uint8Array不分配临时数组 */
wp = stackStart[stackStart.length - 1];
const delta = newSn - newLocalBias;
if (delta >= -107 && delta <= 107) {
out[wp++] = delta + 139;
} else if (delta >= 108 && delta <= 1131) {
const v0 = delta - 108;
out[wp] = 247 + (v0 >> 8); out[wp + 1] = v0 & 0xff;
wp += 2;
} else if (delta >= -1131 && delta <= -108) {
const v0 = -delta - 108;
out[wp] = 251 + (v0 >> 8); out[wp + 1] = v0 & 0xff;
wp += 2;
} else if (delta >= -32768 && delta <= 32767) {
out[wp] = 28; out[wp + 1] = (delta >> 8) & 0xff; out[wp + 2] = delta & 0xff;
wp += 3;
} else {
out[wp] = 29; out[wp + 1] = (delta >>> 24) & 0xff; out[wp + 2] = (delta >> 16) & 0xff; out[wp + 3] = (delta >> 8) & 0xff; out[wp + 4] = delta & 0xff;
wp += 5;
}
out[wp++] = T2_CALLSUBR;
}
stackStart.length = 0;
stackVal.length = 0;
} else if (b0 === T2_CALLGSUBR) {
/** global subr 不子集化operand调用编号原样保留bias 不变 */
out[wp++] = T2_CALLGSUBR;
stackStart.length = 0;
stackVal.length = 0;
} else if (b0 === T2_ENDCHAR) {
out[wp++] = b0;
break;
} else {
out[wp++] = b0;
stackStart.length = 0;
stackVal.length = 0;
}
}
}
return out.subarray(0, wp);
}
/**
* 序列化 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;
}
/**
* 按需查询单个原始 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 gid 原始 gid
*/
function lookupFDSelect(b: Uint8Array, fdSelectOff: number, gid: number): number {
const fmt = b[fdSelectOff];
if (fmt === 0) return b[fdSelectOff + 1 + gid];
/** format 3ranges 起始 = fdSelectOff + 3每 range 3 字节 */
const nRanges = (b[fdSelectOff + 1] << 8) | b[fdSelectOff + 2];
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;
}
return b[rangesStart + lo * 3 + 2];
}
/**
* 编码 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 */
/** Name INDEX 仅需字节范围(透传 headerName+ endTop DICT INDEX 起始),不全量解析 offset */
const nameRange = indexByteRange(b, hdrSize);
/** Top DICT INDEX 紧接 Name INDEX */
const topDictIndex = readIndex(b, nameRange.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 紧接其后。
* 两者仅需字节范围透传 + end下一 INDEX 起始),不全量解析 offset。 */
const stringRange = indexByteRange(b, topDictIndex.end);
const globalSubrRange = indexByteRange(b, stringRange.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;
}
/** 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 的字节区间 [start, end),供后续 subr 子集化重写。 */
const newSubsetNumGlyphs = newSubsetGids.length;
const charStringRanges: { start: number; end: number }[] = new Array(newSubsetNumGlyphs);
for (let gi = 0; gi < newSubsetNumGlyphs; gi++) {
const gid = newSubsetGids[gi];
/** 读 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];
charStringRanges[gi] = { start: csDataStart + o0 - 1, end: csDataStart + o1 - 1 };
}
/** 重建 charsetCID-keyed 字体的 charset 是 gid→CID 映射。格式 0/1/2按 newSubsetGids 取 CID。
* CID 0 固定留给 .notdefgid 0其余按原 charset 顺序。新 charset 用格式 0 最简单:
* format(1) + (numGlyphs-1)×CID(u16)charset 不含 gid 0它隐式为 CID 0
* 按需查 CIDlookupCharsetCID 遍历 range 查单个 gid不全量展开 65535 项。 */
const newCharsetBody: number[] = [];
for (let i = 1; i < newSubsetNumGlyphs; i++) {
/** newSubsetGids[i] 是原始 gid取其原 CID */
newCharsetBody.push(lookupCharsetCID(b, charsetOff, newSubsetGids[i]));
}
const newCharset = encodeCharsetFormat0(newCharsetBody);
/** FDSelect按需查每个 subsetGid 的原 FDlookupFDSelect 二分 range不全量展开 numGlyphs。
* 单次遍历同时构建原FD→新FD 映射fdRemap/usedFds+ 每 gid 的原 FD 数组gidOrigFds
* 后者供 newGidToFd 直接复用,避免对 newSubsetGids 第二次 lookupFDSelect 遍历。 */
const fdRemap = new Map<number, number>();
const usedFds: number[] = [];
const gidOrigFds: number[] = new Array(newSubsetNumGlyphs);
for (let i = 0; i < newSubsetNumGlyphs; i++) {
const gid = newSubsetGids[i];
const fd = lookupFDSelect(b, fdSelectOff, gid);
gidOrigFds[i] = fd;
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;
/** Local Subr INDEX 轻量头部(按需读 offset供引用收集与重建null 表示无 local subr */
localSubrIdx: IndexHeader | null;
/** 原 local subr biaslocalSubrIdx.count 决定) */
localBias: number;
/** 子集 local subr 旧索引→新索引映射引用收集后填充null 表示无需重映射(无 subr 或全保留) */
localRemap: Map<number, number> | null;
/** 子集后 local subr 总数 */
newLocalCount: number;
/** 子集 local subr INDEX 字节(重建后;无 subr 为 null。最终拼入 Private 段 */
newLocalSubr: Uint8Array | null;
}
interface FdInfo { dictBytes: Uint8Array; priv: PrivInfo; }
/** 原始 Private 段去重:相同 origOff 的 Private 共享同一份(含其 Local Subr */
const privSegCache = new Map<number, PrivInfo>();
/** 每个 unique Private 池(按 privOrigOff收集的 local subr 引用集(多 FD 共享一池时合并) */
const privLocalRefs = new Map<number, Set<number>>();
/** global subr 不子集化collectSubrRefs 的 gsubrRefs 参数占位(引用不收集) */
const dummyGsubrRefs = new Set<number>();
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, localSubrIdx: null, localBias: 0, localRemap: null, newLocalCount: 0, newLocalSubr: 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;
let localSubrIdx: IndexHeader | null = null;
let localBias = 0;
if (subrRel !== undefined) {
/** Local Subr INDEX 紧接 Private DICT 字节之后(绝对偏移 = privOrigOff + subrRel */
const subrAbs = privOrigOff + subrRel;
/** 轻量头部:只读 count/offSize/dataStart不解析 (count+1) 个 offset。
* 引用收集/重建只按 subr 编号读个别 offset思源 26550 subr 仅引用十几个),
* 全量 readIndex 占 subsetCFF 一半耗时,按需读消除之。 */
localSubrIdx = readIndexHeader(b, subrAbs);
localBias = subrBias(localSubrIdx.count);
localSubr = b.subarray(localSubrIdx.start, localSubrIdx.end);
}
info = { origOff: privOrigOff, len: privLen, localSubr, localSubrIdx, localBias, localRemap: null, newLocalCount: localSubrIdx ? localSubrIdx.count : 0, newLocalSubr: localSubr };
privSegCache.set(privOrigOff, info);
privLocalRefs.set(privOrigOff, new Set());
}
fdInfos.push({ dictBytes, priv: info });
/** 收集该 FD 所有子集字形的 local subr 引用到其 Private 池的引用集 */
const refs = privLocalRefs.get(info.origOff)!;
if (info.localSubrIdx) {
const idx = info.localSubrIdx;
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);
}
}
}
/** 递归收敛:被引用的 local subr 可能再引用其他 local subr迭代至不动点。
* global subr 不子集化,故不收集 gsubr 引用collectSubrRefs 的 gsubrRefs 占位用 dummyGsubrRefs */
if (privLocalRefs.size > 0) {
let changed = true;
let guard = 0;
while (changed && guard < 64) {
changed = false;
guard++;
for (const [privOrigOff, refs] of privLocalRefs) {
const info = privSegCache.get(privOrigOff)!;
const idx = info.localSubrIdx;
if (!idx) continue;
const before = refs.size;
for (const sn of [...refs]) {
/** 按需读 offset避免全量解析 INDEX */
const ss = idx.dataStart + readIndexOffset(b, idx, sn) - 1;
const se = idx.dataStart + readIndexOffset(b, idx, sn + 1) - 1;
collectSubrRefs(b, ss, se, info.localBias, idx.count, refs, dummyGsubrRefs);
}
if (refs.size > before) changed = true;
}
}
}
/** 各 Private 池:构建旧→新 local subr 映射 + 重建子集 INDEX。引用为空则保留空 INDEX。 */
for (const [privOrigOff, refs] of privLocalRefs) {
const info = privSegCache.get(privOrigOff)!;
const idx = info.localSubrIdx;
if (!idx) continue;
const sortedRefs = [...refs].sort((a, c) => a - c);
const remap = new Map<number, number>();
for (let i = 0; i < sortedRefs.length; i++) remap.set(sortedRefs[i], i);
info.localRemap = remap;
info.newLocalCount = sortedRefs.length;
if (sortedRefs.length === 0) {
/** 无引用:输出空 INDEXcount=02 字节newLocalSubr 占位(下方 patchPrivateDict 后拼入) */
info.newLocalSubr = new Uint8Array(2);
} else {
/** 重建 INDEX按新顺序写出被引用的 subr 字节。subr 内部 callsubr 也需重映射(递归 patch */
const objects: { bytes: Uint8Array; start: number; len: number }[] = [];
for (const oldSn of sortedRefs) {
/** 按需读 offset避免全量解析 INDEX */
const ss = idx.dataStart + readIndexOffset(b, idx, oldSn) - 1;
const se = idx.dataStart + readIndexOffset(b, idx, oldSn + 1) - 1;
const rewritten = rewriteCharstring(b, ss, se, info.localBias, remap, sortedRefs.length);
objects.push({ bytes: rewritten, start: 0, len: rewritten.length });
}
info.newLocalSubr = writeIndex(objects);
}
}
/** 重建 CharStrings INDEX按 newSubsetGids 顺序输出 charstring。
* 所属 FD 有 local subr 子集化时patch callsubr operand重映射到新 subr 编号 - 新 bias
* 否则透传原 charstring 字节。global subr 不子集化callgsubr operand 原样保留。 */
const newCharStringObjects: { bytes: Uint8Array; start: number; len: number }[] = [];
for (let gi = 0; gi < newSubsetNumGlyphs; gi++) {
const r = charStringRanges[gi];
const origFd = gidOrigFds[gi];
/** 该 gid 所属原 FD 对应的 Private 信息(经 privSegCache 去重,按 privOrigOff 查) */
let privInfo: PrivInfo | null = null;
/** usedFds 顺序与 fdInfos 一致,反查 origFd→privInfo */
for (let fi = 0; fi < usedFds.length; fi++) {
if (usedFds[fi] === origFd) { privInfo = fdInfos[fi].priv; break; }
}
if (privInfo && privInfo.localRemap) {
const rewritten = rewriteCharstring(b, r.start, r.end, privInfo.localBias, privInfo.localRemap, privInfo.newLocalCount);
newCharStringObjects.push({ bytes: rewritten, start: 0, len: rewritten.length });
} else {
newCharStringObjects.push({ bytes: b, start: r.start, len: r.end - r.start });
}
}
const newCharStrings = writeIndex(newCharStringObjects);
/** 新 FDSelect每个新 gid → 新 FD 编号(复用首次遍历的 gidOrigFds无需第二次 lookupFDSelect
* 单 FD 用格式 0最省1+numGlyphs 字节);多 FD 用格式 3 ranges与原始 CID 字体一致,兼容 OTS 严格校验)。 */
const newGidToFd: number[] = new Array(newSubsetNumGlyphs);
for (let i = 0; i < newSubsetNumGlyphs; i++) {
newGidToFd[i] = fdRemap.get(gidOrigFds[i]) ?? 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), b.subarray(nameRange.start, nameRange.end)]);
const stringSeg = b.subarray(stringRange.start, stringRange.end);
const globalSubrSeg = b.subarray(globalSubrRange.start, globalSubrRange.end);
/** 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.newLocalSubr !== null);
curPatchedPriv.push({ dict: patchedPriv, subr: pi.newLocalSubr });
}
/** 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;
}
/**
* 按需查询单个原始 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 gid 原始 gid>0gid 0 调用方自行返回 0
*/
function lookupCharsetCID(b: Uint8Array, charsetOff: number, gid: number): number {
if (gid === 0) return 0;
const fmt = b[charsetOff];
/** 格式 0format(1) + (numGlyphs-1)×CID(u16) 紧排gid i(>0) 的 CID 在 (i-1)*2 */
if (fmt === 0) {
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;
}
}
/** 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) */
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 {
/**
* 优化:预分配 Uint8Array + 写指针 wp 替代 chunks: number[][] + Array.from + 二次拼接。
* 原 chunks 方案每个操作码段分配一个 number[](含 Array.from 拷贝 + spread 再拷贝),
* 最后还要两轮遍历拼接。Uint8Array 直接顺序写入,零中间数组。
* 容量上界:原 DICT 长度 + 每个替换 operand 最大 +4 字节(短编码→长编码)。
*/
const cap = dictBytes.length + replacements.size * 4 + 16;
const out = new Uint8Array(cap);
let wp = 0;
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)) {
/** 替换:写入新编码操作数 + 操作码(内联 encodeDictInt 避免 number[] 分配) */
const newVal = replacements.get(op)!;
if (newVal >= -107 && newVal <= 107) {
out[wp++] = newVal + 139;
} else if (newVal >= 108 && newVal <= 1131) {
const v0 = newVal - 108;
out[wp] = 247 + (v0 >> 8); out[wp + 1] = v0 & 0xff;
wp += 2;
} else if (newVal >= -1131 && newVal <= -108) {
const v0 = -newVal - 108;
out[wp] = 251 + (v0 >> 8); out[wp + 1] = v0 & 0xff;
wp += 2;
} else if (newVal >= -32768 && newVal <= 32767) {
out[wp] = 28; out[wp + 1] = (newVal >> 8) & 0xff; out[wp + 2] = newVal & 0xff;
wp += 3;
} else {
out[wp] = 29; out[wp + 1] = (newVal >>> 24) & 0xff; out[wp + 2] = (newVal >> 16) & 0xff; out[wp + 3] = (newVal >> 8) & 0xff; out[wp + 4] = newVal & 0xff;
wp += 5;
}
if (op >= 256) {
out[wp] = 12; out[wp + 1] = op & 0xff;
wp += 2;
} else {
out[wp++] = op;
}
} else {
/** 保留原操作数 + 操作码operandStart..p 原样拷贝) */
out.set(dictBytes.subarray(operandStart, p), wp);
wp += p - operandStart;
}
operandStart = p;
} else if (b0 === 28) {
p += 2;
} else if (b0 === 29) {
p += 4;
} else if (b0 === 30) {
/** BCD 实数:每字节两 nibble遇 0xf 结束。必须完整跳过,否则 BCD 内 <=21 的字节
* 会被误判为 operator 致 operator 边界错乱Top DICT 的 CIDFontVersion 等用 BCD。 */
while (p < len) {
const byte = dictBytes[p++];
if ((byte >> 4) === 0xf || (byte & 0xf) === 0xf) break;
}
} else if (b0 >= 247 && b0 <= 254) {
p += 1;
}
/** 32~246 单字节,无后续 */
}
return out.subarray(0, wp);
}