From 65e238cdca1ae895d77c2e0456582623e0b606f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Fri, 24 Jul 2026 10:57:36 +0800 Subject: [PATCH] =?UTF-8?q?perf(cmap):=20subset=20=E6=A8=A1=E5=BC=8F=20for?= =?UTF-8?q?mat4=20=E5=BB=B6=E8=BF=9F=E8=A7=A3=E6=9E=90=EF=BC=8C=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=204=C3=97segCount=20=E6=95=B0=E7=BB=84=E6=9E=84?= =?UTF-8?q?=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit subset 模式下 format4 不再急切构建 endCode/startCode/idDelta/idRangeOffset 共 4 个 segCount 项数组,只记录各段在 view 的偏移;lookupFormat4 二分时 按需从 view 读(mid*2 偏移)。原 subset 模式仍执行 4×segCount 次 getUint16 + 4 个数组分配,得意黑 segCount=3929 → 1.5 万次读,占 readSubTable 0.101ms。 延迟后 view 读次数 = O(S × log(segCount)),远小于 4×segCount。 lookupFormat4 新增 _lazySegs 分支,非延迟路径(全量模式)保持急切数组不变。 idDelta 仍用 getUint16 读(与原急切模式一致),依赖 % 0x10000 处理符号。 字节逐字节一致(令东/思源/初夏/得意黑/otf 全 cmp 通过),SSIM 全程不变。 cmap: 初夏 0.075→0.009ms(-88%),得意黑→0.004ms(-94%),思源 0.063→0.014(-78%)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lib/ttf/table/cmap/parse.js | 88 ++++++++++++------- .../lib/ttf/util/readWindowsAllCodes.js | 68 ++++++++++---- 2 files changed, 106 insertions(+), 50 deletions(-) diff --git a/vendor/fonteditor-core/lib/ttf/table/cmap/parse.js b/vendor/fonteditor-core/lib/ttf/table/cmap/parse.js index 57b891a..bf6478c 100644 --- a/vendor/fonteditor-core/lib/ttf/table/cmap/parse.js +++ b/vendor/fonteditor-core/lib/ttf/table/cmap/parse.js @@ -88,44 +88,64 @@ function readSubTable(reader, ttf, subTable, cmapOffset) { format4.entrySelector = view.getUint16(vOffset, false); vOffset += 2; format4.rangeShift = view.getUint16(vOffset, false); vOffset += 2; var segCount = format4.segCountX2 / 2; - - var endCode = new Array(segCount); - for (var e = 0; e < segCount; e++) { - endCode[e] = view.getUint16(vOffset, false); - vOffset += 2; - } - format4.endCode = endCode; - format4.reservedPad = view.getUint16(vOffset, false); vOffset += 2; - - var startCode = new Array(segCount); - for (var s = 0; s < segCount; s++) { - startCode[s] = view.getUint16(vOffset, false); - vOffset += 2; - } - format4.startCode = startCode; - - var idDelta = new Array(segCount); - for (var d = 0; d < segCount; d++) { - idDelta[d] = view.getUint16(vOffset, false); - vOffset += 2; - } - format4.idDelta = idDelta; - format4.idRangeOffsetOffset = vOffset - view.byteOffset; - - var idRangeOffset = new Array(segCount); - for (var r = 0; r < segCount; r++) { - idRangeOffset[r] = view.getUint16(vOffset, false); - vOffset += 2; - } - format4.idRangeOffset = idRangeOffset; format4.segCount = segCount; - /* 优化101: subset 模式下跳过 glyphIdArray 解析,直接从 view 按需读取 */ + /* 优化(format4 subset 延迟解析):subset 模式下不展开 4 个 segCount 数组, + * 只记录各段在 view 的偏移,lookupFormat4 二分时按需从 view 读。 + * 原 subset 模式仍构建 endCode/startCode/idDelta/idRangeOffset 共 4×segCount 次 getUint16, + * 得意黑 segCount=3929 → 1.5 万次读 + 4 个数组分配,占 readSubTable 0.101ms。 + * subset 仅查找少数 cp,延迟后 view 读次数 = O(S × log(segCount)),远小于 4×segCount。 + * 非 subset 模式仍需全量遍历 segments,保持急切构建数组。 */ var isSubset4 = ttf.readOptions && ttf.readOptions.subset; if (isSubset4) { - format4.glyphIdArrayOffset = vOffset - view.byteOffset; + /* endCode 段起始(相对 view.byteOffset),紧接 14B header */ + var endCodeOff = vOffset; + /* startCode 段 = endCode 段后 + reservedPad(2) */ + var startCodeOff = endCodeOff + segCount * 2 + 2; + var idDeltaOff = startCodeOff + segCount * 2; + var idRangeOffsetOff = idDeltaOff + segCount * 2; + var glyphIdArrayOff = idRangeOffsetOff + segCount * 2; format4._cmapView = view; + format4._lazySegs = true; + format4._endCodeOff = endCodeOff; + format4._startCodeOff = startCodeOff; + format4._idDeltaOff = idDeltaOff; + format4._idRangeOffsetOff = idRangeOffsetOff; + format4.idRangeOffsetOffset = idRangeOffsetOff - view.byteOffset; + format4.glyphIdArrayOffset = glyphIdArrayOff - view.byteOffset; + /* 优化177: 预计算 glyphIdArrayIndexOffset,消除 lookupFormat4 中的重复除法 */ + format4.glyphIdArrayIndexOffset = (format4.glyphIdArrayOffset - format4.idRangeOffsetOffset) / 2; } else { + var endCode = new Array(segCount); + for (var e = 0; e < segCount; e++) { + endCode[e] = view.getUint16(vOffset, false); + vOffset += 2; + } + format4.endCode = endCode; + format4.reservedPad = view.getUint16(vOffset, false); vOffset += 2; + + var startCode = new Array(segCount); + for (var s = 0; s < segCount; s++) { + startCode[s] = view.getUint16(vOffset, false); + vOffset += 2; + } + format4.startCode = startCode; + + var idDelta = new Array(segCount); + for (var d = 0; d < segCount; d++) { + idDelta[d] = view.getUint16(vOffset, false); + vOffset += 2; + } + format4.idDelta = idDelta; + format4.idRangeOffsetOffset = vOffset - view.byteOffset; + + var idRangeOffset = new Array(segCount); + for (var r = 0; r < segCount; r++) { + idRangeOffset[r] = view.getUint16(vOffset, false); + vOffset += 2; + } + format4.idRangeOffset = idRangeOffset; + var glyphCount4 = (format4.length - (vOffset - view.byteOffset - startOffset)) / 2; format4.glyphIdArrayOffset = vOffset - view.byteOffset; @@ -135,9 +155,9 @@ function readSubTable(reader, ttf, subTable, cmapOffset) { vOffset += 2; } format4.glyphIdArray = glyphIdArray4; + /* 优化177: 预计算 glyphIdArrayIndexOffset */ + format4.glyphIdArrayIndexOffset = (format4.glyphIdArrayOffset - format4.idRangeOffsetOffset) / 2; } - /* 优化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; diff --git a/vendor/fonteditor-core/lib/ttf/util/readWindowsAllCodes.js b/vendor/fonteditor-core/lib/ttf/util/readWindowsAllCodes.js index 178ec9f..18cfd52 100644 --- a/vendor/fonteditor-core/lib/ttf/util/readWindowsAllCodes.js +++ b/vendor/fonteditor-core/lib/ttf/util/readWindowsAllCodes.js @@ -57,38 +57,74 @@ function lookupFormat12(format12, unicode) { /** * 优化114: format4 二分查找 segment,替代线性扫描 * 优化293: 接受预计算的 graphIdArrayIndexOffset 参数,避免每次调用重复计算 + * 优化(subset 延迟):_lazySegs 模式下各段未展开为数组,二分时按需从 view 读取 */ function lookupFormat4(format4, unicode, _graphIdArrayIndexOffset) { + var segCount = format4.segCount || (format4.segCountX2 / 2); + + /* 延迟模式:各段在 view 中连续,按 mid*2 偏移读取。命中后再读 idDelta/idRangeOffset */ + if (format4._lazySegs) { + var view = format4._cmapView; + var endOff = format4._endCodeOff; + var startOff = format4._startCodeOff; + var deltaOff = format4._idDeltaOff; + var rangeOff = format4._idRangeOffsetOff; + var lo = 0, hi = segCount - 1; + while (lo <= hi) { + var mid = (lo + hi) >> 1; + var m2 = mid * 2; + var sCode = view.getUint16(startOff + m2, false); + if (unicode < sCode) { + hi = mid - 1; + } else if (unicode > view.getUint16(endOff + m2, false)) { + lo = mid + 1; + } else { + var idR = view.getUint16(rangeOff + m2, false); + if (idR === 0) { + /* idDelta 用 getUint16 读(与原急切模式一致),依赖 % 0x10000 处理符号 */ + return (unicode + view.getUint16(deltaOff + m2, false)) % 0x10000; + } + var graphIdArrayIndexOffset = _graphIdArrayIndexOffset != null ? _graphIdArrayIndexOffset : (format4.glyphIdArrayOffset - format4.idRangeOffsetOffset) / 2; + var index = mid + (idR >> 1) + (unicode - sCode) - graphIdArrayIndexOffset; + var graphId = view.getUint16(format4.glyphIdArrayOffset + index * 2, false); + if (graphId !== 0) { + return (graphId + view.getUint16(deltaOff + m2, false)) % 0x10000; + } + return 0; + } + } + return -1; + } + var startCode = format4.startCode; var endCode = format4.endCode; var idDelta = format4.idDelta; var idRangeOffset = format4.idRangeOffset; - var segCount = format4.segCount || (format4.segCountX2 / 2); - var lo = 0, hi = segCount - 1; - while (lo <= hi) { - var mid = (lo + hi) >> 1; - if (unicode < startCode[mid]) { - hi = mid - 1; - } else if (unicode > endCode[mid]) { - lo = mid + 1; + var lo2 = 0, hi2 = segCount - 1; + while (lo2 <= hi2) { + var mid2 = (lo2 + hi2) >> 1; + if (unicode < startCode[mid2]) { + hi2 = mid2 - 1; + } else if (unicode > endCode[mid2]) { + lo2 = mid2 + 1; } else { - var i = mid; + var i = mid2; if (idRangeOffset[i] === 0) { return (unicode + idDelta[i]) % 0x10000; } - var graphIdArrayIndexOffset = _graphIdArrayIndexOffset != null ? _graphIdArrayIndexOffset : (format4.glyphIdArrayOffset - format4.idRangeOffsetOffset) / 2; - var index = i + (idRangeOffset[i] >> 1) + (unicode - startCode[i]) - graphIdArrayIndexOffset; - var graphId; + var graphIdArrayIndexOffset2 = _graphIdArrayIndexOffset != null ? _graphIdArrayIndexOffset : (format4.glyphIdArrayOffset - format4.idRangeOffsetOffset) / 2; + var index2 = i + (idRangeOffset[i] >> 1) + (unicode - startCode[i]) - graphIdArrayIndexOffset2; + var graphId2; if (format4.glyphIdArray) { - graphId = format4.glyphIdArray[index]; + graphId2 = format4.glyphIdArray[index2]; } else if (format4._cmapView) { - graphId = format4._cmapView.getUint16(format4.glyphIdArrayOffset + index * 2, false); + graphId2 = format4._cmapView.getUint16(format4.glyphIdArrayOffset + index2 * 2, false); } else { return 0; } - if (graphId !== 0) { - return (graphId + idDelta[i]) % 0x10000; + if (graphId2 !== 0) { + return (graphId2 + idDelta[i]) % 0x10000; } return 0; }