The document model
When you build a document with Rivoli PDF, you are building a tree: a Document Object Model, or DOM. You construct the tree in memory, and later hand it to the layout engine and writer to produce a PDF. The tree is plain data: it describes what the document contains, not how it is rendered.
This separation is deliberate. Because the model knows nothing about pages, fonts, or rasterization, you can serialize it to JSON, diff it, cache it, edit it on a server, and only pay the cost of layout when you actually need pixels.
The shape of the tree
Section titled “The shape of the tree”Every document has the same overall shape:
Document└── Section (one or more, each a run of pages with shared page setup) └── Block (paragraphs, tables, lists, images, containers, page breaks) └── Inline (runs of text, links, inline images, inside paragraphs)A minimal document touches all three levels:
using Rivoli.Pdf;using Rivoli.Pdf.Model;
var doc = Document.Create("My Report", "Jane Doe");doc.DefaultPageSize = PageSize.A4;doc.DefaultMargin = Margin.Standard;
var section = doc.AddSection();
section.Add(new Paragraph() .Add("This report covers ") .AddLink("#details", "key findings") .Add(" from our analysis."));Document
Section titled “Document”Document (namespace Rivoli.Pdf) is the root. It owns the sections, document-wide
defaults, and a few cross-cutting collections.
var doc = new Document(); // emptyvar doc2 = Document.Create("Title", "Author"); // with metadataKey members:
| Member | Purpose |
|---|---|
Metadata | Title, author, subject, keywords, creator |
DefaultPageSize, DefaultMargin | Page setup inherited by sections that don’t override it |
DefaultStyle, StyleSheet | Document-wide styling (see The styling cascade) |
Sections | The ordered list of sections |
FormFields, Layers, Attachments | Cross-cutting content (form fields, optional-content layers, embedded files) |
Outlines | The bookmark tree shown in a viewer’s outline panel; nested OutlineItems targeting pages |
PageLabels | /PageLabels ranges controlling displayed page numbers (“iv, v, 1, 2, …”) |
ImportedAnnotations | Comments/markup preserved page-anchored from an imported PDF |
You add sections with AddSection() (which creates and returns one) or Add(section)
(which appends one you already built).
Section
Section titled “Section”A Section (namespace Rivoli.Pdf.Model) is a run of pages that share page setup:
page size, margins, column layout, header/footer, and background. A document with a
portrait body and a landscape appendix uses two sections.
var appendix = doc.AddSection();appendix.PageSize = PageSize.Letter.Rotate(); // landscape for this section onlyappendix.WithColumns(ColumnLayout.TwoColumns);appendix.Add(new Paragraph("Appendix: detailed data"));If a section leaves PageSize or Margin null, it inherits the document defaults.
A section can also carry print-oriented page geometry: CropBox (the visible
window), plus optional BleedBox, TrimBox, and ArtBox. These are mostly
populated by PDF import so print geometry survives a round-trip, but they can be
set directly when authoring for prepress workflows.
Blocks
Section titled “Blocks”Blocks are the body content. Every block derives from the abstract Block base
class, which carries the properties common to all of them:
public abstract class Block{ public string Id { get; init; } // stable identity (see below) public Style? Style { get; set; } // optional per-block styling public bool StartOnNewPage { get; set; } public bool KeepTogether { get; set; } public bool KeepWithNext { get; set; } public string? LayerId { get; set; }}The concrete block types:
| Block | What it is |
|---|---|
Paragraph | A run of inline content (text, links, inline images) |
Table | Rows and cells, with column widths and borders |
List | Ordered or unordered items, optionally nested |
ImageBlock | A block-level image from a file path |
ContainerBlock | A group of blocks with its own background, border, padding, and orientation |
PageBreak | Forces the following content onto a new page |
The fluent builder shape is consistent: collection methods return the object so you can chain:
section.Add(new Table() .AddHeaderRow("Quarter", "Revenue", "Profit") .AddDataRow("Q1", "$1.2M", "$200K") .AddDataRow("Q2", "$1.5M", "$250K"));
section.Add(List.Unordered() .Add("First finding") .Add("Second finding"));ContainerBlock is worth calling out: it nests blocks and gives them shared framing,
which makes it the natural building block for reusable components. See
Components & reuse.
Inlines
Section titled “Inlines”Inlines are the content inside a paragraph: they flow and wrap as text. They
derive from the abstract Inline base class:
| Inline | What it is |
|---|---|
Run | A span of text with consistent formatting |
Hyperlink | A link (external URL or #anchor) wrapping inline content |
Span | A styled group of inlines |
InlineImage | An image that flows within the line |
InlineSymbol | A named symbol (non-breaking space, em dash, copyright, …) |
Paragraph exposes a convenience Text property and Add(...) overloads so you
rarely build Runs by hand:
var para = new Paragraph() .Add("Visit our ") .AddLink("https://example.com", "website") .Add(" for more.");
// para.Text concatenates the runs back into a single string.Two ways to build: fluent and POCO
Section titled “Two ways to build: fluent and POCO”Every type is a plain C# object, so you can assign properties directly or use the fluent helpers. Both produce the same tree, and you can mix them freely.
// Fluentvar doc = Document.Create("My Document");var section = doc.AddSection();section.Add(new Paragraph("Hello"));
// Direct POCO: handy in loops and conditionalsforeach (var product in products){ var para = new Paragraph(); para.Inlines.Add(new Run(product.Name) { Style = boldStyle }); para.Inlines.Add(new Run($", ${product.Price}")); section.Blocks.Add(para);}For a higher-level entry point, DocumentBuilder (namespace Rivoli.Pdf.Builders)
wraps the same model with a WithSection(...)/Build() style:
using Rivoli.Pdf.Builders;
var doc = DocumentBuilder.Create("Report") .WithPageSize(PageSize.A4) .WithSection(s => s.Add(new Paragraph("Body"))) .Build();See Working with the builder for when to reach for each style.
Node identity
Section titled “Node identity”Every node (Document, Section, every Block and Inline, plus TableRow,
TableCell, and ListItem) carries a stable Id:
public string Id { get; init; } = Guid.NewGuid().ToString();The Id is a GUID string, generated when the node is constructed and immutable
thereafter (init-only). Crucially, it survives JSON round-trips: serialize a
document, deserialize it, and every node keeps the same identity.
Stable identity is what makes the higher-level features possible:
- Change tracking reports which node changed by
Id(see Change tracking). - Caching can key on node identity.
- Collaborative editing and diff/patch can match nodes across two versions of the same document.
You can set an Id explicitly at construction time (the JSON deserializer does
exactly this to preserve identity):
var para = new Paragraph("Intro") { Id = "intro" };What lives where
Section titled “What lives where”The model knows nothing about rendering. Layout hints like KeepTogether and
StartOnNewPage live on the model because they describe intent, but the actual
page-breaking decision happens later, in the layout engine.
Computed positions and sizes are not on the model: they belong to the layout
result. Keeping that line clean is why the same document can be laid out for A4 or
Letter without changing a single block.
- The layout engine: turning this tree into pages.
- The styling cascade: how
Styleresolves across the tree. - The JSON document model: serializing the tree.