fix: 重建DOCX视觉一致性门禁

This commit is contained in:
SkyJourney
2026-08-03 10:09:43 +08:00
parent 842c3f1782
commit 28c88faabb
11 changed files with 811 additions and 49 deletions
@@ -166,6 +166,35 @@ describe("PDF 内容感知段落版式门禁", () => {
);
});
it("拒绝正文内容回流到独立封面页", () => {
const baseline = snapshot("baseline", [
[line("独立封面", 200)],
[line("第一章项目概述", 100)],
]);
const candidate = snapshot("candidate", [
[line("独立封面", 200), line("第一章项目概述", 700)],
[],
]);
const pageSemantics = [
{ physicalPageNumber: 1, kind: "cover" as const },
{ physicalPageNumber: 2, kind: "body-first" as const },
];
const [result] = comparePdfEditableParagraphLayouts(
baseline,
candidate,
[expectation("第一章项目概述")],
pageSemantics,
pageSemantics,
);
expect(result?.status).toBe("failed");
expect(result?.issues).toContainEqual(
expect.objectContaining({
code: "COVER_LAYOUT_MISMATCH",
severity: "failure",
}),
);
});
it("忽略 PDF 文本提取器失真的单行宽度但保留诊断值", () => {
const baseline = snapshot("baseline", [
[line("数据治理平台建设项目", 300, 224)],
@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";
import {
createPdfVisualDiffReport,
getBlockingVisualDiffIssues,
renderPdfVisualDiffHtml,
serializePdfVisualDiffJson,
type PdfDocumentSnapshot,
@@ -30,6 +31,40 @@ function raster(color: string): PdfPageRaster {
};
}
function flowRaster(color: string): PdfPageRaster {
const canvas = createCanvas(40, 50);
const context = canvas.getContext("2d");
context.fillStyle = "#ffffff";
context.fillRect(0, 0, 40, 50);
context.fillStyle = color;
context.fillRect(34, 5, 6, 40);
const png = Uint8Array.from(canvas.toBuffer("image/png"));
return {
widthPx: 40,
heightPx: 50,
dpi: 144,
sha256: createHash("sha256").update(png).digest("hex"),
png,
};
}
function blockRaster(color: string): PdfPageRaster {
const canvas = createCanvas(40, 50);
const context = canvas.getContext("2d");
context.fillStyle = "#ffffff";
context.fillRect(0, 0, 40, 50);
context.fillStyle = color;
context.fillRect(8, 12, 20, 10);
const png = Uint8Array.from(canvas.toBuffer("image/png"));
return {
widthPx: 40,
heightPx: 50,
dpi: 144,
sha256: createHash("sha256").update(png).digest("hex"),
png,
};
}
function snapshot(label: string, pageRaster: PdfPageRaster): PdfDocumentSnapshot {
return {
schemaVersion: 1,
@@ -82,7 +117,7 @@ function flowSnapshot(
items: [],
lines: [...lines],
contentText: lines.map((line) => line.normalizedText).join(""),
raster: raster(color),
raster: flowRaster(color),
})),
};
}
@@ -387,7 +422,7 @@ describe("PDF 视觉差异报告", () => {
);
});
it("整段跨页同时发生换行变化时仍保持严格失败", async () => {
it("整段跨页同时发生换行变化时由语义块门禁失败", async () => {
const baseline = flowSnapshot(
"baseline",
[[contentLine("甲", 8)], [contentLine("乙丙", 8)]],
@@ -411,7 +446,7 @@ describe("PDF 视觉差异报告", () => {
expect(report.status).toBe("failed");
expect(report.basic.bodyFlow).toMatchObject({ detected: true, legal: false });
expect(report.pages.every(
(page) => page.rasterComparisonMode === "strict",
(page) => page.rasterComparisonMode === "body-block",
)).toBe(true);
expect(report.issues.map((issue) => issue.code)).toContain(
"TEXT_BLOCK_LINE_COUNT_MISMATCH",
@@ -498,4 +533,78 @@ describe("PDF 视觉差异报告", () => {
"PAGE_NUMBER_VISIBILITY_MISMATCH",
);
});
it("正文整页差异只作诊断,但语义块局部视觉变化必须失败", async () => {
const baseline = flowSnapshot(
"baseline",
[[contentLine("甲", 8)]],
"#111111",
);
const candidate = flowSnapshot(
"candidate",
[[contentLine("甲", 8)]],
"#111111",
);
baseline.pages[0]!.raster = blockRaster("#111111");
candidate.pages[0]!.raster = blockRaster("#cc0000");
const report = await createPdfVisualDiffReport(baseline, candidate, {
expectedEditableText: "甲",
expectedEditableParagraphs: [flowParagraphs[0]!],
pageSemantics: bodySemantics(1),
});
expect(report.status).toBe("failed");
expect(report.pages[0]?.rasterComparisonMode).toBe("body-block");
expect(report.issues.map((issue) => issue.code)).toContain(
"BODY_PAGE_RASTER_DIFFERENCE",
);
expect(report.issues.map((issue) => issue.code)).toContain(
"SEMANTIC_BLOCK_RASTER_MISMATCH",
);
});
it("缺少语义块证据时不得把正文整页栅格自动降级", async () => {
const report = await createPdfVisualDiffReport(
snapshot("baseline", raster("#111111")),
snapshot("candidate", raster("#cc0000")),
{ pageSemantics: bodySemantics(1) },
);
expect(report.status).toBe("failed");
expect(report.pages[0]?.rasterComparisonMode).toBe("strict");
expect(report.issues.map((issue) => issue.code)).toContain(
"CHANGED_PIXEL_RATIO_EXCEEDED",
);
});
it("所有 failure 级视觉问题都必须进入发布阻断集合", () => {
const failure = {
code: "INK_IOU_BELOW_THRESHOLD" as const,
severity: "failure" as const,
message: "墨迹 IoU 未达到门限",
};
const warning = {
code: "BODY_FLOW_RASTER_DIFFERENCE" as const,
severity: "warning" as const,
message: "正文自然分页差异",
};
expect(getBlockingVisualDiffIssues({ issues: [failure, warning] })).toEqual([
failure,
]);
});
it("正文自然分页 warning 不阻断发布", () => {
expect(
getBlockingVisualDiffIssues({
issues: [
{
code: "BODY_FLOW_RASTER_DIFFERENCE",
severity: "warning",
message: "正文自然分页差异",
},
],
}),
).toEqual([]);
});
});