Files
Voiced/src/pdf/testing/pdfText.ts
T
xavierk a014a907e4 Rebuild the Classic template on the engine with A4/Letter and a template harness
Classic now renders through buildRenderModel, computeFrame, the template
and renderCore, with S.No and HSN/SAC columns, GST slots only through
checked Slot components, taxable value, reverse-charge line, cancelled tag,
continuation band and Page n of N. Letter absorbs its height deficit in
elastic gaps. A harness runs 15 fixtures on A4 and Letter plus an item
sweep and asserts zero audit errors, repeated headers, no split blocks,
rupee extraction and deterministic output. The legacy InvoiceDocument is
removed.
2026-10-04 05:31:14 +05:30

87 lines
2.7 KiB
TypeScript

import { renderToBuffer } from "@react-pdf/renderer";
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
import type { ReactElement } from "react";
export interface TextBox {
str: string;
x0: number;
x1: number;
/** Distance from the top edge of the page, in points. */
top: number;
bottom: number;
}
export interface PageText {
width: number;
height: number;
items: TextBox[];
}
export interface RenderedPdf {
pages: PageText[];
/** Raw PDF bytes as a latin1 string, for grepping object dictionaries. */
raw: string;
bytes: Uint8Array;
}
/** Renders a react-pdf element under Node and returns the text boxes pdf.js finds on each page. */
export async function renderPdfText(element: ReactElement): Promise<RenderedPdf> {
// The renderToBuffer type wants a <Document> element; any element that renders one works.
const buf = await renderToBuffer(element as Parameters<typeof renderToBuffer>[0]);
return parsePdfText(new Uint8Array(buf));
}
/** The text boxes pdf.js finds on each page of already-rendered PDF bytes. */
export async function parsePdfText(bytes: Uint8Array): Promise<RenderedPdf> {
const raw = Buffer.from(bytes).toString("latin1");
const task = getDocument({
data: new Uint8Array(bytes),
useSystemFonts: false,
disableFontFace: true,
});
const doc = await task.promise;
const pages: PageText[] = [];
for (let n = 1; n <= doc.numPages; n++) {
const page = await doc.getPage(n);
const [, , width, height] = page.view;
const content = await page.getTextContent();
const items: TextBox[] = [];
for (const item of content.items) {
if (!("str" in item) || item.str.trim() === "") continue;
const x = item.transform[4];
const f = item.transform[5];
items.push({
str: item.str,
x0: x,
x1: x + item.width,
top: height - f - item.height,
bottom: height - f,
});
}
pages.push({ width, height, items });
}
await task.destroy();
return { pages, raw, bytes };
}
/** Pairs of items on one page whose boxes overlap (horizontal tolerance 0.5pt, vertical overlap must exceed 2pt). */
export function findOverlaps(page: PageText): Array<[TextBox, TextBox]> {
const out: Array<[TextBox, TextBox]> = [];
const its = page.items;
for (let i = 0; i < its.length; i++) {
for (let j = i + 1; j < its.length; j++) {
const a = its[i];
const b = its[j];
const h = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
const v = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
if (h > 0.5 && v > 2) out.push([a, b]);
}
}
return out;
}
export const describeBox = (t: TextBox) =>
`"${t.str}" [x ${t.x0.toFixed(1)}..${t.x1.toFixed(1)}, y ${t.top.toFixed(1)}..${t.bottom.toFixed(1)}]`;