Skip to content

Pipeline

Nebula renders JSX component trees into multi-page PDF documents. A single <Page> can contain more content than fits on one physical sheet. The layout engine sits between your JSX tree and rendering: it measures children, packs them into pages, and returns page groups for Satori/Resvg.

┌─────────────────────────────────────────────────────────────────┐
│ FULL PIPELINE │
│ │
│ JSX Template │
│ │ │
│ ▼ │
│ PdfEngine.generate() │
│ │ │
│ ├─ 1. extractPages() → Find <Page> VNodes │
│ ├─ 2. resolveImages() → Convert img src → base64 │
│ ├─ 3. layoutEngine.paginate() │
│ │ │ │
│ │ ├─ measureAllChildren() (Satori+Resvg) │
│ │ ├─ bin-pack loop │
│ │ │ ├─ Table? → paginateTable() │
│ │ │ ├─ Fits? → push to current page │
│ │ │ ├─ Text? → splitTextNode() │
│ │ │ └─ Atomic?→ move to next page │
│ │ └─ return PageGroup[] │
│ │ │
│ ├─ 4. renderToImage() → Satori SVG → Resvg PNG │
│ └─ 5. assemblePdf() → pdf-lib merges PNGs │
│ │
│ Output: PDF Buffer │
└─────────────────────────────────────────────────────────────────┘

Input to paginate: child VNodes from a <Page>, plus resolved page dimensions.
Output: PageGroup[] — each inner array is the VNodes for one physical PDF page.

Satori handles flexbox on a fixed-size canvas and has no concept of page breaks. Nebula needs an intermediate step that:

  1. Measures how tall each child will be when rendered
  2. Decides which children fit on the current page
  3. Splits or moves overflowing children to subsequent pages
  4. Produces page groups for final rasterization
<Page size="A4" padding={40}>
<Box style={{ height: 100 }}>Header Banner</Box>
<Text style={{ fontSize: 12 }}>A very long paragraph...</Text>
<Table columns={cols} data={bigDataset} />
<Text>Footer note</Text>
</Page>
  1. PdfEngine.generate() extracts the <Page> and resolves dimensions
    A4 portrait + 40px padding → contentWidth ≈ 515.28, contentHeight ≈ 761.89

  2. layoutEngine.paginate(children, dimensions) runs with four children.

  3. Measure each child (Box, Text, Table marker, Text).

  4. Bin-pack — for example:

Iteration Child Action
1 Box (100px) Fits on page 1
2 Text (450px) Fits on page 1
3 Table Specialized paginateTable() → multiple segments
4 Footer Text Fits after the last table segment
  1. Resulting pages might look like:
  • Page 1: [Box, Text, TableSegment0]
  • Page 2: [TableSegment1]
  • Page 3: [TableSegment2, FooterText]
  1. Each page group is wrapped with padding and rendered: Satori → SVG → Resvg → PNG.

  2. PNGs are assembled into one PDF via pdf-lib.

All measurements use PDF points (1pt = 1/72 inch).

┌─────────────────────────── width ──────────────────────────┐
│ ┌─ padding.left padding.right ─┐ │
│ │ ┌──────── contentWidth ────────┐ │ │
│ │ │ Content lives here │ contentHeight │ │
│ │ └──────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
  • contentWidth = width - padding.left - padding.right (or contentWidth override)
  • contentHeight = height - padding.top - padding.bottom

Supported sizes: A4 (595.28 × 841.89), LETTER (612 × 792), LEGAL (612 × 1008).

Read Layout Engine for bin-packing, measurement, Text splitting, and Table segments.