Fix header overlap in invoice PDF and Bongbetic wordmark on light theme
react-pdf wraps text once at the first flex measure; when yoga then shrank the vendor column the cached lines ran under the INVOICE meta column. The header now pins column widths (299 + 24 + 192 pt), sizes the logo from its own aspect (stacked above the text when it would leave under 160 pt), uses absolute per-Text line heights, and turns hyphenation off. Also: repeating table header with an orphan-header guard, wrap=false rows, Page n of N footer, GSTIN printed only when present, one Text per address line, HTML twin header grid. The light-theme header showed the white-text wordmark; the ternary was inverted. Adds vitest + pdfjs-dist harness that fails on the old document (address overlaps Invoice No.) and passes on the new one.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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]);
|
||||
const raw = buf.toString("latin1");
|
||||
const bytes = new Uint8Array(buf);
|
||||
|
||||
const task = getDocument({
|
||||
data: new Uint8Array(buf),
|
||||
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)}]`;
|
||||
Reference in New Issue
Block a user