import { createHash } from "node:crypto"; import { createCanvas } from "@napi-rs/canvas"; import { describe, expect, it } from "vitest"; import { comparePageRasters, type PdfPageRaster, } from "../src/index.js"; function raster(rectangleX: number): PdfPageRaster { const canvas = createCanvas(80, 100); const context = canvas.getContext("2d"); context.fillStyle = "#ffffff"; context.fillRect(0, 0, 80, 100); context.fillStyle = "#111111"; context.fillRect(rectangleX, 20, 20, 40); const png = Uint8Array.from(canvas.toBuffer("image/png")); return { widthPx: 80, heightPx: 100, dpi: 144, sha256: createHash("sha256").update(png).digest("hex"), png, }; } function differentlySizedRaster(width: number, height: number): PdfPageRaster { const canvas = createCanvas(width, height); const context = canvas.getContext("2d"); context.fillStyle = "#ffffff"; context.fillRect(0, 0, width, height); context.fillStyle = "#111111"; context.fillRect(width * 0.25, height * 0.2, width * 0.25, height * 0.4); const png = Uint8Array.from(canvas.toBuffer("image/png")); return { widthPx: width, heightPx: height, dpi: 144, sha256: createHash("sha256").update(png).digest("hex"), png, }; } describe("PDF 栅格差异", () => { it("相同页面的全部指标为无差异", async () => { const page = raster(10); const result = await comparePageRasters(page, page); expect(result.metrics).toMatchObject({ dimensionsMatch: true, spatialTolerancePx: 4, meanAbsoluteError: 0, changedPixelRatio: 0, inkIou: 1, edgeIou: 1, }); expect(result.artifacts.overlayPng.subarray(0, 8)).toEqual( new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), ); }); it("量化位移并生成稳定叠加图与热力图", async () => { const result = await comparePageRasters(raster(10), raster(20), { spatialTolerancePx: 0, }); expect(result.metrics.meanAbsoluteError).toBeGreaterThan(10); expect(result.metrics.changedPixelRatio).toBeCloseTo(0.1, 2); expect(result.metrics.inkIou).toBeCloseTo(1 / 3, 2); expect(result.metrics.edgeIou).toBeLessThan(0.4); expect(result.artifacts.overlaySha256).toHaveLength(64); expect(result.artifacts.heatmapSha256).toHaveLength(64); }); it("只容忍三个像素以内的跨引擎栅格偏移", async () => { const near = await comparePageRasters(raster(10), raster(13)); const far = await comparePageRasters(raster(10), raster(17)); expect(near.metrics.inkIou).toBe(1); expect(near.metrics.edgeIou).toBe(1); expect(far.metrics.inkIou).toBeLessThan(0.75); expect(far.metrics.edgeIou).toBeLessThan(0.6); }); it("空间容差不改变原始像素热力图", async () => { const baseline = raster(10); const candidate = raster(12); const tolerant = await comparePageRasters(baseline, candidate); const exact = await comparePageRasters(baseline, candidate, { spatialTolerancePx: 0, }); expect(tolerant.metrics.inkIou).toBeGreaterThan(exact.metrics.inkIou); expect(tolerant.artifacts.heatmapSha256).toBe( exact.artifacts.heatmapSha256, ); }); it("可按物理页面基线规范化一像素的栅格舍入差", async () => { const result = await comparePageRasters( differentlySizedRaster(80, 100), differentlySizedRaster(81, 101), { targetWidthPx: 80, targetHeightPx: 100, geometryNormalized: true, }, ); expect(result.metrics).toMatchObject({ baselineWidthPx: 80, candidateWidthPx: 81, comparedWidthPx: 80, dimensionsMatch: false, geometryNormalized: true, }); expect(result.metrics.meanAbsoluteError).toBeLessThan(2); }); });