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
Section titled “Full pipeline”┌─────────────────────────────────────────────────────────────────┐│ 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.
Why a custom layout engine?
Section titled “Why a custom layout engine?”Satori handles flexbox on a fixed-size canvas and has no concept of page breaks. Nebula needs an intermediate step that:
- Measures how tall each child will be when rendered
- Decides which children fit on the current page
- Splits or moves overflowing children to subsequent pages
- Produces page groups for final rasterization
End-to-end example
Section titled “End-to-end example”<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>Step-by-step
Section titled “Step-by-step”-
PdfEngine.generate()extracts the<Page>and resolves dimensions
A4 portrait + 40px padding →contentWidth ≈ 515.28,contentHeight ≈ 761.89 -
layoutEngine.paginate(children, dimensions)runs with four children. -
Measure each child (Box, Text, Table marker, Text).
-
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 |
- Resulting pages might look like:
- Page 1:
[Box, Text, TableSegment0] - Page 2:
[TableSegment1] - Page 3:
[TableSegment2, FooterText]
-
Each page group is wrapped with padding and rendered: Satori → SVG → Resvg → PNG.
-
PNGs are assembled into one PDF via pdf-lib.
Page dimensions
Section titled “Page dimensions”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(orcontentWidthoverride)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.