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; } 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; 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[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 `` element, or a component that forwards `onRender` to its `` * (InvoicePdf will). */ export async function renderCore( element: ReactElement, opts: RenderCoreOptions = {}, bytesOf: RenderBytes = renderBytes, ): Promise { 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 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 }) }; }