Files
Voiced/src/pdf/render/core.ts
T
xavierk 8d4f8e666b 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.
2026-10-04 05:05:51 +05:30

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 }) };
}