The layout engine
The document model describes what your document contains. The layout engine decides where everything goes: it measures each block, arranges it on a page, and breaks content across pages when it runs out of room. The result is a set of pages with positioned content: the input the PDF writer turns into pixels.
The engine is deliberately decoupled from PDF generation. The same layout result can drive PDF output, on-screen preview, or analysis and testing. You can also run it directly to inspect how a document paginates.
Measure and arrange
Section titled “Measure and arrange”Rivoli PDF uses the same two-pass model as UI frameworks like WPF/XAML:
- Measure: “How much space do you need?” Each element is asked for its desired size, given a constraint (the available width and height).
- Arrange: “Here is where you go, and how much space you get.” Each element is given a final position and size, and computes its bounding box.
Two passes are necessary because size often depends on available width (text wraps, tables distribute columns) while position depends on the sizes of everything above.
The contract is captured by ILayoutNode:
public interface ILayoutNode{ MeasureResult Measure(LayoutConstraints constraints); ArrangeResult Arrange(Point position, Size size);}Each block type has a matching layout node (ParagraphLayoutNode,
TableLayoutNode, ListLayoutNode, ImageBlockLayoutNode, ContainerLayoutNode,
PageBreakLayoutNode), all derived from the abstract BlockLayoutNode. You rarely
touch these directly; the engine builds them for you.
Coordinates and units
Section titled “Coordinates and units”- Units are points (1/72 inch) everywhere in layout.
- Positions are page-relative, with the origin
(0, 0)at the top-left of the content area: that is, the page rectangle minus its margins.
The core geometry types live in Rivoli.Pdf.Layout:
var size = new Size(width: 100, height: 50);var point = new Point(x: 10, y: 20);var rect = new Rectangle(x: 10, y: 20, width: 100, height: 50);// rect.Right == 110, rect.Bottom == 70
var constraints = new LayoutConstraints(maxWidth: 500, maxHeight: 700);var unbounded = LayoutConstraints.Unbounded; // infinite spacevar fixedWidth = LayoutConstraints.WithWidth(500); // wrap to 500pt, grow downRunning layout
Section titled “Running layout”The entry point is LayoutEngine (namespace Rivoli.Pdf.Layout):
using Rivoli.Pdf.Layout;
var engine = new LayoutEngine();LayoutResult result = engine.LayoutDocument(doc);
Console.WriteLine($"Document laid out to {result.PageCount} pages.");
foreach (LayoutPage page in result.Pages){ Console.WriteLine($"Page {page.PageNumber}: " + $"{page.PageSize} content area {page.ContentArea}");
foreach (var node in page.Nodes) { // node.SourceBlock is the original Block; // node.ArrangedBounds is its final position on the page. }}A LayoutResult is a list of LayoutPages. Each page records its page size,
margins, content area, background, header/footer config, and the arranged block
nodes that landed on it. Because every arranged node points back at its
SourceBlock, you can trace any positioned element to the model node that produced
it.
You can also lay out a single section in isolation with
engine.LayoutSection(section, defaultPageSize, defaultMargin).
Pagination
Section titled “Pagination”The engine flows blocks down the content area and starts a new page when the next block does not fit. You influence where breaks land using hints on the block itself (set on the model, honored during layout):
| Hint | Effect |
|---|---|
PageBreak block | Forces the following content onto a new page |
StartOnNewPage | Forces this block to the top of a fresh page if it isn’t already |
KeepWithNext | Tries to keep this block on the same page as the one after it |
KeepTogether | Tries to avoid breaking within this block |
WidowControl / OrphanControl | Minimum lines kept together at a page boundary (defaults 2) |
var heading = new Paragraph("Results") { KeepWithNext = true }; // don't orphanvar chapter = new Paragraph("Chapter 2") { StartOnNewPage = true };var callout = new ContainerBlock { KeepTogether = true };When several hints apply to the same boundary, they resolve in this priority order:
- An explicit
PageBreakblock. StartOnNewPage.KeepWithNext.KeepTogether.WidowControl/OrphanControl.
Element-specific layout
Section titled “Element-specific layout”A few blocks do more than stack:
- Paragraphs wrap text to the available width, apply alignment (left/center/right/justify), and render text decorations and letter spacing.
- Tables size columns. A column can be a fixed width, automatically sized to its
content, or a relative (proportional) share of the remaining width. See
ColumnWidth. - Lists lay out markers (bullets or numbers) in a gutter and indent the item content, including nested items.
- Containers lay their children out vertically or horizontally and apply their own padding, border, and background as framing.
Why this matters to you
Section titled “Why this matters to you”Most of the time you never call LayoutEngine yourself: generating a PDF runs it
for you. You reach for it directly when you want to:
- Count pages before rendering (
result.PageCount). - Inspect positions for testing or analysis.
- Drive a custom renderer (preview, thumbnails) from the same layout the PDF writer uses.
Because layout is a pure function of the model plus page setup, it is deterministic: the same document and the same page size always produce the same pages. That determinism is the foundation of the project’s round-trip fidelity testing.
- The styling cascade: how the styles layout reads get resolved.
- How PDF works: what the writer does with the laid-out pages.