Skip to main content

Documents, sections and styles

PinataLayout is the layout engine that sits on top of PdfPinata. You describe a document as a tree of objects: a Document holds sections, and each section holds paragraphs, tables, images and text frames. The renderer then decides where every line and page breaks, draws the headers and footers on each page, numbers the pages and resolves cross-references. Use it for reports, invoices, letters and anything else whose content flows from page to page. Use XGraphics when you need to put each mark at an exact position.

You need the PinataLayout.Rendering package and a backend. The Installation page explains both. Register a font resolver before you build a document, because the Normal style asks the resolver for its default font.

The document model

A document is built from Add methods that return the object they created:

  • Document.AddSection() starts a section. Every document needs at least one. You do not add pages: the renderer makes as many as the content needs.
  • Section.AddParagraph(), AddTable(), AddImage(), AddTextFrame() and AddPageBreak() add content to the flow.
  • Paragraph.AddText(), AddFormattedText(), AddTab(), AddLineBreak(), AddHyperlink() and the field methods add content inside a paragraph.

The types are in the PinataLayout.DocumentObjectModel namespace, tables in PinataLayout.DocumentObjectModel.Tables and images and text frames in PinataLayout.DocumentObjectModel.Shapes. Measurements are Unit values. Unit.FromCentimeter, FromMillimeter, FromPoint and FromInch make one, and a string such as "2.5cm" converts to one implicitly.

Styles

Every document starts with a set of predefined styles: Normal, Heading1 to Heading9, Header, Footer, Footnote, Hyperlink and a few more. StyleNames holds their names as constants. Styles inherit: Heading1 is based on Normal, Heading2 on Heading1, and so on. A property a style does not set comes from its base style, so the font you give Normal reaches every other style unless that style sets its own.

src/SampleApp/Demos/InvoiceDemo.cs
var document = new Document
{
Info =
{
Title = "Invoice 2026-0417",
Author = "Thornbury & Vale Ltd"
}
};

document.Styles["Normal"].Font.Name = "Liberation Sans";
document.Styles["Normal"].Font.Size = 9;

var reference = document.Styles.AddStyle("Reference", "Normal");
reference.ParagraphFormat.SpaceBefore = 0;
reference.ParagraphFormat.SpaceAfter = 0;

Styles.AddStyle(name, baseStyleName) creates a style of your own, based on an existing one. Give a paragraph a style by setting Paragraph.Style to its name. Anything you set on Paragraph.Format then overrides the style for that paragraph alone.

The predefined heading styles set ParagraphFormat.OutlineLevel, which makes every heading a PDF bookmark. They do not make a heading bigger or bolder: give them a font size and weight yourself, as the Structure demo does. See Structure, contents and cross-references.

Page setup

Page size, orientation and margins belong to a section, through Section.PageSetup. A document whose parts need different pages, such as a landscape appendix, has one section for each.

src/SampleApp/Demos/InvoiceDemo.cs
var section = document.AddSection();
section.PageSetup.PageFormat = PageFormat.A4;
section.PageSetup.TopMargin = Unit.FromCentimeter(4.5);
section.PageSetup.BottomMargin = Unit.FromCentimeter(2.5);
section.PageSetup.LeftMargin = Unit.FromCentimeter(2.2);
section.PageSetup.RightMargin = Unit.FromCentimeter(2.2);

A section you leave unset gets A4 portrait with 2.5 cm margins (2 cm at the bottom), whatever the machine's region. Set PageFormat to PageFormat.Letter for US Letter, and set Orientation to Orientation.Landscape to turn the page. A later section takes every value it does not set from the section before it. SectionStart decides whether a new section starts on the next page, the next even page or the next odd page.

Headers and footers

Each section has Headers and Footers, and each of those has three slots:

  • Primary appears on every page of the section.
  • FirstPage replaces it on the section's first page, if PageSetup.DifferentFirstPageHeaderFooter is true.
  • EvenPage replaces it on even pages, if PageSetup.OddAndEvenPagesHeaderFooter is true.

