feat: 完成 DOCX R4 元素级视觉门禁
建立 Chromium、Word 与 WPS 的可编辑内容、段落几何、页眉页脚、封面、媒体和逐页视觉比较链路。 支持封面独立分节、正文页码重启、主题页边距与横向纸张,并将自然分页差异降为诊断项。 修正代码块内边距和折叠表格单元格边框映射,14 套主题生产矩阵与六组布局视觉矩阵通过。
This commit is contained in:
@@ -4,6 +4,7 @@ import { createCanvas, loadImage } from "@napi-rs/canvas";
|
||||
|
||||
import type {
|
||||
PdfPageRaster,
|
||||
ComparePageRasterOptions,
|
||||
PdfRasterDiffArtifacts,
|
||||
PdfRasterDiffMetrics,
|
||||
PdfRasterDiffResult,
|
||||
@@ -29,7 +30,7 @@ async function decodeRaster(
|
||||
const context = canvas.getContext("2d");
|
||||
context.fillStyle = "#ffffff";
|
||||
context.fillRect(0, 0, width, height);
|
||||
context.drawImage(image, 0, 0);
|
||||
context.drawImage(image, 0, 0, width, height);
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
@@ -107,8 +108,6 @@ function encodeRgbaPng(
|
||||
}
|
||||
|
||||
function createArtifacts(
|
||||
baseline: PdfPageRaster,
|
||||
candidate: PdfPageRaster,
|
||||
baselineGray: Uint8Array,
|
||||
candidateGray: Uint8Array,
|
||||
pixelDifference: Uint8Array,
|
||||
@@ -142,23 +141,48 @@ function createArtifacts(
|
||||
}
|
||||
const overlayPng = encodeRgbaPng(overlay, width, height);
|
||||
const heatmapPng = encodeRgbaPng(heatmap, width, height);
|
||||
const baselinePng = encodeGrayscalePng(baselineGray, width, height);
|
||||
const candidatePng = encodeGrayscalePng(candidateGray, width, height);
|
||||
return {
|
||||
baselinePng: baseline.png,
|
||||
candidatePng: candidate.png,
|
||||
baselinePng,
|
||||
candidatePng,
|
||||
overlayPng,
|
||||
heatmapPng,
|
||||
baselineSha256: baseline.sha256,
|
||||
candidateSha256: candidate.sha256,
|
||||
baselineSha256: sha256(baselinePng),
|
||||
candidateSha256: sha256(candidatePng),
|
||||
overlaySha256: sha256(overlayPng),
|
||||
heatmapSha256: sha256(heatmapPng),
|
||||
};
|
||||
}
|
||||
|
||||
function encodeGrayscalePng(
|
||||
grayscale: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
): Uint8Array {
|
||||
const rgba = new Uint8ClampedArray(width * height * 4);
|
||||
for (let index = 0; index < grayscale.length; index += 1) {
|
||||
const value = grayscale[index] ?? 255;
|
||||
const offset = index * 4;
|
||||
rgba[offset] = value;
|
||||
rgba[offset + 1] = value;
|
||||
rgba[offset + 2] = value;
|
||||
rgba[offset + 3] = 255;
|
||||
}
|
||||
return encodeRgbaPng(rgba, width, height);
|
||||
}
|
||||
|
||||
export async function comparePageRasters(
|
||||
baseline: PdfPageRaster,
|
||||
candidate: PdfPageRaster,
|
||||
pixelDifferenceThreshold = 8,
|
||||
options: number | ComparePageRasterOptions = {},
|
||||
): Promise<PdfRasterDiffResult> {
|
||||
const resolvedOptions =
|
||||
typeof options === "number"
|
||||
? { pixelDifferenceThreshold: options }
|
||||
: options;
|
||||
const pixelDifferenceThreshold =
|
||||
resolvedOptions.pixelDifferenceThreshold ?? 8;
|
||||
if (
|
||||
!Number.isInteger(pixelDifferenceThreshold) ||
|
||||
pixelDifferenceThreshold < 0 ||
|
||||
@@ -166,8 +190,16 @@ export async function comparePageRasters(
|
||||
) {
|
||||
throw new Error("像素变化阈值必须是 0 到 255 之间的整数");
|
||||
}
|
||||
const width = Math.max(baseline.widthPx, candidate.widthPx);
|
||||
const height = Math.max(baseline.heightPx, candidate.heightPx);
|
||||
const width =
|
||||
resolvedOptions.targetWidthPx ??
|
||||
Math.max(baseline.widthPx, candidate.widthPx);
|
||||
const height =
|
||||
resolvedOptions.targetHeightPx ??
|
||||
Math.max(baseline.heightPx, candidate.heightPx);
|
||||
if (!Number.isSafeInteger(width) || width <= 0 ||
|
||||
!Number.isSafeInteger(height) || height <= 0) {
|
||||
throw new Error("栅格比较尺寸必须是正整数");
|
||||
}
|
||||
const [baselineDecoded, candidateDecoded] = await Promise.all([
|
||||
decodeRaster(baseline, width, height),
|
||||
decodeRaster(candidate, width, height),
|
||||
@@ -230,11 +262,16 @@ export async function comparePageRasters(
|
||||
40,
|
||||
);
|
||||
const metrics: PdfRasterDiffMetrics = {
|
||||
baselineWidthPx: baseline.widthPx,
|
||||
baselineHeightPx: baseline.heightPx,
|
||||
candidateWidthPx: candidate.widthPx,
|
||||
candidateHeightPx: candidate.heightPx,
|
||||
comparedWidthPx: width,
|
||||
comparedHeightPx: height,
|
||||
dimensionsMatch:
|
||||
baseline.widthPx === candidate.widthPx &&
|
||||
baseline.heightPx === candidate.heightPx,
|
||||
geometryNormalized: resolvedOptions.geometryNormalized ?? false,
|
||||
meanAbsoluteError: absoluteError / (pixelCount * 3),
|
||||
changedPixelRatio: changedPixels / pixelCount,
|
||||
inkIou: calculateBinaryIou(baselineInk, candidateInk),
|
||||
@@ -243,8 +280,6 @@ export async function comparePageRasters(
|
||||
return {
|
||||
metrics,
|
||||
artifacts: createArtifacts(
|
||||
baseline,
|
||||
candidate,
|
||||
baselineGray,
|
||||
candidateGray,
|
||||
pixelDifference,
|
||||
|
||||
Reference in New Issue
Block a user