mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 23:02:27 +08:00
- webfont-sdk: 新增 Node 模式(node-registry),GlobalFonts 注册 + 每 chunk 唯一 family + 逗号链回退 - leafer-x-webfont: fontFamily 链写回(Node 端),scan 链归一化防套娃、rewriteFamily 跳过链形态防抹回 - 修复 pnpm 双实例 bug: @napi-rs/canvas 版本对齐 1.0.6(根与 SDK 同版本共享 .pnpm 条目) - 基准测试重构: 用例表抽取为 基准测试用例.ts 共享,SSIM 计算收敛到 scripts/ssim.ts - 新增 基准测试_leafer.test.ts: @leafer-ui/node + Skia 渲染验证(无浏览器),ttf 输出用例 SSIM=1.0000;woff2 用例仅计时(Skia 不吃 woff2,容器验证由 puppeteer 版守护) - Node 端集成测试 scripts/test-leafer-node.mts(插件链路 SSIM=1.0000)
93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
/**
|
||
* SSIM(结构相似性)计算 —— Wang 2004 实现
|
||
*
|
||
* 基准测试(浏览器渲染 vs 裁剪字体渲染)与 leafer Node 端生图验证共用。
|
||
* 使用 11x11 均匀滑动窗口 + 积分图加速,返回 0~1。
|
||
*
|
||
* 约定:输入为 RGBA 像素数据,width/height 必须与数据长度匹配
|
||
*(width * height * 4 === data.length),否则返回 0。
|
||
*/
|
||
|
||
export function calculateSSIM(a: Uint8Array, b: Uint8Array, width: number, height: number): number {
|
||
if (a.length !== b.length) return 0;
|
||
if (width * height * 4 !== a.length) return 0;
|
||
if (width === 0 || height === 0) return 0;
|
||
|
||
/** 转灰度并提取到独立数组 */
|
||
const N = width * height;
|
||
const grayA = new Float64Array(N);
|
||
const grayB = new Float64Array(N);
|
||
for (let i = 0; i < N; i++) {
|
||
const idx = i * 4;
|
||
grayA[i] = 0.299 * a[idx]! + 0.587 * a[idx + 1]! + 0.114 * a[idx + 2]!;
|
||
grayB[i] = 0.299 * b[idx]! + 0.587 * b[idx + 1]! + 0.114 * b[idx + 2]!;
|
||
}
|
||
|
||
/** 构建积分图: S(x,y) = sum of gray[0..x-1, 0..y-1] */
|
||
const w1 = width + 1;
|
||
const intA = new Float64Array(w1 * (height + 1));
|
||
const intA2 = new Float64Array(w1 * (height + 1));
|
||
const intB = new Float64Array(w1 * (height + 1));
|
||
const intB2 = new Float64Array(w1 * (height + 1));
|
||
const intAB = new Float64Array(w1 * (height + 1));
|
||
|
||
for (let y = 0; y < height; y++) {
|
||
const rowOff = y * width;
|
||
const irowOff = (y + 1) * w1;
|
||
for (let x = 0; x < width; x++) {
|
||
const va = grayA[rowOff + x]!;
|
||
const vb = grayB[rowOff + x]!;
|
||
const ip = irowOff + x + 1;
|
||
intA[ip] = va + intA[ip - 1]! + intA[ip - w1]! - intA[ip - w1 - 1]!;
|
||
intA2[ip] = va * va + intA2[ip - 1]! + intA2[ip - w1]! - intA2[ip - w1 - 1]!;
|
||
intB[ip] = vb + intB[ip - 1]! + intB[ip - w1]! - intB[ip - w1 - 1]!;
|
||
intB2[ip] = vb * vb + intB2[ip - 1]! + intB2[ip - w1]! - intB2[ip - w1 - 1]!;
|
||
intAB[ip] = va * vb + intAB[ip - 1]! + intAB[ip - w1]! - intAB[ip - w1 - 1]!;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从积分图计算矩形区域 [x1, x2) x [y1, y2) 的和
|
||
* 矩形包含 (x2-x1) * (y2-y1) 个像素
|
||
*/
|
||
const rectSum = (img: Float64Array, x1: number, y1: number, x2: number, y2: number) =>
|
||
img[y2 * w1 + x2]! - img[y1 * w1 + x2]! - img[y2 * w1 + x1]! + img[y1 * w1 + x1]!;
|
||
|
||
/** 11x11 窗口, 半径=5 */
|
||
const R = 5;
|
||
/** (0.01 * 255)^2 */
|
||
const C1 = 6.5025;
|
||
/** (0.03 * 255)^2 */
|
||
const C2 = 58.5225;
|
||
|
||
let ssimSum = 0;
|
||
let windowCount = 0;
|
||
|
||
for (let y = R; y < height - R; y++) {
|
||
for (let x = R; x < width - R; x++) {
|
||
const x1 = x - R, x2 = x + R + 1;
|
||
const y1 = y - R, y2 = y + R + 1;
|
||
const n = (2 * R + 1) * (2 * R + 1);
|
||
|
||
const sA = rectSum(intA, x1, y1, x2, y2);
|
||
const sA2 = rectSum(intA2, x1, y1, x2, y2);
|
||
const sB = rectSum(intB, x1, y1, x2, y2);
|
||
const sB2 = rectSum(intB2, x1, y1, x2, y2);
|
||
const sAB = rectSum(intAB, x1, y1, x2, y2);
|
||
|
||
const muA = sA / n;
|
||
const muB = sB / n;
|
||
const sigmaA2 = sA2 / n - muA * muA;
|
||
const sigmaB2 = sB2 / n - muB * muB;
|
||
const sigmaAB = sAB / n - muA * muB;
|
||
|
||
const num = (2 * muA * muB + C1) * (2 * sigmaAB + C2);
|
||
const den = (muA * muA + muB * muB + C1) * (sigmaA2 + sigmaB2 + C2);
|
||
ssimSum += num / den;
|
||
windowCount++;
|
||
}
|
||
}
|
||
|
||
return windowCount > 0 ? ssimSum / windowCount : 0;
|
||
}
|