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.
77 lines
3.3 KiB
TypeScript
77 lines
3.3 KiB
TypeScript
import { pdf, renderToBuffer } from "@react-pdf/renderer";
|
|
import { cloneElement, type ReactElement } from "react";
|
|
import { auditLayout, type AuditIssue, type AuditOptions } from "../engine/audit";
|
|
import { layoutFingerprint } from "../engine/fingerprint";
|
|
import type { PageFrame } from "../engine/geometry";
|
|
import { normalizeLayout, type LayoutPage, type Rect } from "../engine/layoutTree";
|
|
|
|
export { normalizeLayout, type LayoutNode, type LayoutPage, type Rect } from "../engine/layoutTree";
|
|
export { auditLayout, type AuditIssue } from "../engine/audit";
|
|
export { layoutFingerprint } from "../engine/fingerprint";
|
|
|
|
export interface RenderCoreOptions {
|
|
/** With a frame the layout is audited; without one `issues` is empty. */
|
|
frame?: PageFrame;
|
|
/** Page-space boxes content must stay out of (audited as `keepout`). */
|
|
decorBoxes?: Rect[];
|
|
/** Further audit options (closing block id, ...). */
|
|
audit?: Omit<AuditOptions, "keepOuts">;
|
|
}
|
|
|
|
export interface RenderCoreResult {
|
|
bytes: Uint8Array;
|
|
pages: number;
|
|
layout: LayoutPage[];
|
|
issues: AuditIssue[];
|
|
/** sha256 of the normalised layout (not of the bytes; Creator/Producer feed the PDF /ID). */
|
|
fingerprint: string;
|
|
}
|
|
|
|
/** Turns an element into PDF bytes. Swappable: the render worker supplies its own. */
|
|
export type RenderBytes = (element: ReactElement) => Promise<Uint8Array>;
|
|
|
|
const isNode = typeof process !== "undefined" && !!process.versions?.node;
|
|
|
|
/**
|
|
* Node uses renderToBuffer; the browser build of react-pdf throws from it, so there (and in a worker)
|
|
* pdf().toBlob() is used. Both call the Document's onRender with the layout tree.
|
|
*/
|
|
export const renderBytes: RenderBytes = async (element) => {
|
|
const el = element as Parameters<typeof renderToBuffer>[0];
|
|
if (isNode) return new Uint8Array(await renderToBuffer(el));
|
|
const blob = await pdf(el).toBlob();
|
|
return new Uint8Array(await blob.arrayBuffer());
|
|
};
|
|
|
|
/**
|
|
* Renders `element` once and returns the bytes, the normalised layout tree, the audit issues and the
|
|
* layout fingerprint.
|
|
*
|
|
* The layout tree comes from the Document's `onRender` (`_INTERNAL__LAYOUT__DATA_`). For now the root
|
|
* element is cloned with an `onRender` added (chained after the one it already has). The root must
|
|
* therefore be a `<Document>` element, or a component that forwards `onRender` to its `<Document>`
|
|
* (InvoicePdf will).
|
|
*/
|
|
export async function renderCore(
|
|
element: ReactElement,
|
|
opts: RenderCoreOptions = {},
|
|
bytesOf: RenderBytes = renderBytes,
|
|
): Promise<RenderCoreResult> {
|
|
let raw: unknown;
|
|
const existing = (element.props as { onRender?: (p: unknown) => void }).onRender;
|
|
const wrapped = cloneElement(element as ReactElement<{ onRender?: (p: unknown) => void }>, {
|
|
onRender: (params: unknown) => {
|
|
raw = (params as { _INTERNAL__LAYOUT__DATA_?: unknown } | undefined)?._INTERNAL__LAYOUT__DATA_;
|
|
existing?.(params);
|
|
},
|
|
});
|
|
|
|
const bytes = await bytesOf(wrapped);
|
|
if (raw === undefined) {
|
|
throw new Error("renderCore: onRender never delivered a layout tree; the root element must be a <Document> or forward onRender to one");
|
|
}
|
|
const { pages } = normalizeLayout(raw);
|
|
const issues = opts.frame ? auditLayout({ pages }, opts.frame, { ...opts.audit, keepOuts: opts.decorBoxes }) : [];
|
|
return { bytes, pages: pages.length, layout: pages, issues, fingerprint: await layoutFingerprint({ pages }) };
|
|
}
|