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()andAddPageBreak()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.
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.
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:
Primaryappears on every page of the section.FirstPagereplaces it on the section's first page, ifPageSetup.DifferentFirstPageHeaderFooteristrue.EvenPagereplaces it on even pages, ifPageSetup.OddAndEvenPagesHeaderFooteristrue.
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":
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:
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 };
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
Documentbuilds its styles, and theNormalstyle 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
DifferentFirstPageHeaderFooterand leaveFirstPageempty. - An empty
FirstPageorEvenPageslot is empty, not a fallback. If you setDifferentFirstPageHeaderFooterand add nothing toFirstPage, the first page has no header. FillingFirstPagewithout setting the flag has no effect. - A new
PdfPagefollows 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. Setpage.Size. Pages thatPdfDocumentRenderercreates take their size from the section'sPageSetup. RenderObjectdraws untagged content. If the result must be accessible, render throughPdfDocumentRenderer, 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
// 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();