mirror of
https://github.com/2234839/web-font.git
synced 2026-06-28 19:08:11 +08:00
性能优化
This commit is contained in:
parent
4990a0f61d
commit
af0ab38cec
@ -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
770
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
13
vendor/fonteditor-core/lib/ttf/otfreader.js
vendored
13
vendor/fonteditor-core/lib/ttf/otfreader.js
vendored
@ -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;
|
||||
}
|
||||
|
||||
20
vendor/fonteditor-core/lib/ttf/reader.js
vendored
20
vendor/fonteditor-core/lib/ttf/reader.js
vendored
@ -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('');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
50
vendor/fonteditor-core/lib/ttf/table/CFF.js
vendored
50
vendor/fonteditor-core/lib/ttf/table/CFF.js
vendored
@ -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 偏移量可能为 0(CFF 规范允许),
|
||||
* 原代码用 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 {
|
||||
|
||||
58
vendor/fonteditor-core/lib/ttf/table/OS2.js
vendored
58
vendor/fonteditor-core/lib/ttf/table/OS2.js
vendored
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
107
vendor/fonteditor-core/lib/ttf/table/cmap/sizeof.js
vendored
107
vendor/fonteditor-core/lib/ttf/table/cmap/sizeof.js
vendored
@ -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 subtable(platformID=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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
38
vendor/fonteditor-core/lib/ttf/table/head.js
vendored
38
vendor/fonteditor-core/lib/ttf/table/head.js
vendored
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
26
vendor/fonteditor-core/lib/ttf/table/hhea.js
vendored
26
vendor/fonteditor-core/lib/ttf/table/hhea.js
vendored
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
25
vendor/fonteditor-core/lib/ttf/table/hmtx.js
vendored
25
vendor/fonteditor-core/lib/ttf/table/hmtx.js
vendored
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
19
vendor/fonteditor-core/lib/ttf/table/loca.js
vendored
19
vendor/fonteditor-core/lib/ttf/table/loca.js
vendored
@ -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;
|
||||
|
||||
8
vendor/fonteditor-core/lib/ttf/ttfwriter.js
vendored
8
vendor/fonteditor-core/lib/ttf/ttfwriter.js
vendored
@ -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;
|
||||
|
||||
35
vendor/fonteditor-core/lib/ttf/util/checkSum.js
vendored
35
vendor/fonteditor-core/lib/ttf/util/checkSum.js
vendored
@ -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;
|
||||
|
||||
@ -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)。
|
||||
* 这是 OTF→TTF 转换字形的必经路径(_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;
|
||||
|
||||
@ -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 bezier:offCurve, 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 bezier:2个控制点 + 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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
27
vendor/fonteditor-core/lib/ttf/writer.js
vendored
27
vendor/fonteditor-core/lib/ttf/writer.js
vendored
@ -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;
|
||||
}
|
||||
|
||||
457
基准测试.test.ts
457
基准测试.test.ts
@ -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, "<")}</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)
|
||||
* 之前 OTF→TTF 转换因 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`);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user