Skip to content

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.

Rivoli PDF uses the same two-pass model as UI frameworks like WPF/XAML:

  1. Measure: “How much space do you need?” Each element is asked for its desired size, given a constraint (the available width and height).
  2. 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.

  • 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 space
var fixedWidth = LayoutConstraints.WithWidth(500); // wrap to 500pt, grow down

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).

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):

HintEffect
PageBreak blockForces the following content onto a new page
StartOnNewPageForces this block to the top of a fresh page if it isn’t already
KeepWithNextTries to keep this block on the same page as the one after it
KeepTogetherTries to avoid breaking within this block
WidowControl / OrphanControlMinimum lines kept together at a page boundary (defaults 2)
var heading = new Paragraph("Results") { KeepWithNext = true }; // don't orphan
var 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:

  1. An explicit PageBreak block.
  2. StartOnNewPage.
  3. KeepWithNext.
  4. KeepTogether.
  5. WidowControl / OrphanControl.

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.

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.