mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 14:53:32 +08:00
与 Local Subr 同理:Name/String/Global Subr INDEX 仅需字节范围透传 + end (下一 INDEX 起始),readIndex 全量解析 count+1 个 offset 是浪费。统一改用 indexByteRange,删除仅服务于这三个的 getIndexBytes。 思源场景 offset 数组不大故实测持平(subsetOTF min 0.49ms 不变),但消除潜在 全量解析浪费,与 Local Subr 处理一致;白狐 5字 min 0.052ms。 字节级零回归(cmp 输出逐字节一致);基准测试全字体 SSIM 与优化前完全一致。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
778 lines
33 KiB
TypeScript
778 lines
33 KiB
TypeScript
/**
|
||
* CFF (Compact Font Format) 表子集化器 —— OTF 字体保留 CFF 轮廓的子集化。
|
||
*
|
||
* 背景:fonteditor-core 对 OTF 输入走 otf2ttfobject(CFF 三次贝塞尔 → glyf 二次贝塞尔),
|
||
* 子集化后再以 glyf 输出。但浏览器对 CFF(三次)与 glyf(二次)的光栅化路径不同,
|
||
* 子集后渲染与原始 OTF 存在像素差异(基准 SSIM 0.93~0.97,ink 差数百像素)。
|
||
*
|
||
* 本模块直接对原始 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 CFF(Top DICT 含 ROS / FDArray / FDSelect)。非 CID(Type 2 name-keyed)
|
||
* 字体走另一套 charstring 索引结构,当前生产用例不涉及,遇到时返回 null 降级。
|
||
*
|
||
* @reference https://learn.microsoft.com/en-us/typography/opentype/spec/cff
|
||
*/
|
||
|
||
/** CFF INDEX 解析结果。offsets 为 1-based,object 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-based:object i 的数据从 dataStart + offsets[i] - 1 开始 */
|
||
const dataStart = op;
|
||
return { start: pos, count, offSize, offsets, dataStart, end: dataStart + offsets[count] - 1 };
|
||
}
|
||
|
||
/** 只取 INDEX 的字节范围 [start, end),不解析中间 offset 数组。
|
||
* Local/Global Subr INDEX 透传时只需整体字节切片,全量解析 count+1 个 offset 是纯浪费
|
||
* (思源等大字体的 Local Subr 可达数千 subr,readIndex 全量解析占 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 + 3(count+offSize 头)+ (count+1)*offSize(offset 数组) */
|
||
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];
|
||
}
|
||
|
||
/**
|
||
* 序列化 CFF INDEX:count + 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;
|
||
|
||
/** 选 offSize:1~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) 是浪费。
|
||
* 格式 0:format(1) + numGlyphs×uint8,直接按 gid 取字节。
|
||
* 格式 3:format(1) + nRanges(u16) + ranges[first(u16),fd(u8)]×nRanges + sentinel(u16),
|
||
* 二分找最后一个 first<=gid 的 range(range 覆盖 [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 3:ranges 起始 = 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。
|
||
* 格式 0:format(1) + numGlyphs×uint8
|
||
* 格式 3:format(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)+ end(Top 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 INDEX;Global 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;
|
||
|
||
/** .notdef(gid 0)必须保留,且 subsetGids[0] 应为 0 */
|
||
const newSubsetGids = subsetGids[0] === 0 ? subsetGids : [0, ...subsetGids];
|
||
|
||
/** 重建 CharStrings INDEX:按 newSubsetGids 顺序透传原 charstring 字节。
|
||
* 按 gid 随机读 2 个 offset 取区间,跳过全量 offset 遍历。 */
|
||
const newCharStringObjects: { bytes: Uint8Array; start: number; len: number }[] = [];
|
||
for (const gid of newSubsetGids) {
|
||
/** 读 offset[gid] 与 offset[gid+1](offSize 字节大端) */
|
||
let o0 = 0;
|
||
let o1 = 0;
|
||
const p0 = csOffArrStart + gid * csOffSize;
|
||
const p1 = csOffArrStart + (gid + 1) * csOffSize;
|
||
for (let j = 0; j < csOffSize; j++) o0 = (o0 << 8) | b[p0 + j];
|
||
for (let j = 0; j < csOffSize; j++) o1 = (o1 << 8) | b[p1 + j];
|
||
const s = csDataStart + o0 - 1;
|
||
const e = csDataStart + o1 - 1;
|
||
newCharStringObjects.push({ bytes: b, start: s, len: e - s });
|
||
}
|
||
const newCharStrings = writeIndex(newCharStringObjects);
|
||
|
||
/** 重建 charset:CID-keyed 字体的 charset 是 gid→CID 映射。格式 0/1/2,按 newSubsetGids 取 CID。
|
||
* CID 0 固定留给 .notdef(gid 0),其余按原 charset 顺序。新 charset 用格式 0 最简单:
|
||
* format(1) + (numGlyphs-1)×CID(u16)(charset 不含 gid 0,它隐式为 CID 0)。
|
||
* 按需查 CID(lookupCharsetCID 遍历 range 查单个 gid),不全量展开 65535 项。 */
|
||
const newSubsetNumGlyphs = newSubsetGids.length;
|
||
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 的原 FD(lookupFDSelect 二分 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 的 DICT,patch 其 Private [len, offset] 指向新 Private 段。
|
||
* Private 数据本身透传(仅重定位 offset)。若 Private 声明了 Local Subr INDEX(op 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 19(Local 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) */
|
||
/** Local Subr INDEX 仅需整体字节切片透传,不全量解析 offset(思源 Local Subr 可达数千 subr) */
|
||
const subrRange = indexByteRange(b, privOrigOff + subrRel);
|
||
localSubr = b.subarray(subrRange.start, subrRange.end);
|
||
}
|
||
info = { origOff: privOrigOff, len: privLen, localSubr };
|
||
privSegCache.set(privOrigOff, info);
|
||
}
|
||
fdInfos.push({ dictBytes, priv: info });
|
||
}
|
||
|
||
/** 新 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);
|
||
|
||
/** 组装新 CFF:Header + 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 DICT:op19 指向新 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 + topDictLen,count=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 INDEX:count=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 定位操作码 18(Private),将其前的两个操作数替换为新编码 [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 {
|
||
/** 按操作码分段,找到 Private(18)替换其两个操作数 */
|
||
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 Subr(hasSubr=false)的 Private 原样返回。
|
||
* @param b 原 CFF 字节
|
||
* @param privOrigOff Private DICT 在原 CFF 的绝对偏移
|
||
* @param privLen Private DICT 字节长度
|
||
* @param hasSubr 是否含 Local Subr INDEX(op 19)
|
||
*/
|
||
function patchPrivateDict(b: Uint8Array, privOrigOff: number, privLen: number, hasSubr: boolean): Uint8Array {
|
||
/** 无 Local Subr:DICT 原样透传(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 不入表)。
|
||
* 格式 0:format(1) + (numGlyphs-1)×CID(u16),按 gid 直接取(charset 表不含 gid 0)。
|
||
* 格式 1/2:遍历 range 找覆盖 gid 的(range first 是 gid,CID = first 的 CID + (gid - range.first))。
|
||
* range 数量远小于 numGlyphs(思源 format2 约 6 千 range vs 65535 gid),遍历省去 65535 项填充。
|
||
* @param b CFF 字节
|
||
* @param charsetOff charset 起始偏移
|
||
* @param gid 原始 gid(>0,gid 0 调用方自行返回 0)
|
||
*/
|
||
function lookupCharsetCID(b: Uint8Array, charsetOff: number, gid: number): number {
|
||
if (gid === 0) return 0;
|
||
const fmt = b[charsetOff];
|
||
/** 格式 0:format(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/2:range[ 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 格式 0:format(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 === 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 单字节,无后续 */
|
||
}
|
||
/** 拼接 */
|
||
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;
|
||
}
|