Files
suaveplan/packages/devtools/docs/pdf/src/render-pipeline/render-pipeline.md
2026-05-27 23:29:53 +01:00

3.4 KiB

render-pipeline

(no submodules)

Purpose

Internal pipeline helpers that the top-level renderDocument orchestrator delegates to. Splitting placement and slot-rendering out of the orchestrator keeps each file under the 300-line hard limit and lets callers test the per-node placement protocol in isolation from the page-emit loop. The pipeline is stateless across calls — every operation receives a PipelineState bundle (writer, layout engine, style resolver, image registry, abort signal, page counter) and mutates only the writer and the page counter.

Features

  • placeNode dispatches on the discriminated DocumentNode union and returns either a content-stream snippet or the "page-break" sentinel that signals the orchestrator to flush + restart.
  • Tables emit a continuation descriptor ({ table, rowStart }) when they partially fit, so the orchestrator can resume on the next page without re-measuring the full table.
  • renderTableSlice swallows PDF.LAYOUT_OVERFLOW from TableElement.renderPage and surfaces it as a rowEnd === rowStart no-progress result.
  • renderSlot flows header / footer nodes top-to-bottom inside the reserved slot rectangle.
  • pickFontResource re-uses the writer's idempotent font registry so one F<id> survives the whole render.

Usage: Basic

import { placeNode, renderSlot, type PipelineState } from "@suaveplan/pdf";

const state: PipelineState = { writer, layout, styles, images, signal: undefined, pageCount: 0 };
const result = placeNode(state, node, cursor, undefined, new Map());
if (result === "page-break") {
    // orchestrator: flush and retry
}

Usage: Advanced

import { placeNode, renderTableSlice, renderSlot } from "@suaveplan/pdf";

// Slice rendering protocol — orchestrator advances rowStart between calls.
let rowStart = 0;
while (rowStart < tableElement.options.rows.length) {
    const slice = renderTableSlice(state, tableElement, rowStart, cursor, config);
    if (slice.rowEnd === rowStart) {
        // No progress — caller must flush + create a fresh cursor.
        break;
    }
    rowStart = slice.rowEnd;
    // ... write slice.stream into the per-page buffer ...
}

API Reference

  • placeNode(state, node, cursor, config?, pageXObjects) → PlacementResult.
  • renderTableSlice(state, table, rowStart, cursor, config?) → { stream, rowEnd }.
  • renderSlot(state, nodes?, height?, slot, config?, pageXObjects) → string[].
  • pickFontResource(state, style) → "F<id>".
  • Types: PipelineState, PlacementResult.

Implementation Notes

placeNode always measures first, then conditionally advances the cursor. Text and divider elements pass the pre-advance cursor.y as the render y (PDF's bottom-up coordinate system means the top of the element lives at the larger y, and the element's .render() contract places baselines at y - i·lineHeight). Images measure once and ignore maxWidth because their bounds are fixed at construction. Tables short-circuit through TableElement.render when the whole element fits and only fall back to the slice protocol when pagination is required. The slot renderer flows nodes downward inside the reserved rectangle, which means a footer slot with two stacked text lines places the first line at the slot top and the second below — matching the visual expectation that flow content reads top-to-bottom regardless of PDF's bottom-up geometry.