You write a header once and it repeats on every page. Fields in it are resolved for each page, so a footer can say "page 3 of 7":

src/SampleApp/Demos/InvoiceDemo.cs
var mark = section.Headers.Primary.AddParagraph();
mark.Format.Alignment = ParagraphAlignment.Right;
var logo = mark.AddImage(ImageSource.FromStream(
"logo.jpg", () => Assets.Open(Assets.ImagePrefix + "pdf-pinata.jpg")));
logo.Height = Unit.FromCentimeter(1.8);
logo.LockAspectRatio = true;

var letterhead = section.Headers.Primary.AddParagraph();
letterhead.Format.Alignment = ParagraphAlignment.Right;
letterhead.Format.Font.Size = 8;
letterhead.Format.Font.Color = Colors.Gray;
letterhead.AddText("Thornbury & Vale Ltd · 14 Cheapside · Bristol BS1 4TR");
letterhead.AddLineBreak();
letterhead.AddText("VAT 271 8834 09 · accounts@thornburyvale.example");

var footer = section.Footers.Primary.AddParagraph();
footer.Format.Alignment = ParagraphAlignment.Center;
footer.Format.Font.Size = 8;
footer.Format.Font.Color = Colors.Gray;
footer.AddText("Invoice 2026-0417 · page ");
footer.AddPageField();
footer.AddText(" of ");
footer.AddNumPagesField();

AddPageField() prints the page number, AddNumPagesField() the number of pages in the document, AddSectionField() and AddSectionPagesField() the section number and its page count, and AddDateField() the date the document was rendered. The renderer knows the page count only after it has laid out the whole document, which is why these values are fields rather than text.

Position a block on the page

Most content flows. A TextFrame is a box you position yourself, relative to the page, the margins or the current paragraph. The invoice uses one to put the address where a window envelope shows it:

src/SampleApp/Demos/InvoiceDemo.cs
var address = section.AddTextFrame();
address.Width = Unit.FromCentimeter(8);
address.Height = Unit.FromCentimeter(3);
address.Left = ShapePosition.Left;
address.RelativeHorizontal = RelativeHorizontal.Margin;
address.Top = Unit.FromCentimeter(4.6);
address.RelativeVertical = RelativeVertical.Page;

address.AddParagraph("Marlowe & Finch LLP").Format.Font.Bold = true;
address.AddParagraph("Attn: Accounts Payable");
address.AddParagraph("88 Corn Street");
address.AddParagraph("Bristol BS1 1HQ");

Render to PDF

PdfDocumentRenderer lays the document out and draws it into a PdfDocument:

var renderer = new PdfDocumentRenderer(true) { Document = document };
renderer.RenderDocument();
renderer.PdfDocument.Save("invoice.pdf");

Pass true to the constructor. It selects Unicode encoding for all text. The parameterless constructor selects WinAnsi, which covers Western European characters only. With WinAnsi, text is not shaped, right-to-left text is not put in reading order, and no fallback font is used for a missing character. Use true for any text that is not plain Latin script. See Unicode and font embedding.

After RenderDocument, renderer.PdfDocument is an ordinary PdfDocument. You can set its viewer options, add pages to it, draw on its pages or save it to a stream. To render into a document you created yourself, assign renderer.PdfDocument before you call RenderDocument.

Tagged output is the default

PdfDocumentRenderer.TagContent is true unless you change it. Every document it renders carries a structure tree: headings are headings, tables have rows and cells, and headers and footers are marked as decoration that a screen reader skips. See Accessibility, and set renderer.Language (for example "en-GB") if the document must pass PDF/UA.

A tagged document cannot be resized. PdfPage.Resize and PdfDocument.ResizePages refuse it, because moving the content would leave the structure tree pointing at the wrong place. If you render a document and then resize its pages, turn tagging off first:

var renderer = new PdfDocumentRenderer(true) { Document = document, TagContent = false };

See Page resizing and bleed.

Mix PinataLayout with XGraphics

