Add pagination blocks, renderCore, layout audit and react-pdf quirk tests

TableGuard, BottomSpacer, FirstPageHeader and PageChrome implement the
verified pagination rules; renderCore returns bytes, a normalised layout
tree, audit issues and a layout fingerprint. auditLayout detects overlaps,
ink overflow, truncation, header-only pages and split blocks. Quirk tests
pin the react-pdf behaviours the design depends on, and a child-process
test pins the page-level absolute Svg hang.
This commit is contained in:
2026-10-04 05:05:51 +05:30
parent 3818b1eeed
commit e2032a3f9c
24 changed files with 2187 additions and 5 deletions
+40
View File
@@ -0,0 +1,40 @@
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
/** `@napi-rs/canvas` is an optional dependency; null when its native binary is not installed. */
export async function loadCanvas(): Promise<typeof import("@napi-rs/canvas") | null> {
try {
return await import("@napi-rs/canvas");
} catch {
return null;
}
}
/** Number of non-white pixels in the box (x0,y0,x1,y1 in pt from the top-left) of page 1 drawn at `scale`. */
export async function inkPixels(
canvasLib: typeof import("@napi-rs/canvas"),
bytes: Uint8Array,
box: { x0: number; y0: number; x1: number; y1: number },
scale = 2,
): Promise<number> {
const task = getDocument({ data: new Uint8Array(bytes), useSystemFonts: false, disableFontFace: true });
const doc = await task.promise;
try {
const page = await doc.getPage(1);
const viewport = page.getViewport({ scale });
const canvas = canvasLib.createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height));
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
await page.render({ canvasContext: ctx as never, canvas: canvas as never, viewport }).promise;
const x = Math.floor(box.x0 * scale);
const y = Math.floor(box.y0 * scale);
const w = Math.ceil((box.x1 - box.x0) * scale);
const h = Math.ceil((box.y1 - box.y0) * scale);
const data = ctx.getImageData(x, y, w, h).data;
let ink = 0;
for (let i = 0; i < data.length; i += 4) if (data[i] < 250 || data[i + 1] < 250 || data[i + 2] < 250) ink++;
return ink;
} finally {
await task.destroy();
}
}