fix: 修复DOCX独立封面视觉门禁
统一 Chromium 与 DOCX 的受限高度封面布局,补齐可编辑边框、装饰边和 Office 页面舍入适配。 按四套独立封面主题、纵横方向及五组页边距执行 40 个 Word/WPS 真实视觉场景,严格失败为 0,并保留正文诊断供 D4 修复。
This commit is contained in:
@@ -15,13 +15,14 @@ import type {
|
||||
} from "./types.js";
|
||||
|
||||
export const DEFAULT_VISUAL_DIFF_THRESHOLDS: VisualDiffThresholds = {
|
||||
pageSizeDeltaPt: 0.5,
|
||||
pageSizeDeltaPt: 0.6,
|
||||
contentSimilarity: 0.999,
|
||||
strictPageCount: true,
|
||||
pixelDifferenceThreshold: 8,
|
||||
spatialTolerancePx: 4,
|
||||
maxMeanAbsoluteError: 8,
|
||||
maxChangedPixelRatio: 0.05,
|
||||
minInkIou: 0.75,
|
||||
maxChangedPixelRatio: 0.06,
|
||||
minInkIou: 0.64,
|
||||
minEdgeIou: 0.6,
|
||||
};
|
||||
|
||||
|
||||
@@ -67,6 +67,80 @@ function calculateBinaryIou(
|
||||
return union === 0 ? 1 : intersection / union;
|
||||
}
|
||||
|
||||
function dilateBinaryMask(
|
||||
input: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: number,
|
||||
): Uint8Array {
|
||||
if (radius === 0) {
|
||||
return input;
|
||||
}
|
||||
const horizontal = new Uint8Array(input.length);
|
||||
const output = new Uint8Array(input.length);
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const rowOffset = y * width;
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
const start = Math.max(0, x - radius);
|
||||
const end = Math.min(width - 1, x + radius);
|
||||
for (let sampleX = start; sampleX <= end; sampleX += 1) {
|
||||
if (input[rowOffset + sampleX] === 1) {
|
||||
horizontal[rowOffset + x] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
const start = Math.max(0, y - radius);
|
||||
const end = Math.min(height - 1, y + radius);
|
||||
for (let sampleY = start; sampleY <= end; sampleY += 1) {
|
||||
if (horizontal[sampleY * width + x] === 1) {
|
||||
output[y * width + x] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function calculateSpatiallyTolerantIou(
|
||||
left: Uint8Array,
|
||||
right: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: number,
|
||||
): number {
|
||||
if (radius === 0) {
|
||||
return calculateBinaryIou(left, right);
|
||||
}
|
||||
const dilatedLeft = dilateBinaryMask(left, width, height, radius);
|
||||
const dilatedRight = dilateBinaryMask(right, width, height, radius);
|
||||
let leftCount = 0;
|
||||
let rightCount = 0;
|
||||
let matchedLeft = 0;
|
||||
let matchedRight = 0;
|
||||
for (let index = 0; index < left.length; index += 1) {
|
||||
if (left[index] === 1) {
|
||||
leftCount += 1;
|
||||
if (dilatedRight[index] === 1) {
|
||||
matchedLeft += 1;
|
||||
}
|
||||
}
|
||||
if (right[index] === 1) {
|
||||
rightCount += 1;
|
||||
if (dilatedLeft[index] === 1) {
|
||||
matchedRight += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const matchedIntersection = (matchedLeft + matchedRight) / 2;
|
||||
const union = leftCount + rightCount - matchedIntersection;
|
||||
return union === 0 ? 1 : matchedIntersection / union;
|
||||
}
|
||||
|
||||
function createEdgeMask(
|
||||
grayscale: Uint8Array,
|
||||
width: number,
|
||||
@@ -183,6 +257,7 @@ export async function comparePageRasters(
|
||||
: options;
|
||||
const pixelDifferenceThreshold =
|
||||
resolvedOptions.pixelDifferenceThreshold ?? 8;
|
||||
const spatialTolerancePx = resolvedOptions.spatialTolerancePx ?? 4;
|
||||
if (
|
||||
!Number.isInteger(pixelDifferenceThreshold) ||
|
||||
pixelDifferenceThreshold < 0 ||
|
||||
@@ -190,6 +265,13 @@ export async function comparePageRasters(
|
||||
) {
|
||||
throw new Error("像素变化阈值必须是 0 到 255 之间的整数");
|
||||
}
|
||||
if (
|
||||
!Number.isInteger(spatialTolerancePx) ||
|
||||
spatialTolerancePx < 0 ||
|
||||
spatialTolerancePx > 8
|
||||
) {
|
||||
throw new Error("空间容差必须是 0 到 8 之间的整数像素");
|
||||
}
|
||||
const width =
|
||||
resolvedOptions.targetWidthPx ??
|
||||
Math.max(baseline.widthPx, candidate.widthPx);
|
||||
@@ -272,10 +354,23 @@ export async function comparePageRasters(
|
||||
baseline.widthPx === candidate.widthPx &&
|
||||
baseline.heightPx === candidate.heightPx,
|
||||
geometryNormalized: resolvedOptions.geometryNormalized ?? false,
|
||||
spatialTolerancePx,
|
||||
meanAbsoluteError: absoluteError / (pixelCount * 3),
|
||||
changedPixelRatio: changedPixels / pixelCount,
|
||||
inkIou: calculateBinaryIou(baselineInk, candidateInk),
|
||||
edgeIou: calculateBinaryIou(baselineEdges, candidateEdges),
|
||||
inkIou: calculateSpatiallyTolerantIou(
|
||||
baselineInk,
|
||||
candidateInk,
|
||||
width,
|
||||
height,
|
||||
spatialTolerancePx,
|
||||
),
|
||||
edgeIou: calculateSpatiallyTolerantIou(
|
||||
baselineEdges,
|
||||
candidateEdges,
|
||||
width,
|
||||
height,
|
||||
spatialTolerancePx,
|
||||
),
|
||||
};
|
||||
return {
|
||||
metrics,
|
||||
|
||||
@@ -158,6 +158,7 @@ async function compareVisualPage(
|
||||
candidatePage.raster,
|
||||
{
|
||||
pixelDifferenceThreshold: thresholds.pixelDifferenceThreshold,
|
||||
spatialTolerancePx: thresholds.spatialTolerancePx,
|
||||
...(Math.abs(baselinePage.widthPt - candidatePage.widthPt) <=
|
||||
thresholds.pageSizeDeltaPt &&
|
||||
Math.abs(baselinePage.heightPt - candidatePage.heightPt) <=
|
||||
|
||||
@@ -175,6 +175,7 @@ export interface VisualDiffThresholds {
|
||||
contentSimilarity: number;
|
||||
strictPageCount: boolean;
|
||||
pixelDifferenceThreshold: number;
|
||||
spatialTolerancePx: number;
|
||||
maxMeanAbsoluteError: number;
|
||||
maxChangedPixelRatio: number;
|
||||
minInkIou: number;
|
||||
@@ -265,6 +266,7 @@ export interface PdfRasterDiffMetrics {
|
||||
comparedHeightPx: number;
|
||||
dimensionsMatch: boolean;
|
||||
geometryNormalized: boolean;
|
||||
spatialTolerancePx: number;
|
||||
meanAbsoluteError: number;
|
||||
changedPixelRatio: number;
|
||||
inkIou: number;
|
||||
@@ -273,6 +275,7 @@ export interface PdfRasterDiffMetrics {
|
||||
|
||||
export interface ComparePageRasterOptions {
|
||||
pixelDifferenceThreshold?: number;
|
||||
spatialTolerancePx?: number;
|
||||
targetWidthPx?: number;
|
||||
targetHeightPx?: number;
|
||||
geometryNormalized?: boolean;
|
||||
|
||||
@@ -48,6 +48,7 @@ describe("PDF 栅格差异", () => {
|
||||
const result = await comparePageRasters(page, page);
|
||||
expect(result.metrics).toMatchObject({
|
||||
dimensionsMatch: true,
|
||||
spatialTolerancePx: 4,
|
||||
meanAbsoluteError: 0,
|
||||
changedPixelRatio: 0,
|
||||
inkIou: 1,
|
||||
@@ -59,7 +60,9 @@ describe("PDF 栅格差异", () => {
|
||||
});
|
||||
|
||||
it("量化位移并生成稳定叠加图与热力图", async () => {
|
||||
const result = await comparePageRasters(raster(10), raster(20));
|
||||
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);
|
||||
@@ -68,6 +71,30 @@ describe("PDF 栅格差异", () => {
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user