Skip to content

PDF Rendering

The Pragmatic.Documents.Pdf package renders a DocumentModel to a PDF byte stream. It uses a native-backed renderer for output quality and font handling.


public static class PdfRenderer
{
public static byte[] Render(DocumentModel model, PdfResources? resources = null);
public static void RenderTo(Stream output, DocumentModel model, PdfResources? resources = null);
}
public static class PdfOperations
{
public static uint GetPageCount(ReadOnlySpan<byte> pdf);
}
  • Render materialises the whole PDF in memory. Fine for invoices, letters, one-offs.
  • RenderTo(Stream, ...) streams directly to the destination. Use this for large reports.
  • GetPageCount is a lightweight PDF-aware scan — no full parsing.

using Pragmatic.Documents.Model;
using Pragmatic.Documents.Pdf;
var doc = new DocumentBuilder()
.Title("Sample")
.Page(p => p
.Heading("Hello, PDF")
.Paragraph(new TextNode { Content = "Rendered by Pragmatic." }))
.Build();
using var fs = File.Create("sample.pdf");
PdfRenderer.RenderTo(fs, doc);

NodeEffect in PDF
TextNodeText run with optional NodeStyle (font, size, colour, weight)
HeadingNodeHeading with auto-generated bookmark (for TOC)
ParagraphNodeBlock-level paragraph with inline children
TableNodeTable with header row, optional styling
ImageNodeRaster image (PNG, JPEG) with width/height/alt
BarcodeNodeRendered barcode (default QR code; several types supported)
HyperlinkNodeClickable link inside the PDF
TocNodeTable of contents built from heading bookmarks
PageBreakNodeStarts a new page
Spacer, HorizontalRuleLayout separators
FieldNodeDynamic field — PageNumber, PageCount, Date, etc.
BookmarkNodeNamed anchor for internal navigation
FootnoteNodeFootnote marker + footer entry on the same page

Fields (FieldType.PageNumber, FieldType.PageCount, FieldType.Date) are resolved during rendering; you don’t supply current-page information.


var doc = new DocumentBuilder()
.Size(PageSize.A4) // A4, Letter, Legal, Custom
.Landscape() // or WithMargins(Margins.Normal | Margins.Narrow | Margins.Wide)
.WithMargins(new Margins(top: 72, right: 72, bottom: 72, left: 72))
.Page(p => { /* ... */ })
.Build();

All dimensions are in points (1 point = 1/72 inch). PageSize.A4 is 595 × 842 points.

.Page(p => p
.Header(
new TextNode { Content = "Pragmatic Quarterly Report", Style = new NodeStyle { FontSize = 9, Color = "#666" } })
.Footer(
new TextNode { Content = "Page " },
new FieldNode { FieldType = FieldType.PageNumber },
new TextNode { Content = " of " },
new FieldNode { FieldType = FieldType.PageCount })
.Heading("Introduction")
.Paragraph(...))

Header and footer nodes repeat on every page of the same Page block.


.Table(t => t
.HeaderRow("Item", "Quantity", "Unit Price", "Total")
.Row("Consulting", "10 h", "€150", "€1500")
.Row("Implementation", "40 h", "€120", "€4800")
.Row("QA", "12 h", "€90", "€1080"))

Tables size their columns automatically. For manual widths or per-cell styling, build the TableNode / TableCell directly rather than via the HeaderRow/Row shortcuts.


.Page(p => p
.Toc(maxLevel: 3, title: "Contents"))
.Page(p => p
.Heading("Chapter 1", level: 1)
.Paragraph(...)
.Heading("Section 1.1", level: 2)
.Paragraph(...))
.Page(p => p
.Heading("Chapter 2", level: 1)
.Paragraph(...))

The TOC is generated during rendering by walking the heading bookmarks that appeared before it. Each entry includes the heading text and the target page number.

Limitations:

  • TocNode must appear before the headings it references in document order
  • Page numbers are resolved after layout, so very long headings that wrap differently than expected may shift page numbers

Attach fonts and images through PdfResources:

var resources = new PdfResources();
resources.AddFont("Inter", File.ReadAllBytes("fonts/Inter-Regular.ttf"));
resources.AddImage("logo", File.ReadAllBytes("assets/logo.png"));
// Reference them in nodes
.Image("logo") // uses embedded image by name
.Text("Branded title", style: new NodeStyle { FontFamily = "Inter" })
PdfRenderer.RenderTo(fs, doc, resources);

If no resources are supplied the renderer falls back to embedded default fonts. See the samples for concrete font / image wiring.


.Barcode("https://pragmaticdesign.net", type: BarcodeType.QrCode)
.Barcode("1234567890128", type: BarcodeType.Ean13)

Available types include QrCode, Code128, Code39, Ean13. The barcode renders at a default size; set it explicitly via BarcodeNode { Width = 120, Height = 120 }.


  • For large reports (many pages, embedded images) use RenderTo(Stream, ...) instead of Render so you don’t hold the whole output in memory.
  • The renderer is deterministic — the same model produces byte-identical output (modulo creation dates), which makes PDFs snapshot-testable.
  • Typical single-page invoice renders in ~5-15ms on modern hardware.

  • No full CSS/HTML input — feed the document model directly.
  • No form fields (fillable PDFs are out of scope).
  • No digital signatures — sign with a separate step after rendering.
  • No PDF/A profile tagging yet.