You can use PinataLayout for part of a page and draw the rest yourself. DocumentRenderer lays a document out without creating a PDF. RenderObject then draws one paragraph, table or shape on any XGraphics, at a position and width you choose:

var pdf = new PdfDocument();
PdfPage page = pdf.AddPage();
page.Size = PageSize.A4;

using (XGraphics gfx = XGraphics.FromPdfPage(page))
{
// Use any family your font resolver serves.
gfx.DrawString("Drawn with XGraphics", new XFont("Arial", 13), XBrushes.Black, 70, 80);

var doc = new Document();
Paragraph para = doc.AddSection().AddParagraph("Laid out by PinataLayout in a 12 cm column.");
para.Format.Alignment = ParagraphAlignment.Justify;

var layout = new DocumentRenderer(doc);
layout.PrepareDocument();
layout.RenderObject(gfx, XUnit.FromCentimeter(2.5), XUnit.FromCentimeter(4),
XUnit.FromCentimeter(12), para);
}

To draw whole laid-out pages instead, call PrepareDocument() and then RenderPage(gfx, pageNumber) for each page. Page numbers start at 1, and layout.FormattedDocument.PageCount says how many there are. Apply gfx.ScaleTransform and gfx.TranslateTransform first to draw a page smaller, for example as a thumbnail.

Things to know

  • Register a font resolver first. Creating a Document builds its styles, and the Normal style asks the resolver for its default font name. The demos' fonts, such as "Liberation Sans", come from the SampleApp's own resolver. Your application needs its own; see Fonts.
  • A section with no header of its own uses the previous section's. The same is true for each of the three slots. To give a title page no header, make it the first section, or set DifferentFirstPageHeaderFooter and leave FirstPage empty.
  • An empty FirstPage or EvenPage slot is empty, not a fallback. If you set DifferentFirstPageHeaderFooter and add nothing to FirstPage, the first page has no header. Filling FirstPage without setting the flag has no effect.
  • A new PdfPage follows the machine's region. This matters only when you add pages yourself, as in the mixing example: a new page is A4 on a metric system and Letter otherwise. Set page.Size. Pages that PdfDocumentRenderer creates take their size from the section's PageSetup.
  • RenderObject draws untagged content. If the result must be accessible, render through PdfDocumentRenderer, or see Tag a page you draw yourself.

See it in action

The Invoice demo builds a one-page invoice: a letterhead and footer that repeat, an address in a text frame, a tab-aligned reference block, a borderless item table with merged total rows, and a shaded terms box.

The full Invoice demo
src/SampleApp/Demos/InvoiceDemo.cs
// The line items. A record rather than an XML file or a database, so that the data
// is visible in the same source as the layout that renders it.
(string Code, string Description, int Quantity, decimal UnitPrice)[] items =
{
("PS-1001", "PdfPinata support, annual", 1, 1200.00m),
("PS-1002", "Migration consultancy, per day", 6, 780.00m),
("PS-2010", "Font licensing review", 1, 450.00m),
("PS-2011", "Embedded subsetting audit", 2, 325.00m),
("PS-3100", "Layout engine training, per seat", 12, 145.00m),
("PS-3101", "Training materials, printed", 12, 18.50m),
("PS-4000", "On-site workshop, two days", 1, 2400.00m),
("PS-4001", "Travel and accommodation", 1, 615.40m),
("PS-5000", "Document template design", 4, 390.00m),
("PS-5001", "Accessibility tagging review", 1, 880.00m),
("PS-6000", "Performance profiling", 3, 540.00m),
("PS-6001", "Rasterization test harness", 1, 720.00m),
("PS-7000", "Priority incident cover, quarterly", 4, 950.00m),
("PS-7001", "Out of hours callout allowance", 2, 275.00m),
("PS-8000", "Archival conversion, per thousand pages", 34, 12.75m),
("PS-8001", "Optical character recognition pass", 34, 8.20m),
("PS-9000", "Signature and encryption review", 1, 1150.00m),
("PS-9001", "Long term validation setup", 1, 640.00m)
};

