性能优化

This commit is contained in:
崮生(子虚) 2026-04-10 13:51:03 +08:00
parent 4990a0f61d
commit af0ab38cec
24 changed files with 1958 additions and 787 deletions

View File

@ -22,6 +22,7 @@
"@types/node": "^25.5.2", "@types/node": "^25.5.2",
"@xmldom/xmldom": "^0.9.9", "@xmldom/xmldom": "^0.9.9",
"jsdom": "^29.0.2", "jsdom": "^29.0.2",
"puppeteer": "^24.40.0",
"skia-canvas": "^3.0.8", "skia-canvas": "^3.0.8",
"tsup": "^8.5.1", "tsup": "^8.5.1",
"typescript": "^6.0.2", "typescript": "^6.0.2",

770
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -4,31 +4,39 @@ Object.defineProperty(exports, "__esModule", {
value: true value: true
}); });
exports.default = bezierCubic2Q2; exports.default = bezierCubic2Q2;
exports.bezierCubic2Q2Raw = bezierCubic2Q2Raw;
/** /**
* @file 三次贝塞尔转二次贝塞尔高精度递归分割版 * @file 三次贝塞尔转二次贝塞尔高精度递归分割版
* @author mengke01(kekee000@gmail.com) * @author mengke01(kekee000@gmail.com)
* *
* 改进递归分割三次贝塞尔直到可精确近似提高 SSIM * 改进递归分割三次贝塞尔直到可精确近似提高 SSIM
* 优化返回扁平数组 [control1, endpoint1, control2, endpoint2, ...] * 优化160+179: 返回扁平数组 [cx, cy, ex, ey, ...]减少对象分配
* 减少 3-element 包装数组的分配 * 优化179: 新增 bezierCubic2Q2Raw 接受原始坐标参数消除调用方对象分配
*/ */
var MAX_DEPTH = 4; var MAX_DEPTH = 8;
function isFlatEnough(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) { function isFlatEnough(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
var ux = 3 * c1x - 2 * p1x - p2x; var ux = 3 * c1x - 2 * p1x - p2x;
var uy = 3 * c1y - 2 * p1y - p2y; var uy = 3 * c1y - 2 * p1y - p2y;
var vx = 3 * c2x - 2 * p2x - p1x; var vx = 3 * c2x - 2 * p2x - p1x;
var vy = 3 * c2y - 2 * p2y - p1y; var vy = 3 * c2y - 2 * p2y - p1y;
return Math.max(ux * ux + uy * uy, vx * vx + vy * vy) <= 0.25; return Math.max(ux * ux + uy * uy, vx * vx + vy * vy) <= 0.0625;
} }
function cubicToQuads(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, depth, endpoints, controls) { /**
* 优化160+179: 直接构建扁平数组 [cx, cy, ex, ey, ...]
* 每个二次贝塞尔段占 4 个元素控制点 x,y + 端点 x,y
*/
function cubicToQuads(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, depth, result) {
if (isFlatEnough(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) || depth >= MAX_DEPTH) { if (isFlatEnough(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) || depth >= MAX_DEPTH) {
controls.push({ /** 控制点 + 端点 */
x: (3 * c2x - p2x + 3 * c1x - p1x) * 0.25, result.push(
y: (3 * c2y - p2y + 3 * c1y - p1y) * 0.25 (3 * c2x - p2x + 3 * c1x - p1x) * 0.25,
}); (3 * c2y - p2y + 3 * c1y - p1y) * 0.25,
p2x,
p2y
);
return; return;
} }
@ -39,35 +47,33 @@ function cubicToQuads(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, depth, endpoints,
var m123x = (m12x + m23x) * 0.5, m123y = (m12y + m23y) * 0.5; var m123x = (m12x + m23x) * 0.5, m123y = (m12y + m23y) * 0.5;
var midx = (m012x + m123x) * 0.5, midy = (m012y + m123y) * 0.5; var midx = (m012x + m123x) * 0.5, midy = (m012y + m123y) * 0.5;
cubicToQuads(p1x, p1y, m01x, m01y, m012x, m012y, midx, midy, depth + 1, endpoints, controls); cubicToQuads(p1x, p1y, m01x, m01y, m012x, m012y, midx, midy, depth + 1, result);
endpoints.push({ x: midx, y: midy, onCurve: true }); cubicToQuads(midx, midy, m123x, m123y, m23x, m23y, p2x, p2y, depth + 1, result);
cubicToQuads(midx, midy, m123x, m123y, m23x, m23y, p2x, p2y, depth + 1, endpoints, controls);
} }
/** /**
* 三次贝塞尔转二次贝塞尔 * 优化179: 接受原始坐标参数消除调用方临时对象分配
* 返回扁平数组: [control1, endpoint1, control2, endpoint2, ...] * 返回扁平数组: [ctrlX, ctrlY, endX, endY, ctrlX, ctrlY, endX, endY, ...]
* 每对 (control, endpoint) 代表一个二次贝塞尔段
*/ */
function bezierCubic2Q2(p1, c1, c2, p2) { function bezierCubic2Q2Raw(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
if (p1.x === c1.x && p1.y === c1.y && c2.x === p2.x && c2.y === p2.y) { if (p1x === c1x && p1y === c1y && c2x === p2x && c2y === p2y) {
return [{ return [
x: (p1.x + p2.x) * 0.5, (p1x + p2x) * 0.5,
y: (p1.y + p2.y) * 0.5 (p1y + p2y) * 0.5,
}, p2]; p2x,
p2y
];
} }
var endpoints = []; var result = [];
var controls = []; cubicToQuads(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, 0, result);
cubicToQuads(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p2.x, p2.y, 0, endpoints, controls);
var result = new Array(controls.length * 2);
var ri = 0;
for (var i = 0, l = controls.length; i < l; i++) {
var next = i < endpoints.length ? endpoints[i] : p2;
next.onCurve = true;
result[ri++] = controls[i];
result[ri++] = next;
}
return result; return result;
} }
/**
* 三次贝塞尔转二次贝塞尔对象接口兼容旧代码
* 返回扁平数组: [ctrlX, ctrlY, endX, endY, ctrlX, ctrlY, endX, endY, ...]
*/
function bezierCubic2Q2(p1, c1, c2, p2) {
return bezierCubic2Q2Raw(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p2.x, p2.y);
}

View File

@ -38,6 +38,10 @@ function otf2ttfobject(otfBuffer, options) {
var g = glyf[i]; var g = glyf[i];
var result = (0, _otfContours2ttfContours.default)(g.contours); var result = (0, _otfContours2ttfContours.default)(g.contours);
g.contours = result.contours; g.contours = result.contours;
/** 优化178: 标记扁平 contour 格式 */
if (result.contours.length > 0 && typeof result.contours[0][0] === 'number') {
g._flatContours = true;
}
if (result.xMin != null) { if (result.xMin != null) {
g.xMin = result.xMin; g.xMin = result.xMin;
g.xMax = result.xMax; g.xMax = result.xMax;

View File

@ -125,11 +125,18 @@ var OTFReader = exports.default = /*#__PURE__*/function () {
} }
// 设置了subsetMap之后需要选取subset中的字形 // 设置了subsetMap之后需要选取subset中的字形
/* 优化125: Object.keys+forEach → for...in 循环 */ /* 优化167: 密集数组替代 for...in消除字符串键转换 */
if (subsetMap) { if (subsetMap) {
var subGlyf = []; var subGlyf = [];
for (var si in subsetMap) { var subsetGids = font.subsetGids;
subGlyf.push(glyf[+si]); if (subsetGids) {
for (var si = 0, sl = subsetGids.length; si < sl; si++) {
subGlyf.push(glyf[subsetGids[si]]);
}
} else {
for (var si in subsetMap) {
subGlyf.push(glyf[+si]);
}
} }
glyf = subGlyf; glyf = subGlyf;
} }

View File

@ -138,15 +138,21 @@ var Reader = exports.default = /*#__PURE__*/function () {
if (length < 0 || offset + length > this.length) { if (length < 0 || offset + length > this.length) {
_error.default.raise(10001, this.length, offset + length); _error.default.raise(10001, this.length, offset + length);
} }
/* 优化22: 批量读取字节后构建字符串,替代逐字节 readUint8 */ /* 优化22+179: 使用 Uint8Array + String.fromCharCode.apply 批量转换 */
var chars = new Array(length);
var viewOffset = this.view.byteOffset + offset; var viewOffset = this.view.byteOffset + offset;
var buf = this.view.buffer; var bytes = new Uint8Array(this.view.buffer, viewOffset, length);
for (var i = 0; i < length; ++i) {
chars[i] = buf.charCodeAt ? String.fromCharCode(buf.charCodeAt(viewOffset + i)) : String.fromCharCode(new Uint8Array(buf, viewOffset + i, 1)[0]);
}
this.offset = offset + length; this.offset = offset + length;
return chars.join(''); if (length <= 1024) {
return String.fromCharCode.apply(null, bytes);
}
/** 长字符串分段构建,避免 call stack 溢出 */
var parts = [];
var chunkSize = 1024;
for (var ci = 0; ci < length; ci += chunkSize) {
var end = ci + chunkSize < length ? ci + chunkSize : length;
parts.push(String.fromCharCode.apply(null, bytes.subarray(ci, end)));
}
return parts.join('');
} }
/** /**

View File

@ -83,7 +83,10 @@ function parseCFFIndex(reader, offset, conversionFn) {
offsets.push(getOffset(reader, offsetSize)); offsets.push(getOffset(reader, offsetSize));
} }
for (i = 0, l = count; i < l; i++) { for (i = 0, l = count; i < l; i++) {
var value = reader.readBytes(offsets[i + 1] - offsets[i]); /** 优化179: 直接从 view 创建 Uint8Array 视图,避免 readBytes 的 slice */
var objSize = offsets[i + 1] - offsets[i];
var value = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, objSize);
reader.offset += objSize;
if (conversionFn) { if (conversionFn) {
value = conversionFn(value); value = conversionFn(value);
} }
@ -113,8 +116,17 @@ function parseCFFIndexOffsets(reader, offset) {
if (count !== 0) { if (count !== 0) {
var offsetSize = reader.readUint8(); var offsetSize = reader.readUint8();
offsets = new Array(count + 1); offsets = new Array(count + 1);
for (var i = 0; i <= count; i++) { /** 优化166: 内联 getOffset消除函数调用开销 */
offsets[i] = getOffset(reader, offsetSize); if (offsetSize === 1) {
for (var i = 0; i <= count; i++) offsets[i] = reader.readUint8();
} else if (offsetSize === 2) {
for (var i = 0; i <= count; i++) offsets[i] = reader.readUint16();
} else if (offsetSize === 3) {
for (var i = 0; i <= count; i++) {
offsets[i] = reader.readUint8() << 16 | reader.readUint8() << 8 | reader.readUint8();
}
} else {
for (var i = 0; i <= count; i++) offsets[i] = reader.readUint32();
} }
} }
return { offsets: offsets, count: count, dataStart: reader.offset, endOffset: reader.offset }; return { offsets: offsets, count: count, dataStart: reader.offset, endOffset: reader.offset };
@ -128,11 +140,14 @@ function parseCFFIndexOffsets(reader, offset) {
* @param {number} idx object 索引0-based * @param {number} idx object 索引0-based
* @return {Uint8Array} object 数据 * @return {Uint8Array} object 数据
*/ */
/**
* 优化169+179: 直接从 view 创建 Uint8Array 视图避免 readBytes slice 开销
*/
function readCFFIndexObject(reader, indexInfo, idx) { function readCFFIndexObject(reader, indexInfo, idx) {
var off = indexInfo.offsets; var off = indexInfo.offsets;
var size = off[idx + 1] - off[idx]; var size = off[idx + 1] - off[idx];
reader.seek(indexInfo.dataStart + off[idx] - 1); var start = indexInfo.dataStart + off[idx] - 1;
return reader.readBytes(size); return new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, size);
} }
// Subroutines are encoded using the negative half of the number space. // Subroutines are encoded using the negative half of the number space.
@ -275,7 +290,9 @@ function parseFDPrivate(reader, cffOffset, fdDictData, strings) {
result.defaultWidthX = privDict.defaultWidthX || 0; result.defaultWidthX = privDict.defaultWidthX || 0;
result.nominalWidthX = privDict.nominalWidthX || 0; result.nominalWidthX = privDict.nominalWidthX || 0;
if (privDict.subrs) { /** subrs 0CFF
* 原代码用 if (privDict.subrs) 检查0 falsy 导致跳过 subrs 读取 */
if (privDict.subrs != null && privDict.subrs > 0) {
var subrIndex = parseCFFIndex(reader, privOffset + privDict.subrs); var subrIndex = parseCFFIndex(reader, privOffset + privDict.subrs);
result.subrs = subrIndex.objects; result.subrs = subrIndex.objects;
result.subrsBias = calcCFFSubroutineBias(result.subrs); result.subrsBias = calcCFFSubroutineBias(result.subrs);
@ -350,7 +367,7 @@ var _default = exports.default = _table.default.create('cff', [], {
} }
// 私有子glyf数据非 CID 字体使用) // 私有子glyf数据非 CID 字体使用)
if (privateDict.subrs) { if (privateDict.subrs != null && privateDict.subrs > 0) {
var subrOffset = privateDictOffset + privateDict.subrs; var subrOffset = privateDictOffset + privateDict.subrs;
var subrIndex = parseCFFIndex(reader, subrOffset); var subrIndex = parseCFFIndex(reader, subrOffset);
cff.subrs = subrIndex.objects; cff.subrs = subrIndex.objects;
@ -425,20 +442,25 @@ var _default = exports.default = _table.default.create('cff', [], {
for (var si = 0, sl = subset.length; si < sl; si++) { for (var si = 0, sl = subset.length; si < sl; si++) {
subsetSet[subset[si]] = true; subsetSet[subset[si]] = true;
} }
for (var c in codes) { /** 优化168: 用 subset 直接遍历替代 codes for...in减少遍历量 */
if (subsetSet[c]) { for (var si = 0, sl = subset.length; si < sl; si++) {
var ci = codes[c]; var ci = codes[subset[si]];
subsetMap[ci] = true; if (ci !== undefined) subsetMap[ci] = true;
}
} }
font.subsetMap = subsetMap; font.subsetMap = subsetMap;
for (var i in subsetMap) { /** 优化163+167: 构建 subsetGids 密集数组,传递给 otfreader 复用 */
i = +i; var subsetGids = [];
for (var gi = 0; gi < nGlyphs; gi++) {
if (subsetMap[gi]) subsetGids.push(gi);
}
for (var si = 0, sl = subsetGids.length; si < sl; si++) {
var i = subsetGids[si];
var charstring = readCFFIndexObject(reader, charStringsInfo, i); var charstring = readCFFIndexObject(reader, charStringsInfo, i);
var glyf = (0, _parseCFFGlyph.default)(charstring, getGlyphFont(i), i); var glyf = (0, _parseCFFGlyph.default)(charstring, getGlyphFont(i), i);
glyf.name = cff.charset[i]; glyf.name = cff.charset[i];
cff.glyf[i] = glyf; cff.glyf[i] = glyf;
} }
font.subsetGids = subsetGids;
} }
// parse all // parse all
else { else {

View File

@ -28,6 +28,64 @@ var _default = exports.default = _table.default.create('OS/2', [['version', _str
['sxHeight', _struct.default.Int16], ['sCapHeight', _struct.default.Int16], ['usDefaultChar', _struct.default.Uint16], ['usBreakChar', _struct.default.Uint16], ['usMaxContext', _struct.default.Uint16] ['sxHeight', _struct.default.Int16], ['sCapHeight', _struct.default.Int16], ['usDefaultChar', _struct.default.Uint16], ['usBreakChar', _struct.default.Uint16], ['usMaxContext', _struct.default.Uint16]
// version 2,3,4 above 46 // version 2,3,4 above 46
], { ], {
/** 优化176: 直接 view 写入 96 字节,绕过 table.js 双重 switch 分发 */
write: function write(writer, ttf) {
var o = ttf['OS/2'];
var pos = writer.offset;
var view = writer.view;
view.setUint16(pos, o.version, false); pos += 2;
view.setInt16(pos, o.xAvgCharWidth, false); pos += 2;
view.setUint16(pos, o.usWeightClass, false); pos += 2;
view.setUint16(pos, o.usWidthClass, false); pos += 2;
view.setUint16(pos, o.fsType, false); pos += 2;
view.setUint16(pos, o.ySubscriptXSize, false); pos += 2;
view.setUint16(pos, o.ySubscriptYSize, false); pos += 2;
view.setUint16(pos, o.ySubscriptXOffset, false); pos += 2;
view.setUint16(pos, o.ySubscriptYOffset, false); pos += 2;
view.setUint16(pos, o.ySuperscriptXSize, false); pos += 2;
view.setUint16(pos, o.ySuperscriptYSize, false); pos += 2;
view.setUint16(pos, o.ySuperscriptXOffset, false); pos += 2;
view.setUint16(pos, o.ySuperscriptYOffset, false); pos += 2;
view.setUint16(pos, o.yStrikeoutSize, false); pos += 2;
view.setUint16(pos, o.yStrikeoutPosition, false); pos += 2;
view.setUint16(pos, o.sFamilyClass, false); pos += 2;
view.setUint8(pos, o.bFamilyType); pos += 1;
view.setUint8(pos, o.bSerifStyle); pos += 1;
view.setUint8(pos, o.bWeight); pos += 1;
view.setUint8(pos, o.bProportion); pos += 1;
view.setUint8(pos, o.bContrast); pos += 1;
view.setUint8(pos, o.bStrokeVariation); pos += 1;
view.setUint8(pos, o.bArmStyle); pos += 1;
view.setUint8(pos, o.bLetterform); pos += 1;
view.setUint8(pos, o.bMidline); pos += 1;
view.setUint8(pos, o.bXHeight); pos += 1;
view.setUint32(pos, o.ulUnicodeRange1 || 0, false); pos += 4;
view.setUint32(pos, o.ulUnicodeRange2 || 0, false); pos += 4;
view.setUint32(pos, o.ulUnicodeRange3 || 0, false); pos += 4;
view.setUint32(pos, o.ulUnicodeRange4 || 0, false); pos += 4;
var vendor = (o.achVendID || ' ').slice(0, 4);
view.setUint8(pos, vendor.charCodeAt(0)); pos += 1;
view.setUint8(pos, vendor.charCodeAt(1)); pos += 1;
view.setUint8(pos, vendor.charCodeAt(2)); pos += 1;
view.setUint8(pos, vendor.charCodeAt(3)); pos += 1;
view.setUint16(pos, o.fsSelection, false); pos += 2;
view.setUint16(pos, o.usFirstCharIndex, false); pos += 2;
view.setUint16(pos, o.usLastCharIndex, false); pos += 2;
view.setInt16(pos, o.sTypoAscender, false); pos += 2;
view.setInt16(pos, o.sTypoDescender, false); pos += 2;
view.setInt16(pos, o.sTypoLineGap, false); pos += 2;
view.setUint16(pos, o.usWinAscent, false); pos += 2;
view.setUint16(pos, o.usWinDescent, false); pos += 2;
view.setUint32(pos, o.ulCodePageRange1 || 0, false); pos += 4;
view.setUint32(pos, o.ulCodePageRange2 || 0, false); pos += 4;
view.setInt16(pos, o.sxHeight || 0, false); pos += 2;
view.setInt16(pos, o.sCapHeight || 0, false); pos += 2;
view.setUint16(pos, o.usDefaultChar || 0, false); pos += 2;
view.setUint16(pos, o.usBreakChar != null ? o.usBreakChar : 32, false); pos += 2;
view.setUint16(pos, o.usMaxContext || 0, false); pos += 2;
writer.offset = pos;
return writer;
},
read: function read(reader, ttf) { read: function read(reader, ttf) {
var format = reader.readUint16(this.offset); var format = reader.readUint16(this.offset);
var struct = this.struct; var struct = this.struct;

View File

@ -7,24 +7,21 @@ exports.default = parseCFFCharstring;
/** /**
* @file 解析cff字形 * @file 解析cff字形
* @author mengke01(kekee000@gmail.com) * @author mengke01(kekee000@gmail.com)
*
* 优化157: stack.shift 索引指针消除 O(n) 数组移位
* 优化158: 内联 lineTo/curveTo/newContour减少函数调用开销
* 优化159: contours.map 内联闭合点检测消除二次遍历
* 优化178: contour 使用扁平数组 [x, y, flag, ...]消除对象分配
*/ */
/** /** onCurve 标志位 */
* 解析cff字形返回直线和三次bezier曲线点数组 var ON_CURVE = 1;
*
* @param {Array} code 操作码
* @param {Object} font 相关联的font对象
* @param {number} index glyf索引
* @return {Object} glyf对象
*/
function parseCFFCharstring(code, font, index) { function parseCFFCharstring(code, font, index) {
var c1x;
var c1y;
var c2x;
var c2y;
var contours = []; var contours = [];
var contour = []; var contour = [];
var stack = []; /** 优化170: 预分配 stack 为 48 元素数组,避免动态扩容 */
var stack = new Array(48);
var glyfs = []; var glyfs = [];
var nStems = 0; var nStems = 0;
var haveWidth = false; var haveWidth = false;
@ -32,47 +29,30 @@ function parseCFFCharstring(code, font, index) {
var open = false; var open = false;
var x = 0; var x = 0;
var y = 0; var y = 0;
function lineTo(x, y) {
contour.push({ /**
onCurve: true, * 优化179: 模块级 closeContour避免闭包捕获
x: x, */
y: y function closeContour(arr) {
}); var cLen = arr.length;
} if (cLen >= 6 && arr[0] === arr[cLen - 3] && arr[1] === arr[cLen - 2]) {
function curveTo(c1x, c1y, c2x, c2y, x, y) { arr.length = cLen - 3;
contour.push({
x: c1x,
y: c1y
});
contour.push({
x: c2x,
y: c2y
});
contour.push({
onCurve: true,
x: x,
y: y
});
}
function newContour(x, y) {
if (open) {
contours.push(contour);
} }
contour = []; contours.push(arr);
lineTo(x, y); }
function startContour(px, py) {
if (open) closeContour(contour);
contour = [px, py, ON_CURVE];
open = true; open = true;
} }
function parseStems() {
// The number of stem operators on the stack is always even. /**
// If the value is uneven, that means a width is specified. * 优化157: sp (stack pointer) si (stack index) 替代 shift/pop
var hasWidthArg = stack.length % 2 !== 0; * push stack[sp++] = val
if (hasWidthArg && !haveWidth) { * pop stack[--sp]
width = stack.shift() + font.nominalWidthX; * shift stack[si++] (读取后 si 追赶 sp)
} * stack.length sp - si (有效元素数)
nStems += stack.length >> 1; */
stack.length = 0;
haveWidth = true;
}
function parse(code) { function parse(code) {
var b1; var b1;
var b2; var b2;
@ -82,83 +62,106 @@ function parseCFFCharstring(code, font, index) {
var subrCode; var subrCode;
var jpx; var jpx;
var jpy; var jpy;
var c1x;
var c1y;
var c2x;
var c2y;
var c3x; var c3x;
var c3y; var c3y;
var c4x; var c4x;
var c4y; var c4y;
var i = 0; var i = 0;
/** 索引指针替代 shift/pop */
var sp = 0;
var si = 0;
while (i < code.length) { while (i < code.length) {
var v = code[i]; var v = code[i];
i += 1; i += 1;
switch (v) { switch (v) {
case 1: case 1:
// hstem // hstem
parseStems();
break;
case 3: case 3:
// vstem // vstem
parseStems(); case 18:
// hstemhm
case 23:
// vstemhm
{
/** parseStems 内联 */
var sLen = sp - si;
if (sLen & 1 && !haveWidth) {
width = stack[si++] + font.nominalWidthX;
sLen--;
}
nStems += sLen >> 1;
sp = si = 0;
haveWidth = true;
}
break; break;
case 4: case 4:
// vmoveto // vmoveto
if (stack.length > 1 && !haveWidth) { if (sp - si > 1 && !haveWidth) {
width = stack.shift() + font.nominalWidthX; width = stack[si++] + font.nominalWidthX;
haveWidth = true; haveWidth = true;
} }
y += stack.pop(); y += stack[--sp];
newContour(x, y); si = sp;
startContour(x, y);
break; break;
case 5: case 5:
// rlineto // rlineto
while (stack.length > 0) { while (sp - si > 0) {
x += stack.shift(); x += stack[si++];
y += stack.shift(); y += stack[si++];
lineTo(x, y); contour.push(x, y, ON_CURVE);
} }
sp = si = 0;
break; break;
case 6: case 6:
// hlineto // hlineto
while (stack.length > 0) { while (sp - si > 0) {
x += stack.shift(); x += stack[si++];
lineTo(x, y); contour.push(x, y, ON_CURVE);
if (stack.length === 0) { if (sp - si === 0) break;
break; y += stack[si++];
} contour.push(x, y, ON_CURVE);
y += stack.shift();
lineTo(x, y);
} }
sp = si = 0;
break; break;
case 7: case 7:
// vlineto // vlineto
while (stack.length > 0) { while (sp - si > 0) {
y += stack.shift(); y += stack[si++];
lineTo(x, y); contour.push(x, y, ON_CURVE);
if (stack.length === 0) { if (sp - si === 0) break;
break; x += stack[si++];
} contour.push(x, y, ON_CURVE);
x += stack.shift();
lineTo(x, y);
} }
sp = si = 0;
break; break;
case 8: case 8:
// rrcurveto // rrcurveto
while (stack.length > 0) { while (sp - si > 0) {
c1x = x + stack.shift(); c1x = x + stack[si++];
c1y = y + stack.shift(); c1y = y + stack[si++];
c2x = c1x + stack.shift(); c2x = c1x + stack[si++];
c2y = c1y + stack.shift(); c2y = c1y + stack[si++];
x = c2x + stack.shift(); x = c2x + stack[si++];
y = c2y + stack.shift(); y = c2y + stack[si++];
curveTo(c1x, c1y, c2x, c2y, x, y); contour.push(c1x, c1y, 0);
contour.push(c2x, c2y, 0);
contour.push(x, y, ON_CURVE);
} }
sp = si = 0;
break; break;
case 10: case 10:
// callsubr // callsubr
codeIndex = stack.pop() + font.subrsBias; codeIndex = stack[--sp] + font.subrsBias;
subrCode = font.subrs[codeIndex]; subrCode = font.subrs[codeIndex];
if (subrCode) { if (subrCode) {
parse(subrCode); parse(subrCode);
} }
si = sp;
break; break;
case 11: case 11:
// return // return
@ -170,338 +173,340 @@ function parseCFFCharstring(code, font, index) {
switch (v) { switch (v) {
case 35: case 35:
// flex // flex
// |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd flex (12 35) |- c1x = x + stack[si++];
c1x = x + stack.shift(); // dx1 c1y = y + stack[si++];
c1y = y + stack.shift(); // dy1 c2x = c1x + stack[si++];
c2x = c1x + stack.shift(); // dx2 c2y = c1y + stack[si++];
c2y = c1y + stack.shift(); // dy2 jpx = c2x + stack[si++];
jpx = c2x + stack.shift(); // dx3 jpy = c2y + stack[si++];
jpy = c2y + stack.shift(); // dy3 c3x = jpx + stack[si++];
c3x = jpx + stack.shift(); // dx4 c3y = jpy + stack[si++];
c3y = jpy + stack.shift(); // dy4 c4x = c3x + stack[si++];
c4x = c3x + stack.shift(); // dx5 c4y = c3y + stack[si++];
c4y = c3y + stack.shift(); // dy5 x = c4x + stack[si++];
x = c4x + stack.shift(); // dx6 y = c4y + stack[si++];
y = c4y + stack.shift(); // dy6 si++;
stack.shift(); // flex depth contour.push(c1x, c1y, 0);
curveTo(c1x, c1y, c2x, c2y, jpx, jpy); contour.push(c2x, c2y, 0);
curveTo(c3x, c3y, c4x, c4y, x, y); contour.push(jpx, jpy, ON_CURVE);
contour.push(c3x, c3y, 0);
contour.push(c4x, c4y, 0);
contour.push(x, y, ON_CURVE);
break; break;
case 34: case 34:
// hflex // hflex
// |- dx1 dx2 dy2 dx3 dx4 dx5 dx6 hflex (12 34) |- c1x = x + stack[si++];
c1x = x + stack.shift(); // dx1 c1y = y;
c1y = y; // dy1 c2x = c1x + stack[si++];
c2x = c1x + stack.shift(); // dx2 c2y = c1y + stack[si++];
c2y = c1y + stack.shift(); // dy2 jpx = c2x + stack[si++];
jpx = c2x + stack.shift(); // dx3 jpy = c2y;
jpy = c2y; // dy3 c3x = jpx + stack[si++];
c3x = jpx + stack.shift(); // dx4 c3y = c2y;
c3y = c2y; // dy4 c4x = c3x + stack[si++];
c4x = c3x + stack.shift(); // dx5 c4y = y;
c4y = y; // dy5 x = c4x + stack[si++];
x = c4x + stack.shift(); // dx6 contour.push(c1x, c1y, 0);
curveTo(c1x, c1y, c2x, c2y, jpx, jpy); contour.push(c2x, c2y, 0);
curveTo(c3x, c3y, c4x, c4y, x, y); contour.push(jpx, jpy, ON_CURVE);
contour.push(c3x, c3y, 0);
contour.push(c4x, c4y, 0);
contour.push(x, y, ON_CURVE);
break; break;
case 36: case 36:
// hflex1 // hflex1
// |- dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6 hflex1 (12 36) |- c1x = x + stack[si++];
c1x = x + stack.shift(); // dx1 c1y = y + stack[si++];
c1y = y + stack.shift(); // dy1 c2x = c1x + stack[si++];
c2x = c1x + stack.shift(); // dx2 c2y = c1y + stack[si++];
c2y = c1y + stack.shift(); // dy2 jpx = c2x + stack[si++];
jpx = c2x + stack.shift(); // dx3 jpy = c2y;
jpy = c2y; // dy3 c3x = jpx + stack[si++];
c3x = jpx + stack.shift(); // dx4 c3y = c2y;
c3y = c2y; // dy4 c4x = c3x + stack[si++];
c4x = c3x + stack.shift(); // dx5 c4y = c3y + stack[si++];
c4y = c3y + stack.shift(); // dy5 x = c4x + stack[si++];
x = c4x + stack.shift(); // dx6 contour.push(c1x, c1y, 0);
curveTo(c1x, c1y, c2x, c2y, jpx, jpy); contour.push(c2x, c2y, 0);
curveTo(c3x, c3y, c4x, c4y, x, y); contour.push(jpx, jpy, ON_CURVE);
contour.push(c3x, c3y, 0);
contour.push(c4x, c4y, 0);
contour.push(x, y, ON_CURVE);
break; break;
case 37: case 37:
// flex1 // flex1
// |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6 flex1 (12 37) |- c1x = x + stack[si++];
c1x = x + stack.shift(); // dx1 c1y = y + stack[si++];
c1y = y + stack.shift(); // dy1 c2x = c1x + stack[si++];
c2x = c1x + stack.shift(); // dx2 c2y = c1y + stack[si++];
c2y = c1y + stack.shift(); // dy2 jpx = c2x + stack[si++];
jpx = c2x + stack.shift(); // dx3 jpy = c2y + stack[si++];
jpy = c2y + stack.shift(); // dy3 c3x = jpx + stack[si++];
c3x = jpx + stack.shift(); // dx4 c3y = jpy + stack[si++];
c3y = jpy + stack.shift(); // dy4 c4x = c3x + stack[si++];
c4x = c3x + stack.shift(); // dx5 c4y = c3y + stack[si++];
c4y = c3y + stack.shift(); // dy5 if (c4x - x > 0 ? c4x - x > -(c4y - y) : -(c4x - x) < c4y - y) {
if (Math.abs(c4x - x) > Math.abs(c4y - y)) { x = c4x + stack[si++];
x = c4x + stack.shift();
} else { } else {
y = c4y + stack.shift(); y = c4y + stack[si++];
} }
curveTo(c1x, c1y, c2x, c2y, jpx, jpy); contour.push(c1x, c1y, 0);
curveTo(c3x, c3y, c4x, c4y, x, y); contour.push(c2x, c2y, 0);
contour.push(jpx, jpy, ON_CURVE);
contour.push(c3x, c3y, 0);
contour.push(c4x, c4y, 0);
contour.push(x, y, ON_CURVE);
break; break;
default: default:
console.warn('Glyph ' + index + ': unknown operator ' + (1200 + v)); console.warn('Glyph ' + index + ': unknown operator ' + (1200 + v));
stack.length = 0;
} }
sp = si = 0;
break; break;
case 14: case 14:
// endchar // endchar
if (stack.length === 1 && !haveWidth) { if (sp - si === 1 && !haveWidth) {
width = stack.shift() + font.nominalWidthX; width = stack[si++] + font.nominalWidthX;
haveWidth = true; haveWidth = true;
} else if (stack.length === 4) { } else if (sp - si === 4) {
glyfs[1] = { glyfs[1] = {
glyphIndex: font.charset.indexOf(font.encoding[stack.pop()]), glyphIndex: font.charset.indexOf(font.encoding[stack[--sp]]),
transform: { transform: { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }
a: 1,
b: 0,
c: 0,
d: 1,
e: 0,
f: 0
}
}; };
glyfs[0] = { glyfs[0] = {
glyphIndex: font.charset.indexOf(font.encoding[stack.pop()]), glyphIndex: font.charset.indexOf(font.encoding[stack[--sp]]),
transform: { transform: { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }
a: 1,
b: 0,
c: 0,
d: 1,
e: 0,
f: 0
}
}; };
glyfs[1].transform.f = stack.pop(); glyfs[1].transform.f = stack[--sp];
glyfs[1].transform.e = stack.pop(); glyfs[1].transform.e = stack[--sp];
} else if (stack.length === 5) { } else if (sp - si === 5) {
if (!haveWidth) { if (!haveWidth) {
width = stack.shift() + font.nominalWidthX; width = stack[si++] + font.nominalWidthX;
} }
haveWidth = true; haveWidth = true;
glyfs[1] = { glyfs[1] = {
glyphIndex: font.charset.indexOf(font.encoding[stack.pop()]), glyphIndex: font.charset.indexOf(font.encoding[stack[--sp]]),
transform: { transform: { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }
a: 1,
b: 0,
c: 0,
d: 1,
e: 0,
f: 0
}
}; };
glyfs[0] = { glyfs[0] = {
glyphIndex: font.charset.indexOf(font.encoding[stack.pop()]), glyphIndex: font.charset.indexOf(font.encoding[stack[--sp]]),
transform: { transform: { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }
a: 1,
b: 0,
c: 0,
d: 1,
e: 0,
f: 0
}
}; };
glyfs[1].transform.f = stack.pop(); glyfs[1].transform.f = stack[--sp];
glyfs[1].transform.e = stack.pop(); glyfs[1].transform.e = stack[--sp];
} }
if (open) { if (open) {
contours.push(contour); closeContour(contour);
open = false; open = false;
} }
break; sp = si = 0;
case 18:
// hstemhm
parseStems();
break; break;
case 19: // hintmask case 19: // hintmask
case 20: case 20:
// cntrmask // cntrmask
parseStems(); {
i += nStems + 7 >> 3; var sLen2 = sp - si;
if (sLen2 & 1 && !haveWidth) {
width = stack[si++] + font.nominalWidthX;
sLen2--;
}
nStems += sLen2 >> 1;
sp = si = 0;
haveWidth = true;
i += nStems + 7 >> 3;
}
break; break;
case 21: case 21:
// rmoveto // rmoveto
if (stack.length > 2 && !haveWidth) { if (sp - si > 2 && !haveWidth) {
width = stack.shift() + font.nominalWidthX; width = stack[si++] + font.nominalWidthX;
haveWidth = true; haveWidth = true;
} }
y += stack.pop(); y += stack[--sp];
x += stack.pop(); x += stack[--sp];
newContour(x, y); si = sp;
startContour(x, y);
break; break;
case 22: case 22:
// hmoveto // hmoveto
if (stack.length > 1 && !haveWidth) { if (sp - si > 1 && !haveWidth) {
width = stack.shift() + font.nominalWidthX; width = stack[si++] + font.nominalWidthX;
haveWidth = true; haveWidth = true;
} }
x += stack.pop(); x += stack[--sp];
newContour(x, y); si = sp;
break; startContour(x, y);
case 23:
// vstemhm
parseStems();
break; break;
case 24: case 24:
// rcurveline // rcurveline
while (stack.length > 2) { while (sp - si > 2) {
c1x = x + stack.shift(); c1x = x + stack[si++];
c1y = y + stack.shift(); c1y = y + stack[si++];
c2x = c1x + stack.shift(); c2x = c1x + stack[si++];
c2y = c1y + stack.shift(); c2y = c1y + stack[si++];
x = c2x + stack.shift(); x = c2x + stack[si++];
y = c2y + stack.shift(); y = c2y + stack[si++];
curveTo(c1x, c1y, c2x, c2y, x, y); contour.push(c1x, c1y, 0);
contour.push(c2x, c2y, 0);
contour.push(x, y, ON_CURVE);
} }
x += stack.shift(); x += stack[si++];
y += stack.shift(); y += stack[si++];
lineTo(x, y); contour.push(x, y, ON_CURVE);
sp = si = 0;
break; break;
case 25: case 25:
// rlinecurve // rlinecurve
while (stack.length > 6) { while (sp - si > 6) {
x += stack.shift(); x += stack[si++];
y += stack.shift(); y += stack[si++];
lineTo(x, y); contour.push(x, y, ON_CURVE);
} }
c1x = x + stack.shift(); c1x = x + stack[si++];
c1y = y + stack.shift(); c1y = y + stack[si++];
c2x = c1x + stack.shift(); c2x = c1x + stack[si++];
c2y = c1y + stack.shift(); c2y = c1y + stack[si++];
x = c2x + stack.shift(); x = c2x + stack[si++];
y = c2y + stack.shift(); y = c2y + stack[si++];
curveTo(c1x, c1y, c2x, c2y, x, y); contour.push(c1x, c1y, 0);
contour.push(c2x, c2y, 0);
contour.push(x, y, ON_CURVE);
sp = si = 0;
break; break;
case 26: case 26:
// vvcurveto // vvcurveto
if (stack.length % 2) { if ((sp - si) & 1) {
x += stack.shift(); x += stack[si++];
} }
while (stack.length > 0) { while (sp - si > 0) {
c1x = x; c1x = x;
c1y = y + stack.shift(); c1y = y + stack[si++];
c2x = c1x + stack.shift(); c2x = c1x + stack[si++];
c2y = c1y + stack.shift(); c2y = c1y + stack[si++];
x = c2x; x = c2x;
y = c2y + stack.shift(); y = c2y + stack[si++];
curveTo(c1x, c1y, c2x, c2y, x, y); contour.push(c1x, c1y, 0);
contour.push(c2x, c2y, 0);
contour.push(x, y, ON_CURVE);
} }
sp = si = 0;
break; break;
case 27: case 27:
// hhcurveto // hhcurveto
if (stack.length % 2) { if ((sp - si) & 1) {
y += stack.shift(); y += stack[si++];
} }
while (stack.length > 0) { while (sp - si > 0) {
c1x = x + stack.shift(); c1x = x + stack[si++];
c1y = y; c1y = y;
c2x = c1x + stack.shift(); c2x = c1x + stack[si++];
c2y = c1y + stack.shift(); c2y = c1y + stack[si++];
x = c2x + stack.shift(); x = c2x + stack[si++];
y = c2y; y = c2y;
curveTo(c1x, c1y, c2x, c2y, x, y); contour.push(c1x, c1y, 0);
contour.push(c2x, c2y, 0);
contour.push(x, y, ON_CURVE);
} }
sp = si = 0;
break; break;
case 28: case 28:
// shortint // shortint
b1 = code[i]; b1 = code[i];
b2 = code[i + 1]; b2 = code[i + 1];
stack.push((b1 << 24 | b2 << 16) >> 16); stack[sp++] = (b1 << 24 | b2 << 16) >> 16;
i += 2; i += 2;
break; break;
case 29: case 29:
// callgsubr // callgsubr
codeIndex = stack.pop() + font.gsubrsBias; codeIndex = stack[--sp] + font.gsubrsBias;
subrCode = font.gsubrs[codeIndex]; subrCode = font.gsubrs[codeIndex];
if (subrCode) { if (subrCode) {
parse(subrCode); parse(subrCode);
} }
si = sp;
break; break;
case 30: case 30:
// vhcurveto // vhcurveto
while (stack.length > 0) { while (sp - si > 0) {
c1x = x; c1x = x;
c1y = y + stack.shift(); c1y = y + stack[si++];
c2x = c1x + stack.shift(); c2x = c1x + stack[si++];
c2y = c1y + stack.shift(); c2y = c1y + stack[si++];
x = c2x + stack.shift(); x = c2x + stack[si++];
y = c2y + (stack.length === 1 ? stack.shift() : 0); y = c2y + (sp - si === 1 ? stack[si++] : 0);
curveTo(c1x, c1y, c2x, c2y, x, y); contour.push(c1x, c1y, 0);
if (stack.length === 0) { contour.push(c2x, c2y, 0);
break; contour.push(x, y, ON_CURVE);
} if (sp - si === 0) break;
c1x = x + stack.shift(); c1x = x + stack[si++];
c1y = y; c1y = y;
c2x = c1x + stack.shift(); c2x = c1x + stack[si++];
c2y = c1y + stack.shift(); c2y = c1y + stack[si++];
y = c2y + stack.shift(); y = c2y + stack[si++];
x = c2x + (stack.length === 1 ? stack.shift() : 0); x = c2x + (sp - si === 1 ? stack[si++] : 0);
curveTo(c1x, c1y, c2x, c2y, x, y); contour.push(c1x, c1y, 0);
contour.push(c2x, c2y, 0);
contour.push(x, y, ON_CURVE);
} }
sp = si = 0;
break; break;
case 31: case 31:
// hvcurveto // hvcurveto
while (stack.length > 0) { while (sp - si > 0) {
c1x = x + stack.shift(); c1x = x + stack[si++];
c1y = y; c1y = y;
c2x = c1x + stack.shift(); c2x = c1x + stack[si++];
c2y = c1y + stack.shift(); c2y = c1y + stack[si++];
y = c2y + stack.shift(); y = c2y + stack[si++];
x = c2x + (stack.length === 1 ? stack.shift() : 0); x = c2x + (sp - si === 1 ? stack[si++] : 0);
curveTo(c1x, c1y, c2x, c2y, x, y); contour.push(c1x, c1y, 0);
if (stack.length === 0) { contour.push(c2x, c2y, 0);
break; contour.push(x, y, ON_CURVE);
} if (sp - si === 0) break;
c1x = x; c1x = x;
c1y = y + stack.shift(); c1y = y + stack[si++];
c2x = c1x + stack.shift(); c2x = c1x + stack[si++];
c2y = c1y + stack.shift(); c2y = c1y + stack[si++];
x = c2x + stack.shift(); x = c2x + stack[si++];
y = c2y + (stack.length === 1 ? stack.shift() : 0); y = c2y + (sp - si === 1 ? stack[si++] : 0);
curveTo(c1x, c1y, c2x, c2y, x, y); contour.push(c1x, c1y, 0);
contour.push(c2x, c2y, 0);
contour.push(x, y, ON_CURVE);
} }
sp = si = 0;
break; break;
default: default:
if (v < 32) { if (v < 32) {
console.warn('Glyph ' + index + ': unknown operator ' + v); console.warn('Glyph ' + index + ': unknown operator ' + v);
} else if (v < 247) { } else if (v < 247) {
stack.push(v - 139); stack[sp++] = v - 139;
} else if (v < 251) { } else if (v < 251) {
b1 = code[i]; b1 = code[i];
i += 1; i += 1;
stack.push((v - 247) * 256 + b1 + 108); stack[sp++] = (v - 247) * 256 + b1 + 108;
} else if (v < 255) { } else if (v < 255) {
b1 = code[i]; b1 = code[i];
i += 1; i += 1;
stack.push(-(v - 251) * 256 - b1 - 108); stack[sp++] = -(v - 251) * 256 - b1 - 108;
} else { } else {
b1 = code[i]; b1 = code[i];
b2 = code[i + 1]; b2 = code[i + 1];
b3 = code[i + 2]; b3 = code[i + 2];
b4 = code[i + 3]; b4 = code[i + 3];
i += 4; i += 4;
stack.push((b1 << 24 | b2 << 16 | b3 << 8 | b4) / 65536); stack[sp++] = (b1 << 24 | b2 << 16 | b3 << 8 | b4) / 65536;
} }
} }
} }
} }
parse(code); parse(code);
var glyf = { var glyf = {
contours: contours.map(function (contour) { contours: contours,
var last = contour.length - 1; advanceWidth: width,
if (last > 0 && contour[0].x === contour[last].x && contour[0].y === contour[last].y) { _flatContours: true
contour.length = last;
}
return contour;
}),
advanceWidth: width
}; };
if (glyfs.length) { if (glyfs.length) {
glyf.compound = true; glyf.compound = true;
glyf.glyfs = glyfs; glyf.glyfs = glyfs;
} }
return glyf; return glyf;
} }

View File

@ -118,6 +118,7 @@ function readSubTable(reader, ttf, subTable, cmapOffset) {
vOffset += 2; vOffset += 2;
} }
format4.idRangeOffset = idRangeOffset; format4.idRangeOffset = idRangeOffset;
format4.segCount = segCount;
/* 优化101: subset 模式下跳过 glyphIdArray 解析,直接从 view 按需读取 */ /* 优化101: subset 模式下跳过 glyphIdArray 解析,直接从 view 按需读取 */
var isSubset4 = ttf.readOptions && ttf.readOptions.subset; var isSubset4 = ttf.readOptions && ttf.readOptions.subset;
@ -135,6 +136,8 @@ function readSubTable(reader, ttf, subTable, cmapOffset) {
} }
format4.glyphIdArray = glyphIdArray4; format4.glyphIdArray = glyphIdArray4;
} }
/* 优化177: 预计算 glyphIdArrayIndexOffset消除 lookupFormat4 中的重复除法 */
format4.glyphIdArrayIndexOffset = (format4.glyphIdArrayOffset - format4.idRangeOffsetOffset) / 2;
} else if (subTable.format === 6) { } else if (subTable.format === 6) {
var format6 = subTable; var format6 = subTable;
format6.length = view.getUint16(vOffset, false); vOffset += 2; format6.length = view.getUint16(vOffset, false); vOffset += 2;

View File

@ -13,6 +13,55 @@ function encodeDelta(delta) {
return delta > 0x7FFF ? delta - 0x10000 : delta < -0x7FFF ? delta + 0x10000 : delta; return delta > 0x7FFF ? delta - 0x10000 : delta < -0x7FFF ? delta + 0x10000 : delta;
} }
/** 优化155: 扁平数组版本的 getSegments使用并行数组替代对象数组 */
function getSegmentsFlat(unicodeArr, idArr, bound) {
var result = [];
var len = unicodeArr.length;
if (len === 0) return result;
var segStart = -1;
var segStartId = 0;
var segDelta = 0;
var prevUnicode = -1;
var prevId = 0;
var hasValid = false;
for (var i = 0; i < len; i++) {
var u = unicodeArr[i];
var id = idArr[i];
if (bound === undefined || u <= bound) {
if (!hasValid) {
segStart = u;
segStartId = id;
segDelta = encodeDelta(id - u);
hasValid = true;
} else if (u !== prevUnicode + 1 || id !== prevId + 1) {
result.push(segStart, prevUnicode, segStartId, segDelta);
segStart = u;
segStartId = id;
segDelta = encodeDelta(id - u);
}
prevUnicode = u;
prevId = id;
}
}
if (hasValid) {
result.push(segStart, prevUnicode, segStartId, segDelta);
}
return result;
}
/** 优化155: 扁平数组版本的 getFormat0Segment */
function getFormat0SegmentFlat(unicodeArr, idArr) {
var unicodes = [];
for (var i = 0, l = unicodeArr.length; i < l; i++) {
if (unicodeArr[i] < 256) {
unicodes.push(unicodeArr[i], idArr[i]);
}
}
return unicodes;
}
function getSegments(glyfUnicodes, bound) { function getSegments(glyfUnicodes, bound) {
var prevGlyph = null; var prevGlyph = null;
var result = []; var result = [];
@ -59,8 +108,10 @@ function getFormat0Segment(glyfUnicodes) {
function sizeof(ttf) { function sizeof(ttf) {
ttf.support.cmap = {}; ttf.support.cmap = {};
var glyfUnicodes = []; /* 优化155: 使用并行扁平数组替代对象数组,减少 GC 压力 */
var glyfs = ttf.glyf; var glyfs = ttf.glyf;
var unicodeArr = [];
var idArr = [];
for (var index = 0, gl = glyfs.length; index < gl; index++) { for (var index = 0, gl = glyfs.length; index < gl; index++) {
var glyph = glyfs[index]; var glyph = glyfs[index];
var unicodes = glyph.unicode; var unicodes = glyph.unicode;
@ -69,36 +120,58 @@ function sizeof(ttf) {
} }
if (unicodes && unicodes.length) { if (unicodes && unicodes.length) {
for (var ui = 0, ul = unicodes.length; ui < ul; ui++) { for (var ui = 0, ul = unicodes.length; ui < ul; ui++) {
glyfUnicodes.push({ unicodeArr.push(unicodes[ui]);
unicode: unicodes[ui], idArr.push(unicodes[ui] !== 0xFFFF ? index : 0);
id: unicodes[ui] !== 0xFFFF ? index : 0
});
} }
} }
} }
glyfUnicodes.sort(function (a, b) { return a.unicode - b.unicode; }); /* 优化179: 二分插入排序O(n log n) 查找 + O(n) 移位 */
ttf.support.cmap.unicodes = glyfUnicodes; var len = unicodeArr.length;
var unicodes2Bytes = glyfUnicodes; for (var i = 1; i < len; i++) {
ttf.support.cmap.format4Segments = getSegments(unicodes2Bytes, 0xFFFF); var uKey = unicodeArr[i];
ttf.support.cmap.format4Size = 24 + ttf.support.cmap.format4Segments.length * 8; var iKey = idArr[i];
ttf.support.cmap.format0Segments = getFormat0Segment(glyfUnicodes); var lo = 0, hi = i - 1;
while (lo <= hi) {
var mid = (lo + hi) >> 1;
if (unicodeArr[mid] > uKey) hi = mid - 1;
else lo = mid + 1;
}
if (lo !== i) {
unicodeArr.copyWithin(lo + 1, lo, i);
idArr.copyWithin(lo + 1, lo, i);
unicodeArr[lo] = uKey;
idArr[lo] = iKey;
}
}
ttf.support.cmap.format4Segments = getSegmentsFlat(unicodeArr, idArr, 0xFFFF);
/** format4Size 需要包含 sentinel segment (+1),与 write.js 中的 segCount = segments.length/4 + 1 一致 */
/**
* format4Size = header(14) + reservedPad(2) + segCount * 2 * 4(four arrays)
* segCount = 实际段数 + 1(sentinel 0xFFFF)
*/
var format4SegCount = ttf.support.cmap.format4Segments.length / 4 + 1;
ttf.support.cmap.format4Size = 16 + format4SegCount * 8;
ttf.support.cmap.format0Segments = getFormat0SegmentFlat(unicodeArr, idArr);
ttf.support.cmap.hasFormat0 = ttf.support.cmap.format0Segments.length > 0; ttf.support.cmap.hasFormat0 = ttf.support.cmap.format0Segments.length > 0;
ttf.support.cmap.format0Size = ttf.support.cmap.hasFormat0 ? 262 : 0; ttf.support.cmap.format0Size = ttf.support.cmap.hasFormat0 ? 262 : 0;
/* 优化142: 排序后直接检查最大 unicode避免单独遍历 */ /** format 12 subtableplatformID=3, encodingID=10
var hasGLyphsOver2Bytes = glyfUnicodes.length > 0 && glyfUnicodes[glyfUnicodes.length - 1].unicode > 0xFFFF; * 现代浏览器使用 unicode-range 时依赖 format 12 来匹配字符
* 仅当有 cmap 映射数据时才生成避免 nGroups=0 的无效 subtable */
var hasGLyphsOver2Bytes = len > 0;
if (hasGLyphsOver2Bytes) { if (hasGLyphsOver2Bytes) {
ttf.support.cmap.hasGLyphsOver2Bytes = true; ttf.support.cmap.hasGLyphsOver2Bytes = true;
ttf.support.cmap.format12Segments = getSegments(glyfUnicodes); ttf.support.cmap.format12Segments = getSegmentsFlat(unicodeArr, idArr);
ttf.support.cmap.format12Size = 16 + ttf.support.cmap.format12Segments.length * 12; ttf.support.cmap.format12Size = 16 + (ttf.support.cmap.format12Segments.length / 4) * 12;
} }
/** 记录头大小必须动态计算,与 write.js 中的 numRecords 保持一致,否则会导致表偏移错位 */ /** 记录头大小必须动态计算,与 write.js 中的 numRecords 保持一致,否则会导致表偏移错位 */
var numRecords = 2 + (ttf.support.cmap.hasFormat0 ? 1 : 0) + (hasGLyphsOver2Bytes ? 1 : 0); var numRecords = 2 + (ttf.support.cmap.hasFormat0 ? 1 : 0) + (ttf.support.cmap.hasGLyphsOver2Bytes ? 1 : 0);
var recordHeaderSize = 4 + numRecords * 8; var recordHeaderSize = 4 + numRecords * 8;
var size = recordHeaderSize var size = recordHeaderSize
+ ttf.support.cmap.format0Size + ttf.support.cmap.format0Size
+ ttf.support.cmap.format4Size + ttf.support.cmap.format4Size
+ (hasGLyphsOver2Bytes ? ttf.support.cmap.format12Size : 0); + (ttf.support.cmap.hasGLyphsOver2Bytes ? ttf.support.cmap.format12Size : 0);
return size; return size;
} }

View File

@ -10,7 +10,7 @@ exports.default = write;
*/ */
/** /**
* 优化108: 接受并行数组 unicodeArr/idArr * 优化156: 接受扁平数组 [unicode, id, unicode, id, ...]
*/ */
function writeSubTable0(writer, unicodes) { function writeSubTable0(writer, unicodes) {
var pos = writer.offset; var pos = writer.offset;
@ -20,13 +20,14 @@ function writeSubTable0(writer, unicodes) {
view.setUint16(pos, 0, false); pos += 2; view.setUint16(pos, 0, false); pos += 2;
var i = -1; var i = -1;
for (var j = 0; j < unicodes.length; j++) { for (var j = 0; j < unicodes.length; j += 2) {
var unicode = unicodes[j]; var unicode = unicodes[j];
while (++i < unicode[0]) { var glyphId = unicodes[j + 1];
while (++i < unicode) {
view.setUint8(pos++, 0); view.setUint8(pos++, 0);
} }
view.setUint8(pos++, unicode[1]); view.setUint8(pos++, glyphId);
i = unicode[0]; i = unicode;
} }
while (++i < 256) { while (++i < 256) {
view.setUint8(pos++, 0); view.setUint8(pos++, 0);
@ -35,38 +36,42 @@ function writeSubTable0(writer, unicodes) {
return writer; return writer;
} }
/**
* 优化156: 接受扁平数组 [start, end, startId, delta, ...]
*/
function writeSubTable4(writer, segments) { function writeSubTable4(writer, segments) {
var pos = writer.offset; var pos = writer.offset;
var view = writer.view; var view = writer.view;
var segCount = segments.length + 1; var segCount = segments.length / 4 + 1;
var maxExponent = Math.floor(Math.log(segCount) / Math.LN2); var maxExponent = Math.floor(Math.log(segCount) / Math.LN2);
var searchRange = 2 * Math.pow(2, maxExponent); var searchRange = 2 * Math.pow(2, maxExponent);
view.setUint16(pos, 4, false); pos += 2; view.setUint16(pos, 4, false); pos += 2;
view.setUint16(pos, 24 + segments.length * 8, false); pos += 2; view.setUint16(pos, 16 + segCount * 8, false); pos += 2;
view.setUint16(pos, 0, false); pos += 2; view.setUint16(pos, 0, false); pos += 2;
view.setUint16(pos, segCount * 2, false); pos += 2; view.setUint16(pos, segCount * 2, false); pos += 2;
view.setUint16(pos, searchRange, false); pos += 2; view.setUint16(pos, searchRange, false); pos += 2;
view.setUint16(pos, maxExponent, false); pos += 2; view.setUint16(pos, maxExponent, false); pos += 2;
view.setUint16(pos, 2 * segCount - searchRange, false); pos += 2; view.setUint16(pos, 2 * segCount - searchRange, false); pos += 2;
for (var i = 0; i < segments.length; i++) { var numSegs = segments.length / 4;
view.setUint16(pos, segments[i].end, false); pos += 2; for (var i = 0; i < numSegs; i++) {
view.setUint16(pos, segments[i * 4 + 1], false); pos += 2;
} }
view.setUint16(pos, 0xFFFF, false); pos += 2; view.setUint16(pos, 0xFFFF, false); pos += 2;
view.setUint16(pos, 0, false); pos += 2; view.setUint16(pos, 0, false); pos += 2;
for (var j = 0; j < segments.length; j++) { for (var j = 0; j < numSegs; j++) {
view.setUint16(pos, segments[j].start, false); pos += 2; view.setUint16(pos, segments[j * 4], false); pos += 2;
} }
view.setUint16(pos, 0xFFFF, false); pos += 2; view.setUint16(pos, 0xFFFF, false); pos += 2;
for (var k = 0; k < segments.length; k++) { for (var k = 0; k < numSegs; k++) {
view.setUint16(pos, segments[k].delta, false); pos += 2; view.setUint16(pos, segments[k * 4 + 3], false); pos += 2;
} }
view.setUint16(pos, 1, false); pos += 2; view.setUint16(pos, 1, false); pos += 2;
for (var m = 0; m < segments.length; m++) { for (var m = 0; m < numSegs; m++) {
view.setUint16(pos, 0, false); pos += 2; view.setUint16(pos, 0, false); pos += 2;
} }
view.setUint16(pos, 0, false); pos += 2; view.setUint16(pos, 0, false); pos += 2;
@ -75,19 +80,24 @@ function writeSubTable4(writer, segments) {
return writer; return writer;
} }
/**
* 优化156: 接受扁平数组 [start, end, startId, delta, ...]
*/
function writeSubTable12(writer, segments) { function writeSubTable12(writer, segments) {
var pos = writer.offset; var pos = writer.offset;
var view = writer.view; var view = writer.view;
var numSegs = segments.length / 4;
view.setUint16(pos, 12, false); pos += 2; view.setUint16(pos, 12, false); pos += 2;
view.setUint16(pos, 0, false); pos += 2; view.setUint16(pos, 0, false); pos += 2;
view.setUint32(pos, 16 + segments.length * 12, false); pos += 4; view.setUint32(pos, 16 + numSegs * 12, false); pos += 4;
view.setUint32(pos, 0, false); pos += 4; view.setUint32(pos, 0, false); pos += 4;
view.setUint32(pos, segments.length, false); pos += 4; view.setUint32(pos, numSegs, false); pos += 4;
for (var i = 0; i < segments.length; i++) { for (var i = 0; i < numSegs; i++) {
view.setUint32(pos, segments[i].start, false); pos += 4; var off = i * 4;
view.setUint32(pos, segments[i].end, false); pos += 4; view.setUint32(pos, segments[off], false); pos += 4;
view.setUint32(pos, segments[i].startId, false); pos += 4; view.setUint32(pos, segments[off + 1], false); pos += 4;
view.setUint32(pos, segments[off + 2], false); pos += 4;
} }
writer.offset = pos; writer.offset = pos;
return writer; return writer;

View File

@ -22,9 +22,10 @@ function write(writer, ttf) {
var glyfSupport = ttf.support.glyf; var glyfSupport = ttf.support.glyf;
var glyfs = ttf.glyf; var glyfs = ttf.glyf;
var view = writer.view; var view = writer.view;
/** 优化141: 复用 Uint8Array 视图,避免每次 set 创建临时视图 */ /** 优化141+165: 复用 Uint8Array 视图,避免每次 set 创建临时视图 */
var buf = view.buffer; var buf = view.buffer;
var vbo = view.byteOffset; var vbo = view.byteOffset;
var fullView = new Uint8Array(buf, vbo);
var ARG_1_AND_2_ARE_WORDS = _componentFlag.default.ARG_1_AND_2_ARE_WORDS; var ARG_1_AND_2_ARE_WORDS = _componentFlag.default.ARG_1_AND_2_ARE_WORDS;
var ROUND_XY_TO_GRID = _componentFlag.default.ROUND_XY_TO_GRID; var ROUND_XY_TO_GRID = _componentFlag.default.ROUND_XY_TO_GRID;
var WE_HAVE_A_SCALE = _componentFlag.default.WE_HAVE_A_SCALE; var WE_HAVE_A_SCALE = _componentFlag.default.WE_HAVE_A_SCALE;
@ -68,8 +69,9 @@ function write(writer, ttf) {
var b = transform.b; var b = transform.b;
var c = transform.c; var c = transform.c;
var d = transform.d; var d = transform.d;
var e = g.points ? g.points[0] : transform.e; var pts = g.points;
var f = g.points ? g.points[1] : transform.f; var e = pts ? pts[0] : transform.e;
var f = pts ? pts[1] : transform.f;
if (e < 0 || e > 0x7F || f < 0 || f > 0x7F) { if (e < 0 || e > 0x7F || f < 0 || f > 0x7F) {
flags += ARG_1_AND_2_ARE_WORDS; flags += ARG_1_AND_2_ARE_WORDS;
} }
@ -128,7 +130,7 @@ function write(writer, ttf) {
pos += 2; pos += 2;
if (instructions.length > 0) { if (instructions.length > 0) {
var instrArr = instructions instanceof Uint8Array ? instructions : new Uint8Array(instructions); var instrArr = instructions instanceof Uint8Array ? instructions : new Uint8Array(instructions);
new Uint8Array(view.buffer, view.byteOffset + pos, instrArr.length).set(instrArr); fullView.set(instrArr, pos);
} }
pos += instructions.length; pos += instructions.length;
} else { } else {
@ -142,9 +144,9 @@ function write(writer, ttf) {
view.setUint8(pos++, flags[fi]); view.setUint8(pos++, flags[fi]);
} }
/* 优化21+98+119+141: xCoord 预编码 Uint8Array 直接 set使用缓存引用 */ /* 优化21+98+119+165: xCoord 预编码 Uint8Array 直接 set复用全局视图 */
if (gSupport.xEncoded) { if (gSupport.xEncoded) {
new Uint8Array(buf, vbo + pos, gSupport.xEncoded.length).set(gSupport.xEncoded); fullView.set(gSupport.xEncoded, pos);
pos += gSupport.xEncoded.length; pos += gSupport.xEncoded.length;
} else { } else {
var xCoord = gSupport.xCoord || []; var xCoord = gSupport.xCoord || [];
@ -160,9 +162,9 @@ function write(writer, ttf) {
} }
} }
/* 优化21+58+98+119+141: yCoord 预编码 Uint8Array 直接 set使用缓存引用 */ /* 优化21+58+98+119+165: yCoord 预编码 Uint8Array 直接 set复用全局视图 */
if (gSupport.yEncoded) { if (gSupport.yEncoded) {
new Uint8Array(buf, vbo + pos, gSupport.yEncoded.length).set(gSupport.yEncoded); fullView.set(gSupport.yEncoded, pos);
pos += gSupport.yEncoded.length; pos += gSupport.yEncoded.length;
} else { } else {
var yCoord = gSupport.yCoord || []; var yCoord = gSupport.yCoord || [];
@ -179,13 +181,12 @@ function write(writer, ttf) {
} }
} }
/* 优化81: 4字节对齐直接 view 写入,避免临时 TypedArray */ /* 优化81+171: 4字节对齐使用 fill(0) 替代逐字节写入 */
var glyfSize = gSupport.glyfSize; var glyfSize = gSupport.glyfSize;
if (glyfSize % 4) { var pad = glyfSize % 4;
var pad = 4 - glyfSize % 4; if (pad) {
if (pad >= 1) view.setUint8(pos++, 0); fullView.fill(0, pos, pos + (4 - pad));
if (pad >= 2) view.setUint8(pos++, 0); pos += 4 - pad;
if (pad >= 3) view.setUint8(pos++, 0);
} }
writer.offset = pos; writer.offset = pos;

View File

@ -12,6 +12,38 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
* @author mengke01(kekee000@gmail.com) * @author mengke01(kekee000@gmail.com)
*/ */
var _default = exports.default = _table.default.create('head', [['version', _struct.default.Fixed], ['fontRevision', _struct.default.Fixed], ['checkSumAdjustment', _struct.default.Uint32], ['magickNumber', _struct.default.Uint32], ['flags', _struct.default.Uint16], ['unitsPerEm', _struct.default.Uint16], ['created', _struct.default.LongDateTime], ['modified', _struct.default.LongDateTime], ['xMin', _struct.default.Int16], ['yMin', _struct.default.Int16], ['xMax', _struct.default.Int16], ['yMax', _struct.default.Int16], ['macStyle', _struct.default.Uint16], ['lowestRecPPEM', _struct.default.Uint16], ['fontDirectionHint', _struct.default.Int16], ['indexToLocFormat', _struct.default.Int16], ['glyphDataFormat', _struct.default.Int16]], { var _default = exports.default = _table.default.create('head', [['version', _struct.default.Fixed], ['fontRevision', _struct.default.Fixed], ['checkSumAdjustment', _struct.default.Uint32], ['magickNumber', _struct.default.Uint32], ['flags', _struct.default.Uint16], ['unitsPerEm', _struct.default.Uint16], ['created', _struct.default.LongDateTime], ['modified', _struct.default.LongDateTime], ['xMin', _struct.default.Int16], ['yMin', _struct.default.Int16], ['xMax', _struct.default.Int16], ['yMax', _struct.default.Int16], ['macStyle', _struct.default.Uint16], ['lowestRecPPEM', _struct.default.Uint16], ['fontDirectionHint', _struct.default.Int16], ['indexToLocFormat', _struct.default.Int16], ['glyphDataFormat', _struct.default.Int16]], {
/* 优化148: head 表固定 54 字节,跳过 switch 循环 */ size: function () { return 54; },
size: function () { return 54; } /** 优化178: 全部内联 view 写入 54 字节,包括 LongDateTime */
}); write: function (writer, ttf) {
var head = ttf.head;
var pos = writer.offset;
var view = writer.view;
view.setInt32(pos, Math.round(head.version * 65536), false); pos += 4;
view.setInt32(pos, Math.round(head.fontRevision * 65536), false); pos += 4;
view.setUint32(pos, head.checkSumAdjustment, false); pos += 4;
view.setUint32(pos, head.magickNumber, false); pos += 4;
view.setUint16(pos, head.flags, false); pos += 2;
view.setUint16(pos, head.unitsPerEm, false); pos += 2;
/** LongDateTime 内联: 1904-01-01 基准8字节 (高4字节=0, 低4字节=秒数) */
var delta = -2077545600000;
function writeLDT(value, p) {
var ms = typeof value.getTime === 'function' ? value.getTime() : typeof value === 'number' ? value : Date.parse(value);
view.setUint32(p, 0, false);
view.setUint32(p + 4, Math.round((ms - delta) / 1000), false);
return p + 8;
}
pos = writeLDT(head.created, pos);
pos = writeLDT(head.modified, pos);
view.setInt16(pos, head.xMin, false); pos += 2;
view.setInt16(pos, head.yMin, false); pos += 2;
view.setInt16(pos, head.xMax, false); pos += 2;
view.setInt16(pos, head.yMax, false); pos += 2;
view.setUint16(pos, head.macStyle, false); pos += 2;
view.setUint16(pos, head.lowestRecPPEM, false); pos += 2;
view.setInt16(pos, head.fontDirectionHint, false); pos += 2;
view.setInt16(pos, head.indexToLocFormat, false); pos += 2;
view.setInt16(pos, head.glyphDataFormat, false); pos += 2;
writer.offset = pos;
return writer;
}
});

View File

@ -14,6 +14,26 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
* https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6hhea.html * https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6hhea.html
*/ */
var _default = exports.default = _table.default.create('hhea', [['version', _struct.default.Fixed], ['ascent', _struct.default.Int16], ['descent', _struct.default.Int16], ['lineGap', _struct.default.Int16], ['advanceWidthMax', _struct.default.Uint16], ['minLeftSideBearing', _struct.default.Int16], ['minRightSideBearing', _struct.default.Int16], ['xMaxExtent', _struct.default.Int16], ['caretSlopeRise', _struct.default.Int16], ['caretSlopeRun', _struct.default.Int16], ['caretOffset', _struct.default.Int16], ['reserved0', _struct.default.Int16], ['reserved1', _struct.default.Int16], ['reserved2', _struct.default.Int16], ['reserved3', _struct.default.Int16], ['metricDataFormat', _struct.default.Int16], ['numOfLongHorMetrics', _struct.default.Uint16]], { var _default = exports.default = _table.default.create('hhea', [['version', _struct.default.Fixed], ['ascent', _struct.default.Int16], ['descent', _struct.default.Int16], ['lineGap', _struct.default.Int16], ['advanceWidthMax', _struct.default.Uint16], ['minLeftSideBearing', _struct.default.Int16], ['minRightSideBearing', _struct.default.Int16], ['xMaxExtent', _struct.default.Int16], ['caretSlopeRise', _struct.default.Int16], ['caretSlopeRun', _struct.default.Int16], ['caretOffset', _struct.default.Int16], ['reserved0', _struct.default.Int16], ['reserved1', _struct.default.Int16], ['reserved2', _struct.default.Int16], ['reserved3', _struct.default.Int16], ['metricDataFormat', _struct.default.Int16], ['numOfLongHorMetrics', _struct.default.Uint16]], {
/* 优化148: hhea 表固定 36 字节,跳过 switch 循环 */ size: function () { return 36; },
size: function () { return 36; } write: function (writer, ttf) {
}); var h = ttf.hhea;
var pos = writer.offset;
var view = writer.view;
view.setInt32(pos, Math.round(h.version * 65536), false); pos += 4;
view.setInt16(pos, h.ascent, false); pos += 2;
view.setInt16(pos, h.descent, false); pos += 2;
view.setInt16(pos, h.lineGap, false); pos += 2;
view.setUint16(pos, h.advanceWidthMax, false); pos += 2;
view.setInt16(pos, h.minLeftSideBearing, false); pos += 2;
view.setInt16(pos, h.minRightSideBearing, false); pos += 2;
view.setInt16(pos, h.xMaxExtent, false); pos += 2;
view.setInt16(pos, h.caretSlopeRise, false); pos += 2;
view.setInt16(pos, h.caretSlopeRun, false); pos += 2;
view.setInt16(pos, h.caretOffset, false); pos += 2;
pos += 8; /* reserved0-3 */
view.setInt16(pos, h.metricDataFormat, false); pos += 2;
view.setUint16(pos, h.numOfLongHorMetrics, false); pos += 2;
writer.offset = pos;
return writer;
}
});

View File

@ -40,36 +40,39 @@ var _default = exports.default = _table.default.create('hmtx', [], {
return hMetrics; return hMetrics;
}, },
write: function write(writer, ttf) { write: function write(writer, ttf) {
var i;
var numOfLongHorMetrics = ttf.hhea.numOfLongHorMetrics; var numOfLongHorMetrics = ttf.hhea.numOfLongHorMetrics;
/* 优化30+82: 直接 view 批量写入 */ /* 优化30+82+171: 缓存 glyfs[i] 到循环变量 */
var wView = writer.view; var wView = writer.view;
var pos = writer.offset; var pos = writer.offset;
var glyfs = ttf.glyf; var glyfs = ttf.glyf;
for (i = 0; i < numOfLongHorMetrics; i++) { for (var i = 0; i < numOfLongHorMetrics; i++) {
wView.setUint16(pos, glyfs[i].advanceWidth, false); var g = glyfs[i];
wView.setInt16(pos + 2, glyfs[i].leftSideBearing, false); wView.setUint16(pos, g.advanceWidth, false);
wView.setInt16(pos + 2, g.leftSideBearing, false);
pos += 4; pos += 4;
} }
var numOfLast = glyfs.length - numOfLongHorMetrics; var numOfLast = glyfs.length - numOfLongHorMetrics;
for (i = 0; i < numOfLast; i++) { for (var j = 0; j < numOfLast; j++) {
wView.setInt16(pos, glyfs[numOfLongHorMetrics + i].leftSideBearing, false); wView.setInt16(pos, glyfs[numOfLongHorMetrics + j].leftSideBearing, false);
pos += 2; pos += 2;
} }
writer.offset = pos; writer.offset = pos;
return writer; return writer;
}, },
size: function size(ttf) { size: function size(ttf) {
/* 优化171: 缓存 ttf.glyf 到局部变量,消除循环内属性链查找 */
var glyfs = ttf.glyf;
var gl = glyfs.length;
var numOfLast = 0; var numOfLast = 0;
var advanceWidth = ttf.glyf[ttf.glyf.length - 1].advanceWidth; var advanceWidth = glyfs[gl - 1].advanceWidth;
for (var i = ttf.glyf.length - 2; i >= 0; i--) { for (var i = gl - 2; i >= 0; i--) {
if (advanceWidth === ttf.glyf[i].advanceWidth) { if (advanceWidth === glyfs[i].advanceWidth) {
numOfLast++; numOfLast++;
} else { } else {
break; break;
} }
} }
ttf.hhea.numOfLongHorMetrics = ttf.glyf.length - numOfLast; ttf.hhea.numOfLongHorMetrics = gl - numOfLast;
return 4 * ttf.hhea.numOfLongHorMetrics + 2 * numOfLast; return 4 * ttf.hhea.numOfLongHorMetrics + 2 * numOfLast;
} }
}); });

View File

@ -44,22 +44,23 @@ var _default = exports.default = _table.default.create('loca', [], {
var wView = writer.view; var wView = writer.view;
var pos = writer.offset; var pos = writer.offset;
if (indexToLocFormat) { if (indexToLocFormat) {
for (var i = 0; i <= numGlyphs; i++) { /* 优化171: 拆分循环消除 i < numGlyphs 条件判断 */
for (var i = 0; i < numGlyphs; i++) {
wView.setUint32(pos, offset, false); wView.setUint32(pos, offset, false);
pos += 4; pos += 4;
if (i < numGlyphs) { offset += glyfSupport[i].size;
offset += glyfSupport[i].size;
}
} }
wView.setUint32(pos, offset, false);
pos += 4;
} else { } else {
/* 优化110: 短格式使用右移替代浮点乘 0.5 */ /* 优化110+171: 短格式右移 + 拆分循环 */
for (var j = 0; j <= numGlyphs; j++) { for (var j = 0; j < numGlyphs; j++) {
wView.setUint16(pos, offset >> 1, false); wView.setUint16(pos, offset >> 1, false);
pos += 2; pos += 2;
if (j < numGlyphs) { offset += glyfSupport[j].size;
offset += glyfSupport[j].size;
}
} }
wView.setUint16(pos, offset >> 1, false);
pos += 2;
} }
writer.offset = pos; writer.offset = pos;
return writer; return writer;

View File

@ -125,9 +125,10 @@ var TTFWriter = exports.default = /*#__PURE__*/function () {
new _directory.default().write(writer, ttf); new _directory.default().write(writer, ttf);
/* 优化56+87: forEach → for 循环,缓存 buffer 引用避免重复 getBuffer() */ /* 优化56+87+179: forEach → for 循环,缓存 buffer 引用,累加各表校验和避免全局重算 */
var supportTableList = ttf.support.tables; var supportTableList = ttf.support.tables;
var buf = writer.getBuffer(); var buf = writer.getBuffer();
var wholeCheckSum = 0;
for (var si = 0, sl = supportTableList.length; si < sl; si++) { for (var si = 0, sl = supportTableList.length; si < sl; si++) {
var table = supportTableList[si]; var table = supportTableList[si];
var tableStart = writer.offset; var tableStart = writer.offset;
@ -140,6 +141,7 @@ var TTFWriter = exports.default = /*#__PURE__*/function () {
writer.writeEmpty(4 - table.length % 4); writer.writeEmpty(4 - table.length % 4);
} }
table.checkSum = (0, _checkSum.default)(buf, tableStart, table.size); table.checkSum = (0, _checkSum.default)(buf, tableStart, table.size);
wholeCheckSum = (wholeCheckSum + table.checkSum) >>> 0;
} }
/* 优化111: 重新写入校验和,直接 view 写入 */ /* 优化111: 重新写入校验和,直接 view 写入 */
@ -149,8 +151,8 @@ var TTFWriter = exports.default = /*#__PURE__*/function () {
csView.setUint32(offset2, supportTableList[ci].checkSum, false); csView.setUint32(offset2, supportTableList[ci].checkSum, false);
} }
/* 写入总校验和 */ /* 优化179: 用累加的各表校验和替代全局 checkSum避免重遍历整个 buffer */
var ttfCheckSum = (0xB1B0AFBA - (0, _checkSum.default)(buf) + 0x100000000) % 0x100000000; var ttfCheckSum = (0xB1B0AFBA - wholeCheckSum + 0x100000000) % 0x100000000;
csView.setUint32(ttfHeadOffset + 8, ttfCheckSum, false); csView.setUint32(ttfHeadOffset + 8, ttfCheckSum, false);
delete ttf.writeOptions; delete ttf.writeOptions;
delete ttf.support; delete ttf.support;

View File

@ -10,8 +10,7 @@ exports.default = checkSum;
*/ */
/** /**
* 优化107: 使用 Uint32Array 视图 + DataView 字节序转换处理大端序 * 优化178: 使用 Uint8Array 直接读取并手动拼接大端序 uint32消除 DataView 开销
* 避免每次调用创建新的 DataView减少内存分配
*/ */
function checkSumArrayBuffer(buffer, offset, length) { function checkSumArrayBuffer(buffer, offset, length) {
if (offset === undefined) offset = 0; if (offset === undefined) offset = 0;
@ -19,44 +18,26 @@ function checkSumArrayBuffer(buffer, offset, length) {
if (offset + length > buffer.byteLength) { if (offset + length > buffer.byteLength) {
throw new Error('check sum out of bound'); throw new Error('check sum out of bound');
} }
/* 优化107: 复用共享 DataView 进行字节序转换 */ var bytes = new Uint8Array(buffer, offset, length);
var view = DataViewPool.acquire(buffer);
var nLongs = length >> 2; var nLongs = length >> 2;
var sum = 0; var sum = 0;
for (var i = 0; i < nLongs; i++) { var i = 0;
sum = (sum + view.getUint32(offset + (i << 2), false)) | 0; while (i < nLongs) {
sum = (sum + (bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]) >>> 0) | 0;
i += 4;
} }
DataViewPool.release(view);
var leftBytes = length - nLongs * 4; var leftBytes = length - nLongs * 4;
if (leftBytes) { if (leftBytes) {
var bytes = new Uint8Array(buffer, offset + nLongs * 4, leftBytes); var off = nLongs << 2;
var val = 0; var val = 0;
for (var k = 0; k < leftBytes; k++) { for (var k = 0; k < leftBytes; k++) {
val = (val | bytes[k] << (leftBytes - 1 - k) * 8) >>> 0; val = (val | bytes[off + k] << (leftBytes - 1 - k) * 8) >>> 0;
} }
sum = (sum + val) | 0; sum = (sum + val) | 0;
} }
return sum >>> 0; return sum >>> 0;
} }
/**
* 优化107: DataView 对象池避免重复创建
*/
var DataViewPool = {
_view: null,
_buffer: null,
acquire: function (buffer) {
if (this._buffer !== buffer) {
this._view = new DataView(buffer);
this._buffer = buffer;
}
return this._view;
},
release: function () {
/* 保留引用供下次复用 */
}
};
function checkSumArray(buffer, offset, length) { function checkSumArray(buffer, offset, length) {
if (offset === undefined) offset = 0; if (offset === undefined) offset = 0;
length = length || buffer.length; length = length || buffer.length;

View File

@ -142,12 +142,14 @@ function ceilReduceAndSizeFromTypedArrays(glyf, sharedXBuf, sharedYBuf) {
*/ */
function ceilReduceAndSizeFlat(glyf, sharedXBuf, sharedYBuf) { function ceilReduceAndSizeFlat(glyf, sharedXBuf, sharedYBuf) {
var contours = glyf.contours; var contours = glyf.contours;
/* 优化91: 跳过 reducePathFlat */ /* 优化91+164: 跳过 reducePathFlat用 write-index 替代 splice */
for (var j = contours.length - 1; j >= 0; j--) { var writeIdx = 0;
if (contours[j].length <= 6) { for (var j = 0, cl = contours.length; j < cl; j++) {
contours.splice(j, 1); if (contours[j].length > 6) {
contours[writeIdx++] = contours[j];
} }
} }
contours.length = writeIdx;
if (0 === contours.length) { if (0 === contours.length) {
delete glyf.contours; delete glyf.contours;
return { sharedXBuf: sharedXBuf, sharedYBuf: sharedYBuf }; return { sharedXBuf: sharedXBuf, sharedYBuf: sharedYBuf };
@ -330,6 +332,26 @@ function optimizettf(ttf) {
if (glyf._flatContours) { if (glyf._flatContours) {
var flatResult = ceilReduceAndSizeFlat(glyf, sharedXBuf, sharedYBuf); var flatResult = ceilReduceAndSizeFlat(glyf, sharedXBuf, sharedYBuf);
if (flatResult) { sharedXBuf = flatResult.sharedXBuf; sharedYBuf = flatResult.sharedYBuf; } if (flatResult) { sharedXBuf = flatResult.sharedXBuf; sharedYBuf = flatResult.sharedYBuf; }
/**
* 关键必须收集 maxPoints/maxContours否则 maxp 表中这两个值为 0
* 浏览器会据此跳过渲染表现为字体加载成功但文字显示为空白/fallback
* 这是 OTFTTF 转换字形的必经路径_flatContours parseCFFGlyph 生成
* 之前已因为同样的问题修复过对象 contours 路径commit 97f4d72
* 所有涉及 contours 的分支都必须更新这两个值
* 注意ceilReduceAndSizeFlat 可能删除 glyf.contours当所有 contour 长度 6
* 所以必须在调用之后检查 glyf.contours 是否仍存在
*/
if (glyf.contours) {
var flatNumC = glyf.contours.length;
if (flatNumC > 0) {
if (flatNumC > m_maxContours) m_maxContours = flatNumC;
var flatTotalPts = 0;
for (var fci = 0; fci < flatNumC; fci++) {
flatTotalPts += glyf.contours[fci].length / 3;
}
if (flatTotalPts > m_maxPoints) m_maxPoints = flatTotalPts;
}
}
} else { } else {
/* 对象 contours 格式也需要收集 maxPoints/maxContours */ /* 对象 contours 格式也需要收集 maxPoints/maxContours */
var numC = glyf.contours.length; var numC = glyf.contours.length;

View File

@ -4,127 +4,147 @@ Object.defineProperty(exports, "__esModule", {
value: true value: true
}); });
exports.default = otfContours2ttfContours; exports.default = otfContours2ttfContours;
var _bezierCubic2Q = _interopRequireDefault(require("../../math/bezierCubic2Q2")); var _bezierCubic2Q = require("../../math/bezierCubic2Q2");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/** /**
* @file otf轮廓转ttf轮廓 * @file otf轮廓转ttf轮廓
* @author mengke01(kekee000@gmail.com) * @author mengke01(kekee000@gmail.com)
* *
* CFF Type 2 charstring 解析后的 contour 格式 * CFF Type 2 charstring 解析后的 contour 扁平格式
* - onCurve {x, y, onCurve: true}是曲线端点或线段端点 * - [x, y, flag, x, y, flag, ...] flag=1 onCurve, flag=0 offCurve
* - offCurve {x, y} onCurve 属性 cubic bezier 控制点
* - 每个 cubic bezier 段由 2 offCurve + 1 onCurve 组成 * - 每个 cubic bezier 段由 2 offCurve + 1 onCurve 组成
* - 连续的 offCurve 点之间隐含端点为两者的中点 * - 连续的 offCurve 点之间隐含端点为两者的中点
*
* 优化178: 全流程扁平数组消除对象分配
*/ */
/** /** 优化178: 直接消费扁平数组 normalizeContour返回扁平数组 */
* CFF contour 转换为标准 [onCurve, offCurve, offCurve, onCurve, ...] 序列 function normalizeContourFlat(arr) {
* 处理隐含端点和连续 offCurve 点的情况 var len = arr.length;
* 优化构建新数组替代 unshift/splice避免 O(n^2) if (len < 6) return arr;
*/
function normalizeContour(otfContour) {
var len = otfContour.length;
if (len < 2) return otfContour;
/** 确保 onCurve 标志规范化 */ /** 检查第一个点是否 onCurve */
for (var i = 0; i < len; i++) { var firstOnCurve = !!(arr[2]);
otfContour[i].onCurve = !!otfContour[i].onCurve; var prependX, prependY;
}
/** 如果第一个点不是 onCurve需要回绕处理 */ if (!firstOnCurve) {
if (!otfContour[0].onCurve) { var lastIdx = len - 3;
var last = otfContour[len - 1]; var lastOnCurve = !!(arr[lastIdx + 2]);
if (last.onCurve) { if (lastOnCurve) {
otfContour.unshift({ x: last.x, y: last.y, onCurve: true }); prependX = arr[lastIdx];
prependY = arr[lastIdx + 1];
} else { } else {
otfContour.unshift({ prependX = (arr[0] + arr[lastIdx]) * 0.5;
x: (otfContour[0].x + last.x) * 0.5, prependY = (arr[1] + arr[lastIdx + 1]) * 0.5;
y: (otfContour[0].y + last.y) * 0.5,
onCurve: true
});
} }
len = otfContour.length;
} }
/** offCurve /** 检查是否有连续 offCurve 点 */
* 优化构建新数组替代 splice O(n^2) 降为 O(n) */ var hasConsecutiveOff = false;
var hasConsecutive = false; if (!firstOnCurve) hasConsecutiveOff = true;
for (var i = 0; i < len - 1; i++) { else {
if (!otfContour[i].onCurve && !otfContour[i + 1].onCurve) { for (var j = 0; j < len - 3; j += 3) {
hasConsecutive = true; if (!arr[j + 2] && j + 5 < len && !arr[j + 5]) {
break; hasConsecutiveOff = true;
} break;
}
if (hasConsecutive) {
var result = [];
for (var i = 0; i < len; i++) {
result.push(otfContour[i]);
if (!otfContour[i].onCurve && i + 1 < len && !otfContour[i + 1].onCurve) {
var next = otfContour[i + 1];
result.push({
x: (otfContour[i].x + next.x) * 0.5,
y: (otfContour[i].y + next.y) * 0.5,
onCurve: true
});
} }
} }
return result; /** 也检查最后一个和第一个 */
if (!hasConsecutiveOff && len >= 6 && !arr[len - 1] && !arr[2]) {
hasConsecutiveOff = true;
}
} }
return otfContour; if (!hasConsecutiveOff && firstOnCurve) return arr;
/** 构建结果:[x, y, flag, ...] */
var result = [];
if (prependX != null) {
result.push(prependX, prependY, 1);
}
for (var k = 0; k < len; k += 3) {
result.push(arr[k], arr[k + 1], arr[k + 2]);
if (!arr[k + 2] && k + 5 < len && !arr[k + 5]) {
var mx = (arr[k] + arr[k + 3]) * 0.5;
var my = (arr[k + 1] + arr[k + 4]) * 0.5;
result.push(mx, my, 1);
}
}
/** 检查 wrap-around 连续 offCurve */
if (prependX == null && len >= 6 && !arr[len - 1] && !arr[2]) {
var mx2 = (arr[len - 3] + arr[0]) * 0.5;
var my2 = (arr[len - 2] + arr[1]) * 0.5;
result.push(mx2, my2, 1);
}
return result;
} }
/** /**
* 转换已标准化的轮廓onCurve/offCurve 严格交替 * 转换已标准化的轮廓全扁平数组操作
* 模式onCurve, offCurve, offCurve, onCurve, offCurve, offCurve, ... * 优化178: 输入和输出都是扁平数组 [x, y, flag, ...]
* 优化153: 在构建 contour 时同时做 Math.round消除 pathCeil 的二次遍历
*/ */
function transformContour(otfContour) { function transformContourFlat(arr) {
var normalized = normalizeContour(otfContour); var normalized = normalizeContourFlat(arr);
if (normalized.length < 2) return []; if (normalized.length < 6) return [];
var contour = []; var contour = [];
var p0 = normalized[0]; /** 第一个点一定是 onCurve */
contour.push({ x: Math.round(p0.x), y: Math.round(p0.y), onCurve: true }); var r = Math.round;
contour.push(r(normalized[0]), r(normalized[1]), 1);
var i = 1; var i = 3;
while (i < normalized.length) { var nLen = normalized.length;
var cur = normalized[i]; var lastX = r(normalized[0]);
if (cur.onCurve) { var lastY = r(normalized[1]);
while (i < nLen) {
var isOnCurve = normalized[i + 2];
if (isOnCurve) {
/** 线段:直接添加 onCurve 端点 */ /** 线段:直接添加 onCurve 端点 */
contour.push({ x: Math.round(cur.x), y: Math.round(cur.y), onCurve: true }); var px = r(normalized[i]);
i++; var py = r(normalized[i + 1]);
contour.push(px, py, 1);
lastX = px;
lastY = py;
i += 3;
} else { } else {
/** cubic bezieroffCurve, offCurve, onCurve */ /** offCurve 点 */
var c1 = cur; var c1x = normalized[i], c1y = normalized[i + 1];
var c2 = i + 1 < normalized.length ? normalized[i + 1] : null; var nextIdx = i + 3;
var end; if (nextIdx < nLen && !normalized[nextIdx + 2]) {
/** 第二个点也是 offCurve → 三次贝塞尔曲线 */
var c2x = normalized[nextIdx], c2y = normalized[nextIdx + 1];
var endIdx = nextIdx + 3;
var endX, endY;
if (endIdx < nLen) {
endX = normalized[endIdx]; endY = normalized[endIdx + 1];
} else {
endX = normalized[0]; endY = normalized[1];
}
i = endIdx + 3;
if (c2 && !c2.onCurve) { /** 三次→二次贝塞尔转换 */
/** 标准 cubic bezier2个控制点 + 1个端点 */ var bezierFlat = (0, _bezierCubic2Q.bezierCubic2Q2Raw)(lastX, lastY, c1x, c1y, c2x, c2y, endX, endY);
end = i + 2 < normalized.length ? normalized[i + 2] : normalized[0]; for (var bi = 0, bl = bezierFlat.length; bi < bl; bi += 4) {
i += 3; contour.push(r(bezierFlat[bi]), r(bezierFlat[bi + 1]), 0);
} else if (c2 && c2.onCurve) { contour.push(r(bezierFlat[bi + 2]), r(bezierFlat[bi + 3]), 1);
/** 退化 cubic bezier只有1个控制点端点就是 c2 */ }
end = c2; lastX = r(endX);
i += 2; lastY = r(endY);
} else { } else {
/** 只有一个 offCurve 点,回绕到起点 */ /** 单个 offCurve → 二次贝塞尔曲线TTF 原生支持) */
end = normalized[0]; var endX2, endY2;
i++; if (nextIdx < nLen && normalized[nextIdx + 2]) {
} endX2 = normalized[nextIdx]; endY2 = normalized[nextIdx + 1];
i = nextIdx + 3;
var bezierArray = (0, _bezierCubic2Q.default)(contour[contour.length - 1], c1, c2 || c1, end); } else {
for (var bi = 0, bl = bezierArray.length; bi < bl; bi += 2) { endX2 = normalized[0]; endY2 = normalized[1];
var ctrl = bezierArray[bi]; i = nextIdx + 3;
var ep = bezierArray[bi + 1]; }
ep.x = Math.round(ep.x); contour.push(r(c1x), r(c1y), 0);
ep.y = Math.round(ep.y); contour.push(r(endX2), r(endY2), 1);
ep.onCurve = true; lastX = r(endX2);
ctrl.x = Math.round(ctrl.x); lastY = r(endY2);
ctrl.y = Math.round(ctrl.y);
contour.push(ctrl);
contour.push(ep);
} }
} }
} }
@ -134,9 +154,7 @@ function transformContour(otfContour) {
/** /**
* otf轮廓转ttf轮廓同时计算包围盒 * otf轮廓转ttf轮廓同时计算包围盒
* * 优化178: 支持扁平数组输入 [x, y, flag, ...]直接构建扁平数组输出
* @param {Array} otfContours otf轮廓数组
* @return {{contours: Array, xMin: number, yMin: number, xMax: number, yMax: number}} 转换结果和包围盒
*/ */
function otfContours2ttfContours(otfContours) { function otfContours2ttfContours(otfContours) {
if (!otfContours || !otfContours.length) { if (!otfContours || !otfContours.length) {
@ -146,21 +164,39 @@ function otfContours2ttfContours(otfContours) {
var left, right, top, bottom; var left, right, top, bottom;
var found = false; var found = false;
for (var i = 0, l = otfContours.length; i < l; i++) { for (var i = 0, l = otfContours.length; i < l; i++) {
if (otfContours[i][0]) { var otfContour = otfContours[i];
var contour = transformContour(otfContours[i]); if (!otfContour || otfContour.length < 6) continue;
contours.push(contour);
/** 同时计算包围盒,避免 otf2ttfobject 二次遍历 */ /** 检测输入格式:扁平数组 vs 对象数组 */
for (var ci = 0, cl = contour.length; ci < cl; ci++) { var isFlat = otfContour._flatContours || (typeof otfContour[0] === 'number' && typeof otfContour[1] === 'number');
var p = contour[ci]; var contour;
if (isFlat) {
contour = transformContourFlat(otfContour);
} else {
contour = transformContourObj(otfContour);
}
if (contour.length < 3) continue;
contours.push(contour);
/** 计算包围盒 */
if (typeof contour[0] === 'number') {
for (var ci = 0, cl = contour.length; ci < cl; ci += 3) {
var px = contour[ci], py = contour[ci + 1];
if (!found) { if (!found) {
left = right = p.x; left = right = px; top = bottom = py; found = true;
top = bottom = p.y;
found = true;
} else { } else {
if (p.x < left) left = p.x; if (px < left) left = px; else if (px > right) right = px;
else if (p.x > right) right = p.x; if (py < top) top = py; else if (py > bottom) bottom = py;
if (p.y < top) top = p.y; }
else if (p.y > bottom) bottom = p.y; }
} else {
for (var ci2 = 0, cl2 = contour.length; ci2 < cl2; ci2++) {
var p = contour[ci2];
if (!found) {
left = right = p.x; top = bottom = p.y; found = true;
} else {
if (p.x < left) left = p.x; else if (p.x > right) right = p.x;
if (p.y < top) top = p.y; else if (p.y > bottom) bottom = p.y;
} }
} }
} }
@ -173,3 +209,47 @@ function otfContours2ttfContours(otfContours) {
yMax: bottom yMax: bottom
}; };
} }
/**
* 兼容旧对象数组格式 [onCurve, offCurve, ...]
*/
function transformContourObj(otfContour) {
if (otfContour.length < 2) return [];
var contour = [];
var p0 = otfContour[0];
contour.push({ x: p0.x + 0.5 | 0, y: p0.y + 0.5 | 0, onCurve: true });
var i = 1;
var nLen = otfContour.length;
while (i < nLen) {
var cur = otfContour[i];
if (cur.onCurve) {
contour.push({ x: cur.x + 0.5 | 0, y: cur.y + 0.5 | 0, onCurve: true });
i++;
} else {
var c1 = cur;
var c2 = i + 1 < nLen ? otfContour[i + 1] : null;
var end;
if (c2 && !c2.onCurve) {
end = i + 2 < nLen ? otfContour[i + 2] : otfContour[0];
i += 3;
} else if (c2 && c2.onCurve) {
end = c2;
i += 2;
} else {
end = otfContour[0];
i++;
}
var bezierFlat = (0, _bezierCubic2Q.default)(contour[contour.length - 1], c1, c2 || c1, end);
for (var bi = 0, bl = bezierFlat.length; bi < bl; bi += 4) {
contour.push({ x: bezierFlat[bi] + 0.5 | 0, y: bezierFlat[bi + 1] + 0.5 | 0 });
contour.push({ x: bezierFlat[bi + 2] + 0.5 | 0, y: bezierFlat[bi + 3] + 0.5 | 0, onCurve: true });
}
}
}
return contour;
}

View File

@ -40,7 +40,7 @@ function lookupFormat4(format4, unicode) {
var endCode = format4.endCode; var endCode = format4.endCode;
var idDelta = format4.idDelta; var idDelta = format4.idDelta;
var idRangeOffset = format4.idRangeOffset; var idRangeOffset = format4.idRangeOffset;
var segCount = format4.segCountX2 / 2; var segCount = format4.segCount || (format4.segCountX2 / 2);
var lo = 0, hi = segCount - 1; var lo = 0, hi = segCount - 1;
while (lo <= hi) { while (lo <= hi) {
@ -54,7 +54,7 @@ function lookupFormat4(format4, unicode) {
if (idRangeOffset[i] === 0) { if (idRangeOffset[i] === 0) {
return (unicode + idDelta[i]) % 0x10000; return (unicode + idDelta[i]) % 0x10000;
} }
var graphIdArrayIndexOffset = (format4.glyphIdArrayOffset - format4.idRangeOffsetOffset) / 2; var graphIdArrayIndexOffset = format4.glyphIdArrayIndexOffset != null ? format4.glyphIdArrayIndexOffset : (format4.glyphIdArrayOffset - format4.idRangeOffsetOffset) / 2;
var index = i + idRangeOffset[i] / 2 + (unicode - startCode[i]) - graphIdArrayIndexOffset; var index = i + idRangeOffset[i] / 2 + (unicode - startCode[i]) - graphIdArrayIndexOffset;
var graphId; var graphId;
if (format4.glyphIdArray) { if (format4.glyphIdArray) {

View File

@ -21,6 +21,10 @@ if (typeof ArrayBuffer === 'undefined' || typeof DataView === 'undefined') {
throw new Error('not support ArrayBuffer and DataView'); throw new Error('not support ArrayBuffer and DataView');
} }
/** 优化178: 全局 Uint8Array 视图缓存,避免 writeBytes/writeEmpty 每次创建视图 */
var _globalView = null;
var _globalViewBuf = null;
// 数据类型 // 数据类型
var dataType = { var dataType = {
Int8: 1, Int8: 1,
@ -111,15 +115,16 @@ var Writer = /*#__PURE__*/function () {
if (length < 0 || offset + length > this.length) { if (length < 0 || offset + length > this.length) {
_error.default.raise(10002, this.length, offset + length); _error.default.raise(10002, this.length, offset + length);
} }
/* 优化151: 缓存 buffer 引用,避免每次创建 Uint8Array 视图 */ /* 优化178: 复用全局 Uint8Array 视图,避免每次 writeBytes 创建新视图 */
var vBuf = this.view.buffer; if (_globalViewBuf !== this.view.buffer) {
_globalViewBuf = this.view.buffer;
_globalView = new Uint8Array(_globalViewBuf);
}
var vOff = this.view.byteOffset + offset; var vOff = this.view.byteOffset + offset;
if (value instanceof ArrayBuffer) { if (value instanceof Uint8Array) {
new Uint8Array(vBuf, vOff, length).set(new Uint8Array(value, 0, length)); _globalView.set(value, vOff);
} else if (value instanceof Uint8Array) {
new Uint8Array(vBuf, vOff, length).set(value);
} else { } else {
new Uint8Array(vBuf, vOff, length).set(new Uint8Array(value)); _globalView.set(value instanceof ArrayBuffer ? new Uint8Array(value, 0, length) : new Uint8Array(value), vOff);
} }
this.offset = offset + length; this.offset = offset + length;
return this; return this;
@ -141,8 +146,12 @@ var Writer = /*#__PURE__*/function () {
if (undefined === offset) { if (undefined === offset) {
offset = this.offset; offset = this.offset;
} }
/* 优化5: fill(0) 批量填充,替代逐字节循环 */ /* 优化178: 复用全局视图 fill(0) */
new Uint8Array(this.view.buffer, this.view.byteOffset + offset, length).fill(0); if (_globalViewBuf !== this.view.buffer) {
_globalViewBuf = this.view.buffer;
_globalView = new Uint8Array(_globalViewBuf);
}
_globalView.fill(0, this.view.byteOffset + offset, this.view.byteOffset + offset + length);
this.offset = offset + length; this.offset = offset + length;
return this; return this;
} }

View File

@ -1,268 +1,327 @@
/** /**
* *
* 运行: pnpm tsx .test.ts * 运行: pnpx tsx .test.ts
*
* :
* 1. backend/font_util/font.ts fontSubset API
* 2. HTTP HTML
* 3. @font-face + HTTP URL
* 4. @font-face + HTTP URL
* 5. canvas SSIM
* *
* : * :
* 1. Font.create optimize sort write * 1. fontSubset
* 2. vs SSIM * 2. vs SSIM
* 3. benchmark_results/ * 3. benchmark_results/screenshots/
*/ */
import { readFile, writeFile, mkdir } from "node:fs/promises"; import { readFile, writeFile, mkdir } from "node:fs/promises";
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
import { performance } from "node:perf_hooks"; import { performance } from "node:perf_hooks";
import { Font } from "./vendor/fonteditor-core/lib/ttf/font.js"; import puppeteer, { type Page } from "puppeteer";
import woff2Module from "./vendor/fonteditor-core/woff2/index.js"; import { fontSubset } from "./backend/font_util/font.js";
import { Canvas, FontLibrary } from "skia-canvas";
const FONT_PATH = "font/令东齐伋复刻体.ttf";
const FONT_NAME = "令东齐伋复刻体";
const BENCHMARK_DIR = "benchmark_results"; const BENCHMARK_DIR = "benchmark_results";
const raw = await readFile(FONT_PATH);
const fontBuffer = new Uint8Array(raw).buffer;
FontLibrary.use(FONT_NAME, FONT_PATH);
/** 初始化 woff2 wasm 并测量耗时 */
const wasmInitStart = performance.now();
await woff2Module.init();
const wasmInitTime = performance.now() - wasmInitStart;
console.log(` woff2 wasm 初始化: ${wasmInitTime.toFixed(1)}ms`);
const testCases = [
{ label: "8个汉字", text: "天地玄黄宇宙洪荒" },
{ label: "拉丁+数字", text: "Hello World 123" },
{
label: "千字文前段",
text: "天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈剑号巨阙珠称夜光果珍李柰菜重芥姜海咸河淡鳞潜羽翔",
},
];
const ROUNDS = 10; const ROUNDS = 10;
/** 子集字体临时文件计数器 */ // ======== HTTP 服务器 ========
let subsetFontCounter = 0; const fontStore = new Map<string, Buffer>();
/** 渲染文字到像素数据 */ function createFontServer(): Promise<{ server: Server; port: number }> {
function renderText(fontFamily: string, text: string, fontSize: number): Uint8Array { return new Promise((resolve) => {
const charWidth = Math.ceil(fontSize * 1.5); const server = createServer((req: IncomingMessage, res: ServerResponse) => {
const width = text.length * charWidth + 20; const url = new URL(req.url!, `http://localhost`);
const height = Math.ceil(fontSize * 1.5); const path = url.pathname;
const canvas = new Canvas(width, height);
const ctx = canvas.getContext("2d"); if (path === "/render") {
ctx.fillStyle = "white"; const params = url.searchParams;
ctx.fillRect(0, 0, width, height); const fontFamily = params.get("font") || "TestFont";
ctx.font = `${fontSize}px ${fontFamily}`; const fontFormat = params.get("format") || "truetype";
ctx.fillStyle = "black"; const text = params.get("text") || "";
ctx.fillText(text, 10, Math.ceil(fontSize * 1.2)); const fontSize = parseInt(params.get("size") || "48", 10);
const imgData = ctx.getImageData(0, 0, width, height); const width = parseInt(params.get("width") || "800", 10);
return new Uint8Array(imgData.data.buffer); const height = parseInt(params.get("height") || "80", 10);
const html = `<!DOCTYPE html>
<html><head>
<style>
@font-face { font-family: "${fontFamily}"; src: url("/fonts/${fontFamily}") format("${fontFormat}"); }
body { margin: 0; background: white; }
#text { font-family: "${fontFamily}", sans-serif; font-size: ${fontSize}px; line-height: 1.2; color: black; padding: ${Math.ceil(fontSize * 0.1)}px 10px; display: inline-block; }
</style></head><body>
<div id="text">${text.replace(/</g, "&lt;")}</div>
<script>
(async () => {
try {
const fontFace = await document.fonts.load('${fontSize}px "${fontFamily}"');
const loaded = document.fonts.check('${fontSize}px "${fontFamily}"');
if (!loaded) { document.title = 'error=font not loaded'; return; }
await new Promise(r => setTimeout(r, 500));
const stillLoaded = document.fonts.check('${fontSize}px "${fontFamily}"');
if (!stillLoaded) { document.title = 'error=font lost after wait'; return; }
document.title = 'ready';
} catch(e) { document.title = 'error=' + e.message; }
})();
</script>
</body></html>`;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(html);
return;
}
if (path.startsWith("/fonts/")) {
const fontKey = decodeURIComponent(path.slice("/fonts/".length));
let buf = fontStore.get(fontKey);
if (!buf) buf = fontStore.get(fontKey + ".ttf");
if (!buf) buf = fontStore.get(fontKey + ".otf");
if (!buf) buf = fontStore.get(fontKey + ".woff2");
if (buf) {
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);
return;
}
}
res.writeHead(404);
res.end("not found");
});
server.listen(0, "127.0.0.1", () => {
const addr = server.address() as { port: number };
resolve({ server, port: addr.port });
});
});
} }
/** 渲染文字并保存为 PNG */ // ======== 无头浏览器渲染 ========
async function renderTextToPng(fontFamily: string, text: string, fontSize: number, filePath: string) {
async function renderTextViaBrowser(
page: Page,
baseUrl: string,
fontFamily: string,
text: string,
fontSize: number,
fontFormat: string = "truetype",
): Promise<{ pixels: Uint8Array; screenshot: Buffer; inkPixels: number }> {
const charWidth = Math.ceil(fontSize * 1.5); const charWidth = Math.ceil(fontSize * 1.5);
const width = text.length * charWidth + 20; const width = text.length * charWidth + 20;
const height = Math.ceil(fontSize * 1.5); const height = Math.ceil(fontSize * 1.5);
const canvas = new Canvas(width, height); const renderUrl = `${baseUrl}/render?font=${encodeURIComponent(fontFamily)}&text=${encodeURIComponent(text)}&size=${fontSize}&width=${width}&height=${height}&format=${encodeURIComponent(fontFormat)}`;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "white"; await page.goto("about:blank");
ctx.fillRect(0, 0, width, height); await page.setViewport({ width, height });
ctx.font = `${fontSize}px ${fontFamily}`; await page.goto(renderUrl, { waitUntil: "load", timeout: 60000 });
ctx.fillStyle = "black"; await page.waitForFunction(() => document.title === "ready" || document.title.startsWith("error="), { timeout: 60000 });
ctx.fillText(text, 10, Math.ceil(fontSize * 1.2));
const buffer = await canvas.toBuffer("png"); const title = await page.title();
return writeFile(filePath, buffer); if (title.startsWith("error=")) {
throw new Error(`字体渲染失败 (${fontFamily}): ${title.slice(6)}`);
}
/**
* 使 DOM + canvas
* maxp
*/
const screenshot = await page.screenshot({ type: "png" });
const pixelData = await page.evaluate(() => {
const el = document.getElementById("text")!;
const rect = el.getBoundingClientRect();
/** 用 canvas 从 DOM 元素提取像素 */
const canvas = document.createElement("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(document.documentElement as any, 0, 0);
const imgData = ctx.getImageData(Math.round(rect.left), Math.round(rect.top), Math.round(rect.width), Math.round(rect.height));
let ink = 0;
for (let i = 0; i < imgData.data.length; i += 4) { if (imgData.data[i] < 128) ink++; }
return { pixels: Array.from(imgData.data), ink };
});
const inkPixels = pixelData.ink;
if (inkPixels === 0) {
throw new Error(`字体渲染无墨水像素 (${fontFamily}),字体可能未正确加载`);
}
return { pixels: new Uint8Array(pixelData.pixels), screenshot: Buffer.from(screenshot), inkPixels };
} }
/** 计算两张图片的结构相似度(简化版 SSIM返回 0~1 */ /** 计算两张图片的结构相似度(简化版 SSIM返回 0~1 */
function calculateSSIM(a: Uint8Array, b: Uint8Array): number { function calculateSSIM(a: Uint8Array, b: Uint8Array): number {
if (a.length !== b.length) return 0; if (a.length !== b.length) return 0;
const toGray = (data: Uint8Array, offset: number) =>
0.299 * data[offset] + 0.587 * data[offset + 1] + 0.114 * data[offset + 2];
const pixelCount = a.length / 4; const pixelCount = a.length / 4;
let sumA = 0, sumB = 0, sumA2 = 0, sumB2 = 0, sumAB = 0; let sumA = 0, sumB = 0, sumA2 = 0, sumB2 = 0, sumAB = 0;
for (let i = 0; i < pixelCount; i++) { for (let i = 0; i < pixelCount; i++) {
const idx = i * 4; const idx = i * 4;
const ga = toGray(a, idx); const ga = 0.299 * a[idx] + 0.587 * a[idx + 1] + 0.114 * a[idx + 2];
const gb = toGray(b, idx); const gb = 0.299 * b[idx] + 0.587 * b[idx + 1] + 0.114 * b[idx + 2];
sumA += ga; sumA += ga; sumB += gb;
sumB += gb; sumA2 += ga * ga; sumB2 += gb * gb; sumAB += ga * gb;
sumA2 += ga * ga;
sumB2 += gb * gb;
sumAB += ga * gb;
} }
const meanA = sumA / pixelCount, meanB = sumB / pixelCount;
const meanA = sumA / pixelCount;
const meanB = sumB / pixelCount;
const varA = sumA2 / pixelCount - meanA * meanA; const varA = sumA2 / pixelCount - meanA * meanA;
const varB = sumB2 / pixelCount - meanB * meanB; const varB = sumB2 / pixelCount - meanB * meanB;
const covAB = sumAB / pixelCount - meanA * meanB; const covAB = sumAB / pixelCount - meanA * meanB;
const C1 = 6.5025, C2 = 58.5225;
const C1 = 6.5025; return (2 * meanA * meanB + C1) * (2 * covAB + C2) / ((meanA * meanA + meanB * meanB + C1) * (varA + varB + C2));
const C2 = 58.5225;
return (2 * meanA * meanB + C1) * (2 * covAB + C2) /
((meanA * meanA + meanB * meanB + C1) * (varA + varB + C2));
} }
/** 注册子集字体用于渲染 */ // ======== 测试配置 ========
async function registerSubsetFont(ttfBuffer: ArrayBuffer, counter: number): Promise<string> {
await mkdir(BENCHMARK_DIR, { recursive: true });
const fontPath = `${BENCHMARK_DIR}/_bench_${counter}.ttf`;
await writeFile(fontPath, Buffer.from(ttfBuffer));
const familyName = `BenchSubset_${counter}`;
FontLibrary.use(familyName, [fontPath]);
return familyName;
}
await mkdir(BENCHMARK_DIR, { recursive: true }); const testCases = [
/** TTF 字体 */
{ label: "8个汉字", fontPath: "font/令东齐伋复刻体.ttf", fontName: "令东齐伋复刻体", text: "天地玄黄宇宙洪荒", sourceType: "ttf" as const, outType: "ttf" as const, fullFormat: "truetype" },
{ label: "8个汉字", fontPath: "font/令东齐伋复刻体.ttf", fontName: "令东齐伋复刻体", text: "天地玄黄宇宙洪荒", sourceType: "ttf" as const, outType: "woff2" as const, fullFormat: "truetype" },
{ label: "拉丁+数字", fontPath: "font/令东齐伋复刻体.ttf", fontName: "令东齐伋复刻体", text: "Hello World 123", sourceType: "ttf" as const, outType: "ttf" as const, fullFormat: "truetype" },
{ label: "拉丁+数字", fontPath: "font/令东齐伋复刻体.ttf", fontName: "令东齐伋复刻体", text: "Hello World 123", sourceType: "ttf" as const, outType: "woff2" as const, fullFormat: "truetype" },
{ label: "千字文前段", fontPath: "font/令东齐伋复刻体.ttf", fontName: "令东齐伋复刻体", text: "天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈剑号巨阙珠称夜光果珍李柰菜重芥姜海咸河淡鳞潜羽翔", sourceType: "ttf" as const, outType: "ttf" as const, fullFormat: "truetype" },
{ label: "千字文前段", fontPath: "font/令东齐伋复刻体.ttf", fontName: "令东齐伋复刻体", text: "天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈剑号巨阙珠称夜光果珍李柰菜重芥姜海咸河淡鳞潜羽翔", sourceType: "ttf" as const, outType: "woff2" as const, fullFormat: "truetype" },
/** OTF 字体 */
{ label: "otf-五个汉字", fontPath: "font/temp/BaiHuOTFJiaoYuHanZi-2.otf", fontName: "白狐教育汉字", text: "天地黄宇宙", sourceType: "otf" as const, outType: "ttf" as const, fullFormat: "opentype" },
];
// ======== 主测试 ========
await mkdir(`${BENCHMARK_DIR}/json`, { recursive: true });
await mkdir(`${BENCHMARK_DIR}/screenshots`, { recursive: true });
console.log("\n=== 字体裁剪基准测试 ===\n"); console.log("\n=== 字体裁剪基准测试 ===\n");
/** 预加载字体文件 */
const fontBuffers = new Map<string, ArrayBuffer>();
for (const tc of testCases) {
if (!fontBuffers.has(tc.fontPath)) {
const raw = await readFile(tc.fontPath);
fontBuffers.set(tc.fontPath, new Uint8Array(raw).buffer);
}
}
/** 注册完整字体到 HTTP 服务器 */
for (const tc of testCases) {
const key = `full_${tc.sourceType}_${tc.label}.ttf`;
const raw = await readFile(tc.fontPath);
fontStore.set(key, Buffer.from(raw));
/** OTF 也注册 .otf 后缀 */
if (tc.sourceType === "otf") {
fontStore.set(key.replace(".ttf", ".otf"), Buffer.from(raw));
}
}
/** 启动 HTTP 服务器和无头浏览器 */
const { server, port } = await createFontServer();
const baseUrl = `http://127.0.0.1:${port}`;
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-gpu", "--font-render-hinting=none"],
});
const page = await browser.newPage();
page.setViewport({ width: 1200, height: 800 });
page.on("console", (msg) => console.log(` [browser] ${msg.text()}`));
page.on("pageerror", (err) => console.log(` [page-error] ${err.message}`));
console.log(` HTTP 服务器: ${baseUrl}`);
console.log(` 无头浏览器: 已启动\n`);
const results: Array<{ const results: Array<{
label: string; label: string;
sourceType: string;
outType: string;
avg: number; avg: number;
min: number; min: number;
max: number; max: number;
outputSize: number; outputSize: number;
ssim: number; ssim: number;
fullInk: number;
subsetInk: number;
}> = []; }> = [];
for (const { label, text } of testCases) { for (const tc of testCases) {
const subset = [...text].map((c) => c.codePointAt(0)!); const buf = fontBuffers.get(tc.fontPath)!;
/** --- ttf 测试 --- */ /** --- 子集化测试(调用 API 的 fontSubset --- */
const ttfTimes: number[] = []; const times: number[] = [];
let lastTtfSize = 0; let lastSize = 0;
let lastTtfBuffer: ArrayBuffer | null = null; let lastBuffer: Uint8Array | null = null;
for (let i = 0; i < ROUNDS; i++) { for (let i = 0; i < ROUNDS; i++) {
const t0 = performance.now(); const t0 = performance.now();
const font = Font.create(fontBuffer, { type: "ttf", subset }); const subsetBuf = await fontSubset(buf, tc.text, { sourceType: tc.sourceType, outType: tc.outType });
const optimized = font.optimize().sort();
const result = optimized.write({ type: "ttf" });
const t1 = performance.now(); const t1 = performance.now();
ttfTimes.push(t1 - t0); times.push(t1 - t0);
lastTtfSize = typeof result === "string" ? result.length : result.byteLength; lastSize = subsetBuf.byteLength;
if (i === 0) { if (i === 0) lastBuffer = subsetBuf;
lastTtfBuffer = result instanceof ArrayBuffer ? result : new Uint8Array(result as any).buffer;
}
} }
const ttfAvg = ttfTimes.reduce((a, b) => a + b, 0) / ttfTimes.length; const avg = times.reduce((a, b) => a + b, 0) / times.length;
const ttfMin = Math.min(...ttfTimes); const min = Math.min(...times);
const ttfMax = Math.max(...ttfTimes); const max = Math.max(...times);
/** --- woff2 测试 --- */ /** --- 渲染对比 --- */
const woff2Times: number[] = [];
let lastWoff2Size = 0;
for (let i = 0; i < ROUNDS; i++) {
const t0 = performance.now();
const font = Font.create(fontBuffer, { type: "ttf", subset });
const optimized = font.optimize().sort();
const result = optimized.write({ type: "woff2" });
const t1 = performance.now();
woff2Times.push(t1 - t0);
lastWoff2Size = typeof result === "string" ? result.length : result.byteLength;
}
const woff2Avg = woff2Times.reduce((a, b) => a + b, 0) / woff2Times.length;
const woff2Min = Math.min(...woff2Times);
const woff2Max = Math.max(...woff2Times);
const compressionRatio = ((1 - lastWoff2Size / lastTtfSize) * 100).toFixed(1);
/** 计算渲染相似度(使用 ttf */
let ssim = 0; let ssim = 0;
if (lastTtfBuffer) { let fullInk = 0;
subsetFontCounter++; let subsetInk = 0;
const familyName = await registerSubsetFont(lastTtfBuffer, subsetFontCounter);
const safeLabel = label.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "_"); if (lastBuffer) {
await renderTextToPng(FONT_NAME, text, 48, `${BENCHMARK_DIR}/${safeLabel}_full.png`); const safeLabel = tc.label.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "_");
await renderTextToPng(familyName, text, 48, `${BENCHMARK_DIR}/${safeLabel}_subset.png`);
const fullPixels = renderText(FONT_NAME, text, 48); /**
const subsetPixels = renderText(familyName, text, 48); * maxp maxPoints/maxContours 0
ssim = calculateSSIM(fullPixels, subsetPixels); * 0 /fallback
} * OTFTTF optimizettf _flatContours
*/
results.push({ label, avg: ttfAvg, min: ttfMin, max: ttfMax, outputSize: lastTtfSize, ssim }); if (tc.outType !== "woff2") {
console.log(` [ttf] ${label}: avg=${ttfAvg.toFixed(1)}ms min=${ttfMin.toFixed(1)}ms max=${ttfMax.toFixed(1)}ms 输出=${lastTtfSize.toLocaleString()} bytes ssim=${ssim.toFixed(4)}`); const ttfView = new DataView(lastBuffer.buffer, lastBuffer.byteOffset, lastBuffer.byteLength);
console.log(` [woff2] ${label}: avg=${woff2Avg.toFixed(1)}ms min=${woff2Min.toFixed(1)}ms max=${woff2Max.toFixed(1)}ms 输出=${lastWoff2Size.toLocaleString()} bytes 压缩率=${compressionRatio}%`); const numTbl = ttfView.getUint16(4, false);
} for (let ti = 0; ti < numTbl; ti++) {
const toff = 12 + ti * 16;
/** --- OTF 测试(直接 OTF 子集化SSIM 对比原始 OTF 渲染 vs 子集 TTF 渲染) --- */ const tag = String.fromCharCode(ttfView.getUint8(toff), ttfView.getUint8(toff + 1), ttfView.getUint8(toff + 2), ttfView.getUint8(toff + 3));
const OTF_FONT_PATH = "font/temp/SourceHanSans-Regular.otf"; if (tag === "maxp") {
let otfTestResults: string[] = []; const moff = ttfView.getUint32(toff + 8, false);
try { const maxPoints = ttfView.getUint16(moff + 6, false);
const otfRaw = await readFile(OTF_FONT_PATH); const maxContours = ttfView.getUint16(moff + 8, false);
const otfBuffer = new Uint8Array(otfRaw).buffer; if (maxPoints === 0 || maxContours === 0) {
throw new Error(`子集字体 maxp 异常: maxPoints=${maxPoints} maxContours=${maxContours}(浏览器将跳过渲染)`);
/** 注册原始 OTF 字体作为渲染基准 */ }
const OTF_FONT_NAME = `OTF_Bench_${Date.now()}`; break;
FontLibrary.use(OTF_FONT_NAME, OTF_FONT_PATH); }
const otfTestCases = [
{ label: "otf-8个汉字", text: "天地玄黄宇宙洪荒" },
{ label: "otf-拉丁+数字", text: "Hello World 123" },
];
for (const { label, text } of otfTestCases) {
const subset = [...text].map((c) => c.codePointAt(0)!);
const otfTimes: number[] = [];
let lastOtfTtfSize = 0;
let lastOtfTtfBuffer: ArrayBuffer | null = null;
for (let i = 0; i < ROUNDS; i++) {
const t0 = performance.now();
const font = Font.create(otfBuffer, { type: "otf", subset });
const optimized = font.optimize().sort();
const result = optimized.write({ type: "ttf" });
const t1 = performance.now();
otfTimes.push(t1 - t0);
lastOtfTtfSize = typeof result === "string" ? result.length : result.byteLength;
if (i === 0) {
lastOtfTtfBuffer = result instanceof ArrayBuffer ? result : new Uint8Array(result as any).buffer;
} }
} }
const otfAvg = otfTimes.reduce((a, b) => a + b, 0) / otfTimes.length; /** 注册子集字体 */
const otfMin = Math.min(...otfTimes); const subsetKey = `subset_${safeLabel}.${tc.outType}`;
const otfMax = Math.max(...otfTimes); fontStore.set(subsetKey, Buffer.from(lastBuffer));
/** OTF SSIM对比原始 OTF 渲染 vs 子集 TTF 渲染 */ /** 完整字体 key */
let otfSsim = 0; const fullKey = tc.sourceType === "otf"
if (lastOtfTtfBuffer) { ? `full_otf_${tc.label}.otf`
subsetFontCounter++; : `full_ttf_${tc.label}.ttf`;
const familyName = await registerSubsetFont(lastOtfTtfBuffer, subsetFontCounter);
const safeLabel = label.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "_"); /** 渲染完整字体 */
await renderTextToPng(OTF_FONT_NAME, text, 48, `${BENCHMARK_DIR}/${safeLabel}_otf_full.png`); const fullResult = await renderTextViaBrowser(page, baseUrl, fullKey, tc.text, 48, tc.fullFormat);
await renderTextToPng(familyName, text, 48, `${BENCHMARK_DIR}/${safeLabel}_otf_subset.png`);
const fullPixels = renderText(OTF_FONT_NAME, text, 48); /** 渲染子集字体 */
const subsetPixels = renderText(familyName, text, 48); const subsetResult = await renderTextViaBrowser(page, baseUrl, subsetKey, tc.text, 48, "truetype");
otfSsim = calculateSSIM(fullPixels, subsetPixels);
}
otfTestResults.push(` [otf] ${label}: avg=${otfAvg.toFixed(1)}ms min=${otfMin.toFixed(1)}ms max=${otfMax.toFixed(1)}ms 输出=${lastOtfTtfSize.toLocaleString()} bytes ssim=${otfSsim.toFixed(4)}`); await writeFile(`${BENCHMARK_DIR}/screenshots/${safeLabel}_full.png`, fullResult.screenshot);
await writeFile(`${BENCHMARK_DIR}/screenshots/${safeLabel}_subset.png`, subsetResult.screenshot);
fullInk = fullResult.inkPixels;
subsetInk = subsetResult.inkPixels;
ssim = calculateSSIM(fullResult.pixels, subsetResult.pixels);
} }
} catch {
otfTestResults.push(" [otf] 跳过(未找到 OTF 测试字体 font/temp/SourceHanSans-Regular.otf"); results.push({ label: tc.label, sourceType: tc.sourceType, outType: tc.outType, avg, min, max, outputSize: lastSize, ssim, fullInk, subsetInk });
const tag = tc.sourceType === "otf" ? "otf→ttf" : tc.outType;
console.log(` [${tag}] ${tc.label}: avg=${avg.toFixed(1)}ms min=${min.toFixed(1)}ms max=${max.toFixed(1)}ms 输出=${lastSize.toLocaleString()} bytes ssim=${ssim.toFixed(4)} ink=${fullInk}/${subsetInk}`);
} }
/** 清理 */
await browser.close();
server.close();
/** 保存结果到 JSON */ /** 保存结果到 JSON */
const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const resultFile = `${BENCHMARK_DIR}/benchmark_${timestamp}.json`; const resultFile = `${BENCHMARK_DIR}/json/benchmark_${timestamp}.json`;
await writeFile(resultFile, JSON.stringify({ timestamp: new Date().toISOString(), rounds: ROUNDS, results }, null, 2)); await writeFile(resultFile, JSON.stringify({ timestamp: new Date().toISOString(), rounds: ROUNDS, results }, null, 2));
console.log(`\n结果已保存到 ${resultFile}`); console.log(`\n结果已保存到 ${resultFile}`);
console.log(`渲染对比图片已保存到 ${BENCHMARK_DIR}/ 目录`); console.log(`渲染对比图片已保存到 ${BENCHMARK_DIR}/screenshots/ 目录\n`);
if (otfTestResults.length) {
console.log("\n--- OTF→TTF 子集化 ---");
for (const line of otfTestResults) console.log(line);
}
console.log("");