web-font/scripts/ssim.ts
崮生(子虚) a0c2b4aafc feat: webfont-sdk Node 模式 + leafer 版基准测试(双方案并存)
- 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)
2026-08-15 15:51:42 +08:00

93 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
}