性能优化

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",
"@xmldom/xmldom": "^0.9.9",
"jsdom": "^29.0.2",
"puppeteer": "^24.40.0",
"skia-canvas": "^3.0.8",
"tsup": "^8.5.1",
"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
});
exports.default = bezierCubic2Q2;
exports.bezierCubic2Q2Raw = bezierCubic2Q2Raw;
/**
* @file 三次贝塞尔转二次贝塞尔高精度递归分割版
* @author mengke01(kekee000@gmail.com)
*
* 改进递归分割三次贝塞尔直到可精确近似提高 SSIM
* 优化返回扁平数组 [control1, endpoint1, control2, endpoint2, ...]
* 减少 3-element 包装数组的分配
* 优化160+179: 返回扁平数组 [cx, cy, ex, ey, ...]减少对象分配
* 优化179: 新增 bezierCubic2Q2Raw 接受原始坐标参数消除调用方对象分配
*/
var MAX_DEPTH = 4;
var MAX_DEPTH = 8;
function isFlatEnough(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
var ux = 3 * c1x - 2 * p1x - p2x;
var uy = 3 * c1y - 2 * p1y - p2y;
var vx = 3 * c2x - 2 * p2x - p1x;
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) {
controls.push({
x: (3 * c2x - p2x + 3 * c1x - p1x) * 0.25,
y: (3 * c2y - p2y + 3 * c1y - p1y) * 0.25
});
/** 控制点 + 端点 */
result.push(
(3 * c2x - p2x + 3 * c1x - p1x) * 0.25,
(3 * c2y - p2y + 3 * c1y - p1y) * 0.25,
p2x,
p2y
);
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 midx = (m012x + m123x) * 0.5, midy = (m012y + m123y) * 0.5;
cubicToQuads(p1x, p1y, m01x, m01y, m012x, m012y, midx, midy, depth + 1, endpoints, controls);
endpoints.push({ x: midx, y: midy, onCurve: true });
cubicToQuads(midx, midy, m123x, m123y, m23x, m23y, p2x, p2y, depth + 1, endpoints, controls);
cubicToQuads(p1x, p1y, m01x, m01y, m012x, m012y, midx, midy, depth + 1, result);
cubicToQuads(midx, midy, m123x, m123y, m23x, m23y, p2x, p2y, depth + 1, result);
}
/**
* 三次贝塞尔转二次贝塞尔
* 返回扁平数组: [control1, endpoint1, control2, endpoint2, ...]
* 每对 (control, endpoint) 代表一个二次贝塞尔段
* 优化179: 接受原始坐标参数消除调用方临时对象分配
* 返回扁平数组: [ctrlX, ctrlY, endX, endY, ctrlX, ctrlY, endX, endY, ...]
*/
function bezierCubic2Q2(p1, c1, c2, p2) {
if (p1.x === c1.x && p1.y === c1.y && c2.x === p2.x && c2.y === p2.y) {
return [{
x: (p1.x + p2.x) * 0.5,
y: (p1.y + p2.y) * 0.5
}, p2];
function bezierCubic2Q2Raw(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
if (p1x === c1x && p1y === c1y && c2x === p2x && c2y === p2y) {
return [
(p1x + p2x) * 0.5,
(p1y + p2y) * 0.5,
p2x,
p2y
];
}
var endpoints = [];
var controls = [];
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;
}
var result = [];
cubicToQuads(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, 0, 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 result = (0, _otfContours2ttfContours.default)(g.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) {
g.xMin = result.xMin;
g.xMax = result.xMax;

View File

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

View File

@ -138,15 +138,21 @@ var Reader = exports.default = /*#__PURE__*/function () {
if (length < 0 || offset + length > this.length) {
_error.default.raise(10001, this.length, offset + length);
}
/* 优化22: 批量读取字节后构建字符串,替代逐字节 readUint8 */
var chars = new Array(length);
/* 优化22+179: 使用 Uint8Array + String.fromCharCode.apply 批量转换 */
var viewOffset = this.view.byteOffset + offset;
var buf = this.view.buffer;
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]);
}
var bytes = new Uint8Array(this.view.buffer, viewOffset, 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));
}
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) {
value = conversionFn(value);
}
@ -113,8 +116,17 @@ function parseCFFIndexOffsets(reader, offset) {
if (count !== 0) {
var offsetSize = reader.readUint8();
offsets = new Array(count + 1);
for (var i = 0; i <= count; i++) {
offsets[i] = getOffset(reader, offsetSize);
/** 优化166: 内联 getOffset消除函数调用开销 */
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 };
@ -128,11 +140,14 @@ function parseCFFIndexOffsets(reader, offset) {
* @param {number} idx object 索引0-based
* @return {Uint8Array} object 数据
*/
/**
* 优化169+179: 直接从 view 创建 Uint8Array 视图避免 readBytes slice 开销
*/
function readCFFIndexObject(reader, indexInfo, idx) {
var off = indexInfo.offsets;
var size = off[idx + 1] - off[idx];
reader.seek(indexInfo.dataStart + off[idx] - 1);
return reader.readBytes(size);
var start = indexInfo.dataStart + off[idx] - 1;
return new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, size);
}
// 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.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);
result.subrs = subrIndex.objects;
result.subrsBias = calcCFFSubroutineBias(result.subrs);
@ -350,7 +367,7 @@ var _default = exports.default = _table.default.create('cff', [], {
}
// 私有子glyf数据非 CID 字体使用)
if (privateDict.subrs) {
if (privateDict.subrs != null && privateDict.subrs > 0) {
var subrOffset = privateDictOffset + privateDict.subrs;
var subrIndex = parseCFFIndex(reader, subrOffset);
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++) {
subsetSet[subset[si]] = true;
}
for (var c in codes) {
if (subsetSet[c]) {
var ci = codes[c];
subsetMap[ci] = true;
}
/** 优化168: 用 subset 直接遍历替代 codes for...in减少遍历量 */
for (var si = 0, sl = subset.length; si < sl; si++) {
var ci = codes[subset[si]];
if (ci !== undefined) subsetMap[ci] = true;
}
font.subsetMap = subsetMap;
for (var i in subsetMap) {
i = +i;
/** 优化163+167: 构建 subsetGids 密集数组,传递给 otfreader 复用 */
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 glyf = (0, _parseCFFGlyph.default)(charstring, getGlyphFont(i), i);
glyf.name = cff.charset[i];
cff.glyf[i] = glyf;
}
font.subsetGids = subsetGids;
}
// parse all
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]
// 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) {
var format = reader.readUint16(this.offset);
var struct = this.struct;

View File

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

View File

@ -118,6 +118,7 @@ function readSubTable(reader, ttf, subTable, cmapOffset) {
vOffset += 2;
}
format4.idRangeOffset = idRangeOffset;
format4.segCount = segCount;
/* 优化101: subset 模式下跳过 glyphIdArray 解析,直接从 view 按需读取 */
var isSubset4 = ttf.readOptions && ttf.readOptions.subset;
@ -135,6 +136,8 @@ function readSubTable(reader, ttf, subTable, cmapOffset) {
}
format4.glyphIdArray = glyphIdArray4;
}
/* 优化177: 预计算 glyphIdArrayIndexOffset消除 lookupFormat4 中的重复除法 */
format4.glyphIdArrayIndexOffset = (format4.glyphIdArrayOffset - format4.idRangeOffsetOffset) / 2;
} else if (subTable.format === 6) {
var format6 = subTable;
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;
}
/** 优化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) {
var prevGlyph = null;
var result = [];
@ -59,8 +108,10 @@ function getFormat0Segment(glyfUnicodes) {
function sizeof(ttf) {
ttf.support.cmap = {};
var glyfUnicodes = [];
/* 优化155: 使用并行扁平数组替代对象数组,减少 GC 压力 */
var glyfs = ttf.glyf;
var unicodeArr = [];
var idArr = [];
for (var index = 0, gl = glyfs.length; index < gl; index++) {
var glyph = glyfs[index];
var unicodes = glyph.unicode;
@ -69,36 +120,58 @@ function sizeof(ttf) {
}
if (unicodes && unicodes.length) {
for (var ui = 0, ul = unicodes.length; ui < ul; ui++) {
glyfUnicodes.push({
unicode: unicodes[ui],
id: unicodes[ui] !== 0xFFFF ? index : 0
});
unicodeArr.push(unicodes[ui]);
idArr.push(unicodes[ui] !== 0xFFFF ? index : 0);
}
}
}
glyfUnicodes.sort(function (a, b) { return a.unicode - b.unicode; });
ttf.support.cmap.unicodes = glyfUnicodes;
var unicodes2Bytes = glyfUnicodes;
ttf.support.cmap.format4Segments = getSegments(unicodes2Bytes, 0xFFFF);
ttf.support.cmap.format4Size = 24 + ttf.support.cmap.format4Segments.length * 8;
ttf.support.cmap.format0Segments = getFormat0Segment(glyfUnicodes);
/* 优化179: 二分插入排序O(n log n) 查找 + O(n) 移位 */
var len = unicodeArr.length;
for (var i = 1; i < len; i++) {
var uKey = unicodeArr[i];
var iKey = idArr[i];
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.format0Size = ttf.support.cmap.hasFormat0 ? 262 : 0;
/* 优化142: 排序后直接检查最大 unicode避免单独遍历 */
var hasGLyphsOver2Bytes = glyfUnicodes.length > 0 && glyfUnicodes[glyfUnicodes.length - 1].unicode > 0xFFFF;
/** format 12 subtableplatformID=3, encodingID=10
* 现代浏览器使用 unicode-range 时依赖 format 12 来匹配字符
* 仅当有 cmap 映射数据时才生成避免 nGroups=0 的无效 subtable */
var hasGLyphsOver2Bytes = len > 0;
if (hasGLyphsOver2Bytes) {
ttf.support.cmap.hasGLyphsOver2Bytes = true;
ttf.support.cmap.format12Segments = getSegments(glyfUnicodes);
ttf.support.cmap.format12Size = 16 + ttf.support.cmap.format12Segments.length * 12;
ttf.support.cmap.format12Segments = getSegmentsFlat(unicodeArr, idArr);
ttf.support.cmap.format12Size = 16 + (ttf.support.cmap.format12Segments.length / 4) * 12;
}
/** 记录头大小必须动态计算,与 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 size = recordHeaderSize
+ ttf.support.cmap.format0Size
+ ttf.support.cmap.format4Size
+ (hasGLyphsOver2Bytes ? ttf.support.cmap.format12Size : 0);
+ (ttf.support.cmap.hasGLyphsOver2Bytes ? ttf.support.cmap.format12Size : 0);
return size;
}

View File

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

View File

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

View File

@ -12,6 +12,38 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
* @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]], {
/* 优化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
*/
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;
},
write: function write(writer, ttf) {
var i;
var numOfLongHorMetrics = ttf.hhea.numOfLongHorMetrics;
/* 优化30+82: 直接 view 批量写入 */
/* 优化30+82+171: 缓存 glyfs[i] 到循环变量 */
var wView = writer.view;
var pos = writer.offset;
var glyfs = ttf.glyf;
for (i = 0; i < numOfLongHorMetrics; i++) {
wView.setUint16(pos, glyfs[i].advanceWidth, false);
wView.setInt16(pos + 2, glyfs[i].leftSideBearing, false);
for (var i = 0; i < numOfLongHorMetrics; i++) {
var g = glyfs[i];
wView.setUint16(pos, g.advanceWidth, false);
wView.setInt16(pos + 2, g.leftSideBearing, false);
pos += 4;
}
var numOfLast = glyfs.length - numOfLongHorMetrics;
for (i = 0; i < numOfLast; i++) {
wView.setInt16(pos, glyfs[numOfLongHorMetrics + i].leftSideBearing, false);
for (var j = 0; j < numOfLast; j++) {
wView.setInt16(pos, glyfs[numOfLongHorMetrics + j].leftSideBearing, false);
pos += 2;
}
writer.offset = pos;
return writer;
},
size: function size(ttf) {
/* 优化171: 缓存 ttf.glyf 到局部变量,消除循环内属性链查找 */
var glyfs = ttf.glyf;
var gl = glyfs.length;
var numOfLast = 0;
var advanceWidth = ttf.glyf[ttf.glyf.length - 1].advanceWidth;
for (var i = ttf.glyf.length - 2; i >= 0; i--) {
if (advanceWidth === ttf.glyf[i].advanceWidth) {
var advanceWidth = glyfs[gl - 1].advanceWidth;
for (var i = gl - 2; i >= 0; i--) {
if (advanceWidth === glyfs[i].advanceWidth) {
numOfLast++;
} else {
break;
}
}
ttf.hhea.numOfLongHorMetrics = ttf.glyf.length - numOfLast;
ttf.hhea.numOfLongHorMetrics = gl - 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 pos = writer.offset;
if (indexToLocFormat) {
for (var i = 0; i <= numGlyphs; i++) {
/* 优化171: 拆分循环消除 i < numGlyphs 条件判断 */
for (var i = 0; i < numGlyphs; i++) {
wView.setUint32(pos, offset, false);
pos += 4;
if (i < numGlyphs) {
offset += glyfSupport[i].size;
}
offset += glyfSupport[i].size;
}
wView.setUint32(pos, offset, false);
pos += 4;
} else {
/* 优化110: 短格式使用右移替代浮点乘 0.5 */
for (var j = 0; j <= numGlyphs; j++) {
/* 优化110+171: 短格式右移 + 拆分循环 */
for (var j = 0; j < numGlyphs; j++) {
wView.setUint16(pos, offset >> 1, false);
pos += 2;
if (j < numGlyphs) {
offset += glyfSupport[j].size;
}
offset += glyfSupport[j].size;
}
wView.setUint16(pos, offset >> 1, false);
pos += 2;
}
writer.offset = pos;
return writer;

View File

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

View File

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

View File

@ -142,12 +142,14 @@ function ceilReduceAndSizeFromTypedArrays(glyf, sharedXBuf, sharedYBuf) {
*/
function ceilReduceAndSizeFlat(glyf, sharedXBuf, sharedYBuf) {
var contours = glyf.contours;
/* 优化91: 跳过 reducePathFlat */
for (var j = contours.length - 1; j >= 0; j--) {
if (contours[j].length <= 6) {
contours.splice(j, 1);
/* 优化91+164: 跳过 reducePathFlat用 write-index 替代 splice */
var writeIdx = 0;
for (var j = 0, cl = contours.length; j < cl; j++) {
if (contours[j].length > 6) {
contours[writeIdx++] = contours[j];
}
}
contours.length = writeIdx;
if (0 === contours.length) {
delete glyf.contours;
return { sharedXBuf: sharedXBuf, sharedYBuf: sharedYBuf };
@ -330,6 +332,26 @@ function optimizettf(ttf) {
if (glyf._flatContours) {
var flatResult = ceilReduceAndSizeFlat(glyf, sharedXBuf, 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 {
/* 对象 contours 格式也需要收集 maxPoints/maxContours */
var numC = glyf.contours.length;

View File

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

View File

@ -21,6 +21,10 @@ if (typeof ArrayBuffer === 'undefined' || typeof DataView === 'undefined') {
throw new Error('not support ArrayBuffer and DataView');
}
/** 优化178: 全局 Uint8Array 视图缓存,避免 writeBytes/writeEmpty 每次创建视图 */
var _globalView = null;
var _globalViewBuf = null;
// 数据类型
var dataType = {
Int8: 1,
@ -111,15 +115,16 @@ var Writer = /*#__PURE__*/function () {
if (length < 0 || offset + length > this.length) {
_error.default.raise(10002, this.length, offset + length);
}
/* 优化151: 缓存 buffer 引用,避免每次创建 Uint8Array 视图 */
var vBuf = this.view.buffer;
/* 优化178: 复用全局 Uint8Array 视图,避免每次 writeBytes 创建新视图 */
if (_globalViewBuf !== this.view.buffer) {
_globalViewBuf = this.view.buffer;
_globalView = new Uint8Array(_globalViewBuf);
}
var vOff = this.view.byteOffset + offset;
if (value instanceof ArrayBuffer) {
new Uint8Array(vBuf, vOff, length).set(new Uint8Array(value, 0, length));
} else if (value instanceof Uint8Array) {
new Uint8Array(vBuf, vOff, length).set(value);
if (value instanceof Uint8Array) {
_globalView.set(value, vOff);
} 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;
return this;
@ -141,8 +146,12 @@ var Writer = /*#__PURE__*/function () {
if (undefined === offset) {
offset = this.offset;
}
/* 优化5: fill(0) 批量填充,替代逐字节循环 */
new Uint8Array(this.view.buffer, this.view.byteOffset + offset, length).fill(0);
/* 优化178: 复用全局视图 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;
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
* 2. vs SSIM
* 3. benchmark_results/
* 1. fontSubset
* 2. vs SSIM
* 3. benchmark_results/screenshots/
*/
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 { Font } from "./vendor/fonteditor-core/lib/ttf/font.js";
import woff2Module from "./vendor/fonteditor-core/woff2/index.js";
import { Canvas, FontLibrary } from "skia-canvas";
import puppeteer, { type Page } from "puppeteer";
import { fontSubset } from "./backend/font_util/font.js";
const FONT_PATH = "font/令东齐伋复刻体.ttf";
const FONT_NAME = "令东齐伋复刻体";
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;
/** 子集字体临时文件计数器 */
let subsetFontCounter = 0;
// ======== HTTP 服务器 ========
const fontStore = new Map<string, Buffer>();
/** 渲染文字到像素数据 */
function renderText(fontFamily: string, text: string, fontSize: number): Uint8Array {
const charWidth = Math.ceil(fontSize * 1.5);
const width = text.length * charWidth + 20;
const height = Math.ceil(fontSize * 1.5);
const canvas = new Canvas(width, height);
const ctx = canvas.getContext("2d");
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.fillStyle = "black";
ctx.fillText(text, 10, Math.ceil(fontSize * 1.2));
const imgData = ctx.getImageData(0, 0, width, height);
return new Uint8Array(imgData.data.buffer);
function createFontServer(): Promise<{ server: Server; port: number }> {
return new Promise((resolve) => {
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url!, `http://localhost`);
const path = url.pathname;
if (path === "/render") {
const params = url.searchParams;
const fontFamily = params.get("font") || "TestFont";
const fontFormat = params.get("format") || "truetype";
const text = params.get("text") || "";
const fontSize = parseInt(params.get("size") || "48", 10);
const width = parseInt(params.get("width") || "800", 10);
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 width = text.length * charWidth + 20;
const height = Math.ceil(fontSize * 1.5);
const canvas = new Canvas(width, height);
const ctx = canvas.getContext("2d");
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.fillStyle = "black";
ctx.fillText(text, 10, Math.ceil(fontSize * 1.2));
const buffer = await canvas.toBuffer("png");
return writeFile(filePath, buffer);
const renderUrl = `${baseUrl}/render?font=${encodeURIComponent(fontFamily)}&text=${encodeURIComponent(text)}&size=${fontSize}&width=${width}&height=${height}&format=${encodeURIComponent(fontFormat)}`;
await page.goto("about:blank");
await page.setViewport({ width, height });
await page.goto(renderUrl, { waitUntil: "load", timeout: 60000 });
await page.waitForFunction(() => document.title === "ready" || document.title.startsWith("error="), { timeout: 60000 });
const title = await page.title();
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 */
function calculateSSIM(a: Uint8Array, b: Uint8Array): number {
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;
let sumA = 0, sumB = 0, sumA2 = 0, sumB2 = 0, sumAB = 0;
for (let i = 0; i < pixelCount; i++) {
const idx = i * 4;
const ga = toGray(a, idx);
const gb = toGray(b, idx);
sumA += ga;
sumB += gb;
sumA2 += ga * ga;
sumB2 += gb * gb;
sumAB += ga * gb;
const ga = 0.299 * a[idx] + 0.587 * a[idx + 1] + 0.114 * a[idx + 2];
const gb = 0.299 * b[idx] + 0.587 * b[idx + 1] + 0.114 * b[idx + 2];
sumA += ga; sumB += gb;
sumA2 += ga * ga; sumB2 += gb * gb; sumAB += ga * gb;
}
const meanA = sumA / pixelCount;
const meanB = sumB / pixelCount;
const meanA = sumA / pixelCount, meanB = sumB / pixelCount;
const varA = sumA2 / pixelCount - meanA * meanA;
const varB = sumB2 / pixelCount - meanB * meanB;
const covAB = sumAB / pixelCount - meanA * meanB;
const C1 = 6.5025;
const C2 = 58.5225;
return (2 * meanA * meanB + C1) * (2 * covAB + C2) /
((meanA * meanA + meanB * meanB + C1) * (varA + varB + C2));
const C1 = 6.5025, 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");
/** 预加载字体文件 */
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<{
label: string;
sourceType: string;
outType: string;
avg: number;
min: number;
max: number;
outputSize: number;
ssim: number;
fullInk: number;
subsetInk: number;
}> = [];
for (const { label, text } of testCases) {
const subset = [...text].map((c) => c.codePointAt(0)!);
for (const tc of testCases) {
const buf = fontBuffers.get(tc.fontPath)!;
/** --- ttf 测试 --- */
const ttfTimes: number[] = [];
let lastTtfSize = 0;
let lastTtfBuffer: ArrayBuffer | null = null;
/** --- 子集化测试(调用 API 的 fontSubset --- */
const times: number[] = [];
let lastSize = 0;
let lastBuffer: Uint8Array | null = null;
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: "ttf" });
const subsetBuf = await fontSubset(buf, tc.text, { sourceType: tc.sourceType, outType: tc.outType });
const t1 = performance.now();
ttfTimes.push(t1 - t0);
lastTtfSize = typeof result === "string" ? result.length : result.byteLength;
if (i === 0) {
lastTtfBuffer = result instanceof ArrayBuffer ? result : new Uint8Array(result as any).buffer;
}
times.push(t1 - t0);
lastSize = subsetBuf.byteLength;
if (i === 0) lastBuffer = subsetBuf;
}
const ttfAvg = ttfTimes.reduce((a, b) => a + b, 0) / ttfTimes.length;
const ttfMin = Math.min(...ttfTimes);
const ttfMax = Math.max(...ttfTimes);
const avg = times.reduce((a, b) => a + b, 0) / times.length;
const min = Math.min(...times);
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;
if (lastTtfBuffer) {
subsetFontCounter++;
const familyName = await registerSubsetFont(lastTtfBuffer, subsetFontCounter);
let fullInk = 0;
let subsetInk = 0;
const safeLabel = label.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "_");
await renderTextToPng(FONT_NAME, text, 48, `${BENCHMARK_DIR}/${safeLabel}_full.png`);
await renderTextToPng(familyName, text, 48, `${BENCHMARK_DIR}/${safeLabel}_subset.png`);
if (lastBuffer) {
const safeLabel = tc.label.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "_");
const fullPixels = renderText(FONT_NAME, text, 48);
const subsetPixels = renderText(familyName, text, 48);
ssim = calculateSSIM(fullPixels, subsetPixels);
}
results.push({ label, avg: ttfAvg, min: ttfMin, max: ttfMax, outputSize: lastTtfSize, ssim });
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)}`);
console.log(` [woff2] ${label}: avg=${woff2Avg.toFixed(1)}ms min=${woff2Min.toFixed(1)}ms max=${woff2Max.toFixed(1)}ms 输出=${lastWoff2Size.toLocaleString()} bytes 压缩率=${compressionRatio}%`);
}
/** --- OTF 测试(直接 OTF 子集化SSIM 对比原始 OTF 渲染 vs 子集 TTF 渲染) --- */
const OTF_FONT_PATH = "font/temp/SourceHanSans-Regular.otf";
let otfTestResults: string[] = [];
try {
const otfRaw = await readFile(OTF_FONT_PATH);
const otfBuffer = new Uint8Array(otfRaw).buffer;
/** 注册原始 OTF 字体作为渲染基准 */
const OTF_FONT_NAME = `OTF_Bench_${Date.now()}`;
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;
/**
* maxp maxPoints/maxContours 0
* 0 /fallback
* OTFTTF optimizettf _flatContours
*/
if (tc.outType !== "woff2") {
const ttfView = new DataView(lastBuffer.buffer, lastBuffer.byteOffset, lastBuffer.byteLength);
const numTbl = ttfView.getUint16(4, false);
for (let ti = 0; ti < numTbl; ti++) {
const toff = 12 + ti * 16;
const tag = String.fromCharCode(ttfView.getUint8(toff), ttfView.getUint8(toff + 1), ttfView.getUint8(toff + 2), ttfView.getUint8(toff + 3));
if (tag === "maxp") {
const moff = ttfView.getUint32(toff + 8, false);
const maxPoints = ttfView.getUint16(moff + 6, false);
const maxContours = ttfView.getUint16(moff + 8, false);
if (maxPoints === 0 || maxContours === 0) {
throw new Error(`子集字体 maxp 异常: maxPoints=${maxPoints} maxContours=${maxContours}(浏览器将跳过渲染)`);
}
break;
}
}
}
const otfAvg = otfTimes.reduce((a, b) => a + b, 0) / otfTimes.length;
const otfMin = Math.min(...otfTimes);
const otfMax = Math.max(...otfTimes);
/** 注册子集字体 */
const subsetKey = `subset_${safeLabel}.${tc.outType}`;
fontStore.set(subsetKey, Buffer.from(lastBuffer));
/** OTF SSIM对比原始 OTF 渲染 vs 子集 TTF 渲染 */
let otfSsim = 0;
if (lastOtfTtfBuffer) {
subsetFontCounter++;
const familyName = await registerSubsetFont(lastOtfTtfBuffer, subsetFontCounter);
/** 完整字体 key */
const fullKey = tc.sourceType === "otf"
? `full_otf_${tc.label}.otf`
: `full_ttf_${tc.label}.ttf`;
const safeLabel = label.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "_");
await renderTextToPng(OTF_FONT_NAME, text, 48, `${BENCHMARK_DIR}/${safeLabel}_otf_full.png`);
await renderTextToPng(familyName, text, 48, `${BENCHMARK_DIR}/${safeLabel}_otf_subset.png`);
/** 渲染完整字体 */
const fullResult = await renderTextViaBrowser(page, baseUrl, fullKey, tc.text, 48, tc.fullFormat);
const fullPixels = renderText(OTF_FONT_NAME, text, 48);
const subsetPixels = renderText(familyName, text, 48);
otfSsim = calculateSSIM(fullPixels, subsetPixels);
}
/** 渲染子集字体 */
const subsetResult = await renderTextViaBrowser(page, baseUrl, subsetKey, tc.text, 48, "truetype");
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 */
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));
console.log(`\n结果已保存到 ${resultFile}`);
console.log(`渲染对比图片已保存到 ${BENCHMARK_DIR}/ 目录`);
if (otfTestResults.length) {
console.log("\n--- OTF→TTF 子集化 ---");
for (const line of otfTestResults) console.log(line);
}
console.log("");
console.log(`渲染对比图片已保存到 ${BENCHMARK_DIR}/screenshots/ 目录\n`);