Files
MorphDoc/scripts/create-pdf-snapshot-artifact.mjs
T
SkyJourney 58f87cc19f feat: 完成 DOCX R4 元素级视觉门禁
建立 Chromium、Word 与 WPS 的可编辑内容、段落几何、页眉页脚、封面、媒体和逐页视觉比较链路。

支持封面独立分节、正文页码重启、主题页边距与横向纸张,并将自然分页差异降为诊断项。

修正代码块内边距和折叠表格单元格边框映射,14 套主题生产矩阵与六组布局视觉矩阵通过。
2026-08-02 03:27:11 +08:00

94 lines
2.9 KiB
JavaScript

import fs from "node:fs";
import { serialize } from "node:v8";
import { createCanvas, loadImage } from "@napi-rs/canvas";
import { createPdfDocumentSnapshot } from "../packages/document-visual-diff/dist/index.js";
const [pdfPath, outputPath, sourceJson, dpiText, markersJson] =
process.argv.slice(2);
if (!pdfPath || !outputPath || !sourceJson) {
throw new Error("缺少 PDF 快照 worker 参数");
}
const source = JSON.parse(sourceJson);
const snapshot = await createPdfDocumentSnapshot(fs.readFileSync(pdfPath), {
source,
dpi: Number(dpiText || 144)
});
const markerDefinitions = JSON.parse(markersJson || "[]");
const markers = [];
for (const definition of markerDefinitions) {
const target = {
r: Number.parseInt(definition.color.slice(1, 3), 16),
g: Number.parseInt(definition.color.slice(3, 5), 16),
b: Number.parseInt(definition.color.slice(5, 7), 16)
};
let best;
for (const page of snapshot.pages) {
if (!page.raster) {
continue;
}
const image = await loadImage(Buffer.from(page.raster.png));
const canvas = createCanvas(page.raster.widthPx, page.raster.heightPx);
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
const pixels = context.getImageData(
0,
0,
page.raster.widthPx,
page.raster.heightPx
).data;
let count = 0;
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
let maxX = -1;
let maxY = -1;
for (let offset = 0; offset < pixels.length; offset += 4) {
if (
Math.abs(pixels[offset] - target.r) <= 12 &&
Math.abs(pixels[offset + 1] - target.g) <= 12 &&
Math.abs(pixels[offset + 2] - target.b) <= 12 &&
pixels[offset + 3] >= 200
) {
const pixelIndex = offset / 4;
const x = pixelIndex % page.raster.widthPx;
const y = Math.floor(pixelIndex / page.raster.widthPx);
count += 1;
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
if (!best || count > best.count) {
best = {
pageNumber: page.pageNumber,
pageWidthPt: page.widthPt,
dpi: page.raster.dpi,
count,
minX,
minY,
maxX,
maxY
};
}
}
if (!best || best.count < 100) {
throw new Error(`${source.label} 未定位 ${definition.label}`);
}
const scale = 72 / best.dpi;
markers.push({
id: definition.id,
label: definition.label,
pageNumber: best.pageNumber,
pixelCount: best.count,
xPt: best.minX * scale,
yPt: best.minY * scale,
widthPt: (best.maxX - best.minX + 1) * scale,
heightPt: (best.maxY - best.minY + 1) * scale,
centerXPt: ((best.minX + best.maxX + 1) / 2) * scale,
pageWidthPt: best.pageWidthPt
});
}
fs.writeFileSync(outputPath, serialize({ snapshot, markers }));