var document = new Document
{
Info =
{
Title = "Invoice 2026-0417",
Author = "Thornbury & Vale Ltd"
}
};

document.Styles["Normal"].Font.Name = "Liberation Sans";
document.Styles["Normal"].Font.Size = 9;

var reference = document.Styles.AddStyle("Reference", "Normal");
reference.ParagraphFormat.SpaceBefore = 0;
reference.ParagraphFormat.SpaceAfter = 0;

var section = document.AddSection();
section.PageSetup.PageFormat = PageFormat.A4;
section.PageSetup.TopMargin = Unit.FromCentimeter(4.5);
section.PageSetup.BottomMargin = Unit.FromCentimeter(2.5);
section.PageSetup.LeftMargin = Unit.FromCentimeter(2.2);
section.PageSetup.RightMargin = Unit.FromCentimeter(2.2);

// ---- Letterhead ---------------------------------------------------------------
// The image goes through the ImageSource seam rather than XImage, which is how
// PinataLayout reaches a backend. The stream factory reads the embedded photograph.
var mark = section.Headers.Primary.AddParagraph();
mark.Format.Alignment = ParagraphAlignment.Right;
var logo = mark.AddImage(ImageSource.FromStream(
"logo.jpg", () => Assets.Open(Assets.ImagePrefix + "pdf-pinata.jpg")));
logo.Height = Unit.FromCentimeter(1.8);
logo.LockAspectRatio = true;

var letterhead = section.Headers.Primary.AddParagraph();
letterhead.Format.Alignment = ParagraphAlignment.Right;
letterhead.Format.Font.Size = 8;
letterhead.Format.Font.Color = Colors.Gray;
letterhead.AddText("Thornbury & Vale Ltd · 14 Cheapside · Bristol BS1 4TR");
letterhead.AddLineBreak();
letterhead.AddText("VAT 271 8834 09 · accounts@thornburyvale.example");

var footer = section.Footers.Primary.AddParagraph();
footer.Format.Alignment = ParagraphAlignment.Center;
footer.Format.Font.Size = 8;
footer.Format.Font.Color = Colors.Gray;
footer.AddText("Invoice 2026-0417 · page ");
footer.AddPageField();
footer.AddText(" of ");
footer.AddNumPagesField();

// ---- Addressee ----------------------------------------------------------------
// A text frame is positioned rather than flowed, which is what puts an address
// where a window envelope expects to find it.
var address = section.AddTextFrame();
address.Width = Unit.FromCentimeter(8);
address.Height = Unit.FromCentimeter(3);
address.Left = ShapePosition.Left;
address.RelativeHorizontal = RelativeHorizontal.Margin;
address.Top = Unit.FromCentimeter(4.6);
address.RelativeVertical = RelativeVertical.Page;

address.AddParagraph("Marlowe & Finch LLP").Format.Font.Bold = true;
address.AddParagraph("Attn: Accounts Payable");
address.AddParagraph("88 Corn Street");
address.AddParagraph("Bristol BS1 1HQ");

// ---- Reference block ----------------------------------------------------------
// Tab stops align a two column block without the weight of a table. The right
// aligned stop is what keeps the values flush with the margin.
var spacer = section.AddParagraph();
spacer.Format.SpaceAfter = Unit.FromCentimeter(2.6);

var invoiceTitle = section.AddParagraph("INVOICE");
invoiceTitle.Format.Font.Size = 20;
invoiceTitle.Format.Font.Bold = true;
invoiceTitle.Format.SpaceAfter = Unit.FromPoint(10);

(string Label, string Value)[] references =
{
("Invoice number", "2026-0417"),
("Invoice date", "12 August 2026"),
("Payment due", "11 September 2026"),
("Purchase order", "MF-PO-88213")
};

foreach ((var label, var value) in references)
{
var line = section.AddParagraph();
line.Style = "Reference";
line.Format.TabStops.ClearAll();
line.Format.TabStops.AddTabStop(Unit.FromCentimeter(4), TabAlignment.Left);
line.AddText(label);
line.AddTab();
line.AddFormattedText(value, TextFormat.Bold);
}

