fix: 重建DOCX视觉一致性门禁
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { comparePdfSnapshotsBasic, DEFAULT_VISUAL_DIFF_THRESHOLDS } from "./compare.js";
|
||||
import { comparePageRasters } from "./raster.js";
|
||||
import { comparePdfPageSemantics } from "./page-semantics.js";
|
||||
import { comparePdfSemanticBlockVisuals } from "./semantic-block.js";
|
||||
import type {
|
||||
CreatePdfVisualDiffOptions,
|
||||
PdfDocumentSnapshot,
|
||||
@@ -21,6 +22,12 @@ function resolveStatus(
|
||||
return issues.length > 0 ? "warning" : "passed";
|
||||
}
|
||||
|
||||
export function getBlockingVisualDiffIssues(
|
||||
report: Pick<PdfVisualDiffReport, "issues">,
|
||||
): VisualDiffIssue[] {
|
||||
return report.issues.filter((issue) => issue.severity === "failure");
|
||||
}
|
||||
|
||||
function rasterIssues(
|
||||
pair: Extract<PdfPagePair, { status: "paired" }>,
|
||||
metrics: NonNullable<PdfVisualPageComparison["metrics"]>,
|
||||
@@ -91,7 +98,7 @@ async function compareVisualPage(
|
||||
baseline: PdfDocumentSnapshot,
|
||||
candidate: PdfDocumentSnapshot,
|
||||
thresholds: VisualDiffThresholds,
|
||||
rasterComparisonMode: "strict" | "body-flow" = "strict",
|
||||
rasterComparisonMode: "strict" | "body-flow" | "body-block" = "strict",
|
||||
): Promise<PdfVisualPageComparison> {
|
||||
if (pair.status !== "paired") {
|
||||
const page =
|
||||
@@ -101,7 +108,7 @@ async function compareVisualPage(
|
||||
return {
|
||||
pair,
|
||||
rasterComparisonMode,
|
||||
status: rasterComparisonMode === "body-flow" ? "warning" : "failed",
|
||||
status: rasterComparisonMode === "strict" ? "failed" : "warning",
|
||||
...(page?.raster
|
||||
? {
|
||||
unpairedPng: page.raster.png,
|
||||
@@ -109,12 +116,18 @@ async function compareVisualPage(
|
||||
}
|
||||
: {}),
|
||||
issues:
|
||||
rasterComparisonMode === "body-flow"
|
||||
rasterComparisonMode !== "strict"
|
||||
? [
|
||||
{
|
||||
code: "BODY_FLOW_RASTER_DIFFERENCE",
|
||||
code:
|
||||
rasterComparisonMode === "body-flow"
|
||||
? "BODY_FLOW_RASTER_DIFFERENCE"
|
||||
: "BODY_PAGE_RASTER_DIFFERENCE",
|
||||
severity: "warning",
|
||||
message: "正文自然分页产生未配对物理页,保留页面快照供审查",
|
||||
message:
|
||||
rasterComparisonMode === "body-flow"
|
||||
? "正文自然分页产生未配对物理页,保留页面快照供审查"
|
||||
: "正文物理页未配对,整页栅格仅作为诊断,最终由语义块门禁判定",
|
||||
...(pair.status === "baseline-only"
|
||||
? { baselinePageNumber: pair.baselinePageNumber }
|
||||
: { candidatePageNumber: pair.candidatePageNumber }),
|
||||
@@ -128,14 +141,15 @@ async function compareVisualPage(
|
||||
if (!baselinePage?.raster || !candidatePage?.raster) {
|
||||
const issue: VisualDiffIssue = {
|
||||
code: "RASTER_MISSING",
|
||||
severity: "failure",
|
||||
severity: rasterComparisonMode === "strict" ? "failure" : "warning",
|
||||
message: `第 ${pair.baselinePageNumber} 页缺少栅格快照`,
|
||||
baselinePageNumber: pair.baselinePageNumber,
|
||||
candidatePageNumber: pair.candidatePageNumber,
|
||||
};
|
||||
return {
|
||||
pair,
|
||||
status: "failed",
|
||||
rasterComparisonMode,
|
||||
status: rasterComparisonMode === "strict" ? "failed" : "warning",
|
||||
issues: [issue],
|
||||
};
|
||||
}
|
||||
@@ -161,12 +175,18 @@ async function compareVisualPage(
|
||||
);
|
||||
const strictIssues = rasterIssues(pair, result.metrics, thresholds);
|
||||
const issues =
|
||||
rasterComparisonMode === "body-flow" && strictIssues.length > 0
|
||||
rasterComparisonMode !== "strict" && strictIssues.length > 0
|
||||
? [
|
||||
{
|
||||
code: "BODY_FLOW_RASTER_DIFFERENCE" as const,
|
||||
code:
|
||||
rasterComparisonMode === "body-flow"
|
||||
? ("BODY_FLOW_RASTER_DIFFERENCE" as const)
|
||||
: ("BODY_PAGE_RASTER_DIFFERENCE" as const),
|
||||
severity: "warning" as const,
|
||||
message: `第 ${pair.baselinePageNumber} 页栅格差异来自已验证的正文自然分页流动`,
|
||||
message:
|
||||
rasterComparisonMode === "body-flow"
|
||||
? `第 ${pair.baselinePageNumber} 页栅格差异来自已验证的正文自然分页流动`
|
||||
: `第 ${pair.baselinePageNumber} 页为正文页,整页栅格差异仅作诊断,最终由语义块门禁判定`,
|
||||
baselinePageNumber: pair.baselinePageNumber,
|
||||
candidatePageNumber: pair.candidatePageNumber,
|
||||
details: {
|
||||
@@ -218,6 +238,26 @@ export async function createPdfVisualDiffReport(
|
||||
? {}
|
||||
: { expectedEditableParagraphs: options.expectedEditableParagraphs }),
|
||||
});
|
||||
if (basic.paragraphLayouts) {
|
||||
const semanticBlockVisuals = await comparePdfSemanticBlockVisuals(
|
||||
baseline,
|
||||
candidate,
|
||||
basic.paragraphLayouts,
|
||||
thresholds,
|
||||
);
|
||||
basic.semanticBlockVisuals = semanticBlockVisuals;
|
||||
basic.issues.push(
|
||||
...semanticBlockVisuals.flatMap((comparison) => comparison.issues),
|
||||
);
|
||||
basic.status = resolveStatus(basic.issues);
|
||||
if (basic.bodyFlow) {
|
||||
basic.bodyFlow.legal =
|
||||
basic.bodyFlow.legal &&
|
||||
semanticBlockVisuals.every(
|
||||
(comparison) => comparison.status !== "failed",
|
||||
);
|
||||
}
|
||||
}
|
||||
const pages: PdfVisualPageComparison[] = [];
|
||||
for (const pair of basic.pagePairs) {
|
||||
const baselineSemanticExpectation =
|
||||
@@ -244,12 +284,21 @@ export async function createPdfVisualDiffReport(
|
||||
pair.candidatePageNumber,
|
||||
))),
|
||||
);
|
||||
const isSemanticBodyPage =
|
||||
baselineSemanticExpectation?.kind !== "cover" &&
|
||||
candidateSemanticExpectation?.kind !== "cover" &&
|
||||
Boolean(baselineSemanticExpectation || candidateSemanticExpectation) &&
|
||||
(basic.semanticBlockVisuals?.length ?? 0) > 0;
|
||||
const page = await compareVisualPage(
|
||||
pair,
|
||||
baseline,
|
||||
candidate,
|
||||
thresholds,
|
||||
flowAffected ? "body-flow" : "strict",
|
||||
flowAffected
|
||||
? "body-flow"
|
||||
: isSemanticBodyPage
|
||||
? "body-block"
|
||||
: "strict",
|
||||
);
|
||||
const primarySemanticExpectation =
|
||||
baselineSemanticExpectation ?? candidateSemanticExpectation;
|
||||
@@ -366,6 +415,8 @@ function pageTitle(page: PdfVisualPageComparison): string {
|
||||
const mode =
|
||||
page.rasterComparisonMode === "body-flow"
|
||||
? "(正文流动比较)"
|
||||
: page.rasterComparisonMode === "body-block"
|
||||
? "(正文整页诊断)"
|
||||
: "(严格比较)";
|
||||
if (page.pair.status === "paired") {
|
||||
return `基线第 ${page.pair.baselinePageNumber} 页 ↔ 候选第 ${page.pair.candidatePageNumber} 页${mode}`;
|
||||
@@ -376,6 +427,27 @@ function pageTitle(page: PdfVisualPageComparison): string {
|
||||
return `候选第 ${page.pair.candidatePageNumber} 页为溢出页${mode}`;
|
||||
}
|
||||
|
||||
function renderSemanticBlockVisuals(report: PdfVisualDiffReport): string {
|
||||
const comparisons = report.basic.semanticBlockVisuals;
|
||||
if (!comparisons) {
|
||||
return "";
|
||||
}
|
||||
const failures = comparisons.filter(
|
||||
(comparison) => comparison.status === "failed",
|
||||
);
|
||||
const rows = (failures.length > 0 ? failures : comparisons.slice(0, 8))
|
||||
.map((comparison) => {
|
||||
const expectation = comparison.expectation;
|
||||
return `<tr><td>${expectation.index + 1}</td><td>${escapeHtml(expectation.section)}</td><td>${escapeHtml(expectation.role)}</td><td>${escapeHtml(comparison.baselineFontFamilies.join(", ") || "未识别")}</td><td>${escapeHtml(comparison.candidateFontFamilies.join(", ") || "未识别")}</td><td>${comparison.lines.length}</td><td>${escapeHtml(comparison.issues.map((issue) => issue.code).join(", ") || "通过")}</td></tr>`;
|
||||
})
|
||||
.join("");
|
||||
return `<div class="semantic-block-visuals">
|
||||
<h2>正文语义块局部视觉 <span class="status ${failures.length > 0 ? "failed" : "passed"}">${failures.length > 0 ? "failed" : "passed"}</span></h2>
|
||||
<p>正文语义块 ${comparisons.length} 个,失败 ${failures.length} 个;块按内容顺序匹配,局部栅格参与硬门禁,物理页号不参与。PDF.js 字体分类仅作诊断,真实字体解析由独立字体门禁判定。</p>
|
||||
<table><thead><tr><th>块</th><th>分节</th><th>角色</th><th>基线字体分类</th><th>候选字体分类</th><th>局部行</th><th>结果</th></tr></thead><tbody>${rows}</tbody></table>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderParagraphLayouts(report: PdfVisualDiffReport): string {
|
||||
const paragraphs = report.basic.paragraphLayouts;
|
||||
if (!paragraphs) {
|
||||
@@ -451,6 +523,7 @@ export function renderPdfVisualDiffHtml(
|
||||
<p><strong>生成时间:</strong>${escapeHtml(report.generatedAt)} <strong>正文相似度:</strong>${percentage(report.basic.contentSimilarity)}</p>
|
||||
${editableMetrics}
|
||||
${renderParagraphLayouts(report)}
|
||||
${renderSemanticBlockVisuals(report)}
|
||||
<table><thead><tr><th>级别</th><th>代码</th><th>说明</th></tr></thead><tbody>${issueRows}</tbody></table>
|
||||
</section>
|
||||
${pages}
|
||||
|
||||
Reference in New Issue
Block a user