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:
@@ -0,0 +1,76 @@
|
||||
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 }) };
|
||||
}
|
||||
Reference in New Issue
Block a user