fix: 修复DOCX独立封面视觉门禁

统一 Chromium 与 DOCX 的受限高度封面布局,补齐可编辑边框、装饰边和 Office 页面舍入适配。

按四套独立封面主题、纵横方向及五组页边距执行 40 个 Word/WPS 真实视觉场景,严格失败为 0,并保留正文诊断供 D4 修复。
This commit is contained in:
SkyJourney
2026-08-03 18:15:21 +08:00
parent 9dee7e8605
commit b275c671fc
19 changed files with 785 additions and 82 deletions
+97 -2
View File
@@ -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,