// ---- Items --------------------------------------------------------------------
var itemsGap = section.AddParagraph();
itemsGap.Format.SpaceAfter = Unit.FromPoint(16);

var table = section.AddTable();
table.Borders.Width = 0;
table.Rows.LeftIndent = 0;

table.AddColumn(Unit.FromCentimeter(2.2)).Format.Alignment = ParagraphAlignment.Left;
table.AddColumn(Unit.FromCentimeter(7.4)).Format.Alignment = ParagraphAlignment.Left;
table.AddColumn(Unit.FromCentimeter(1.6)).Format.Alignment = ParagraphAlignment.Right;
table.AddColumn(Unit.FromCentimeter(2.6)).Format.Alignment = ParagraphAlignment.Right;
table.AddColumn(Unit.FromCentimeter(2.8)).Format.Alignment = ParagraphAlignment.Right;

var head = table.AddRow();
head.HeadingFormat = true;
head.Format.Font.Bold = true;
head.Borders.Bottom.Width = 0.8;
head.Borders.Bottom.Color = Colors.Black;
head.TopPadding = Unit.FromPoint(2);
head.BottomPadding = Unit.FromPoint(4);

string[] headings = { "Code", "Description", "Qty", "Unit price", "Amount" };
for (var column = 0; column < headings.Length; column++)
head.Cells[column].AddParagraph(headings[column]);

decimal net = 0;
for (var index = 0; index < items.Length; index++)
{
(var code, var description, var quantity, var unitPrice) = items[index];
var amount = quantity * unitPrice;
net += amount;

var row = table.AddRow();
row.TopPadding = Unit.FromPoint(3);
row.BottomPadding = Unit.FromPoint(3);
row.Borders.Bottom.Width = 0.25;
row.Borders.Bottom.Color = Colors.Gainsboro;

row.Cells[0].AddParagraph(code);
row.Cells[1].AddParagraph(description);
row.Cells[2].AddParagraph(quantity.ToString());
row.Cells[3].AddParagraph($"{unitPrice:N2}");
row.Cells[4].AddParagraph($"{amount:N2}");
}

var vat = net * 0.20m;

void Total(string label, decimal amount, bool emphasis)
{
var row = table.AddRow();
row.TopPadding = Unit.FromPoint(4);
row.BottomPadding = Unit.FromPoint(4);
row.Cells[0].MergeRight = 3;
row.Cells[0].Format.Alignment = ParagraphAlignment.Right;
row.Cells[0].AddParagraph(label);
row.Cells[4].AddParagraph($"{amount:N2}");

if (emphasis)
{
row.Format.Font.Bold = true;
row.Borders.Top.Width = 0.8;
row.Borders.Top.Color = Colors.Black;
}
}

Total("Net", net, false);
Total("VAT at 20%", vat, false);
Total("Total due (GBP)", net + vat, true);

// ---- Terms --------------------------------------------------------------------
var terms = section.AddParagraph();
terms.Format.SpaceBefore = Unit.FromPoint(20);
terms.Format.Borders.Width = 0.5;
terms.Format.Borders.Color = Colors.Gainsboro;
terms.Format.Shading.Color = Colors.WhiteSmoke;
terms.Format.LeftIndent = Unit.FromPoint(8);
terms.Format.RightIndent = Unit.FromPoint(8);
terms.Format.SpaceAfter = Unit.FromPoint(8);
terms.Format.Font.Size = 8;
terms.AddFormattedText("Terms. ", TextFormat.Bold);
terms.AddText("Payment within 30 days by transfer to the account above, quoting the "
+ "invoice number. Interest is charged on overdue amounts at 8% above base rate "
+ "under the Late Payment of Commercial Debts (Interest) Act 1998.");

var renderer = new PdfDocumentRenderer(true) { Document = document };
renderer.RenderDocument();