Structure, contents and cross-references
A long document needs a shape the reader can find their way around: headings, a bookmarks panel, a table of contents, and references such as "see page 12". In PinataLayout you do not write page numbers or build the bookmarks panel yourself. You mark headings and name places in the text, and the renderer works out the pages after it has laid the whole document out. When the text changes, the numbers change with it.
Headings become bookmarks
A paragraph whose ParagraphFormat.OutlineLevel is Level1 to Level9 becomes an entry in the
PDF's outline, the panel that Adobe Acrobat calls "Bookmarks". The predefined styles Heading1 to
Heading9 already set levels 1 to 9, so a paragraph in a heading style is an outline entry with no
extra code. You can also set the level on any style, or on one paragraph's Format.
The Structure demo gives the heading styles their fonts and states the levels explicitly:
// Every predefined style is already there and already related: Heading1 is based on Normal,
// Heading2 on Heading1, and so on, so setting the font on Normal reaches all of them.
// StyleNames holds the names rather than the styles - they are looked up on the document.
var normal = report.Styles[StyleNames.Normal];
normal.Font.Name = "Liberation Serif";
normal.Font.Size = 10.5;
normal.ParagraphFormat.SpaceAfter = Unit.FromPoint(6);
var heading1 = report.Styles[StyleNames.Heading1];
heading1.Font.Name = "Liberation Sans";
heading1.Font.Size = 18;
heading1.Font.Bold = true;
heading1.ParagraphFormat.SpaceBefore = Unit.FromPoint(18);
heading1.ParagraphFormat.SpaceAfter = Unit.FromPoint(8);
// OutlineLevel is the whole of the bookmark story. A paragraph carrying one becomes an
// entry in the PDF's outline when the document is rendered - DocumentRenderer.AddOutline
// is called for it - so nothing here ever mentions PdfOutline.
heading1.ParagraphFormat.OutlineLevel = OutlineLevel.Level1;
var heading2 = report.Styles[StyleNames.Heading2];
heading2.Font.Size = 13;
heading2.ParagraphFormat.SpaceBefore = Unit.FromPoint(12);
heading2.ParagraphFormat.OutlineLevel = OutlineLevel.Level2;
Levels nest: a Level2 entry goes under the last Level1 entry before it. The entry's text is the
heading's text, and following it takes the reader to the heading itself, not only to the top of its
page. In a tagged document the same level also makes the heading an H1 to H6 element. The
Bookmarks and outlines page shows how to build an outline
by hand when you draw with XGraphics.
To open the bookmarks panel when the reader opens the file, set the page mode after rendering:
var renderer = new PdfDocumentRenderer(unicode: true) { Document = report };
renderer.RenderDocument();
// The outline panel is worth opening on arrival, since the whole point of the page above
// is that it filled itself in.
renderer.PdfDocument.PageMode = PdfPageMode.UseOutlines;
Name a place with a bookmark
Two different things are called "bookmark", and choosing the wrong one is the usual reason nothing appears in the finished PDF:
| You want | You use |
|---|---|
| An entry in the reader's bookmarks panel | ParagraphFormat.OutlineLevel |
| A named place to link to, or to print the page number of | Paragraph.AddBookmark(name) |
AddBookmark adds a BookmarkField. It draws nothing. It names a point in the flow, and hyperlinks
and page references find that point by its name. Put it on the heading itself, so the two cannot
drift apart when the document reflows:
var chapter = bodyText.AddParagraph();
chapter.Style = StyleNames.Heading1;
chapter.AddBookmark("chapter-one");
chapter.AddText("1 Sections and headers");
To name a point between two blocks rather than inside a paragraph, call
section.Elements.AddBookmark(name).
Build a table of contents
A table of contents is a set of ordinary paragraphs. Each entry has a hyperlink to a bookmark, a tab,
and a PageRefField that prints the page the bookmark ended up on. A style with a right-aligned tab
stop and a dot leader gives every entry the same layout:
// The contents entries are a style too, carrying the tab stop the leader belongs to.
var entry = report.Styles.AddStyle("Entry", StyleNames.Normal);
entry.ParagraphFormat.SpaceAfter = Unit.FromPoint(2);
entry.ParagraphFormat.TabStops.AddTabStop(Unit.FromCentimeter(15),
TabAlignment.Right, TabLeader.Dots);
(string Bookmark, string Text)[] entries =
{
("chapter-one", "1 Sections and headers"),
("lists", "2 Lists"),
("references", "3 Cross-references and hyperlinks")
};
foreach (var item in entries)
{
var line = contents.AddParagraph();
line.Style = "Entry";
line.AddHyperlink(item.Bookmark).AddText(item.Text);
line.AddTab();
line.AddPageRefField(item.Bookmark);
}
The renderer resolves page references after it has laid out the whole document, so a reference to a bookmark later in the document works as well as one to a bookmark earlier on. The contents can come first, as they usually do.
PinataLayout does not collect the headings for you. You list the entries, as the demo does with its array. If you build the document from data, build the contents from the same data.
Cross-references
AddPageRefField works anywhere in the text, not only in a table of contents:
var crossRef = bodyText.AddParagraph("The lists above begin on page ");
crossRef.AddPageRefField("lists");
crossRef.AddText(", and this sentence knows that without anybody counting: a PageRefField "
+ "is resolved against its bookmark when the document is laid out.");
A hyperlink to the same bookmark takes the reader there. See Hyperlinks on the paragraphs page.
Sections and their numbers
A section is where a page setup and a set of headers and footers live, so a document with a title page, front matter and a body usually has one section for each. Fields count sections and pages:
| Field | Prints |
|---|---|
AddPageField() | The page number |
AddNumPagesField() | The number of pages in the document |
AddSectionField() | The number of the current section |
AddSectionPagesField() | The number of pages in the current section |
AddPageRefField(name) | The page a bookmark is on |
var sectionNote = bodyText.AddParagraph("This is section ");
sectionNote.AddSectionField();
sectionNote.AddText(" of the document, and it has ");
sectionNote.AddSectionPagesField();
sectionNote.AddText(" page(s) of its own.");
sectionNote.Style = "Caption";
PageSetup.StartingNumber sets the number of a section's first page, so a body can start again at
page 1 after the front matter. Section.AddPageBreak() starts a new page without starting a new
section. For headers, footers and page setup, see
Documents, sections and styles.
Things to know
BookmarkFieldis not an outline entry. Adding one gives the document a link target and nothing in the bookmarks panel. Set an outline level for that.- A misspelt bookmark name shows on the page. A
PageRefFieldwhose name matches no bookmark printsBookmark 'name' is not defined within the document.in place of the number. Search your output for that text. - Do not skip levels. A
Level3heading with noLevel2above it hangs from a blank entry, which the reader sees as an empty line in the panel. - A table of contents takes space. If the contents run over more pages than you expected, every page number after them moves. The renderer handles this, but a hand-typed page number would not.
- Headings are not styled for you. The predefined heading styles set only the outline level. Give
them a font size, weight and
KeepWithNext.
See it in action
The Structure demo builds a five-page report in three sections: a title page with no running head, a contents page whose entries are links with dotted leaders and resolved page numbers, and a body with headings that fill the bookmarks panel, all six list types, a page reference, hyperlinks and section fields.
The full Structure demo
var report = new Document
{
Info =
{
Title = "Structure"
}
};
// Every predefined style is already there and already related: Heading1 is based on Normal,
// Heading2 on Heading1, and so on, so setting the font on Normal reaches all of them.
// StyleNames holds the names rather than the styles - they are looked up on the document.
var normal = report.Styles[StyleNames.Normal];
normal.Font.Name = "Liberation Serif";
normal.Font.Size = 10.5;
normal.ParagraphFormat.SpaceAfter = Unit.FromPoint(6);
var heading1 = report.Styles[StyleNames.Heading1];
heading1.Font.Name = "Liberation Sans";
heading1.Font.Size = 18;
heading1.Font.Bold = true;
heading1.ParagraphFormat.SpaceBefore = Unit.FromPoint(18);
heading1.ParagraphFormat.SpaceAfter = Unit.FromPoint(8);
// OutlineLevel is the whole of the bookmark story. A paragraph carrying one becomes an
// entry in the PDF's outline when the document is rendered - DocumentRenderer.AddOutline
// is called for it - so nothing here ever mentions PdfOutline.
heading1.ParagraphFormat.OutlineLevel = OutlineLevel.Level1;
var heading2 = report.Styles[StyleNames.Heading2];
heading2.Font.Size = 13;
heading2.ParagraphFormat.SpaceBefore = Unit.FromPoint(12);
heading2.ParagraphFormat.OutlineLevel = OutlineLevel.Level2;
// A style of one's own, based on another. Everything not set here comes from Normal, so a
// change to Normal's font reaches this too - which is the point of basing rather than
// copying.
var caption = report.Styles.AddStyle("Caption", StyleNames.Normal);
caption.Font.Size = 8.5;
caption.Font.Italic = true;
caption.Font.Color = Colors.DimGray;
// The contents entries are a style too, carrying the tab stop the leader belongs to.
var entry = report.Styles.AddStyle("Entry", StyleNames.Normal);
entry.ParagraphFormat.SpaceAfter = Unit.FromPoint(2);
entry.ParagraphFormat.TabStops.AddTabStop(Unit.FromCentimeter(15),
TabAlignment.Right, TabLeader.Dots);
// ----- section one: a title page with a page setup of its own -----
var title = report.AddSection();
title.PageSetup.TopMargin = Unit.FromCentimeter(6);
title.PageSetup.LeftMargin = Unit.FromCentimeter(3);
title.PageSetup.RightMargin = Unit.FromCentimeter(3);
// A section's headers are its own. Leaving this one's empty is how a title page comes out
// without the running head the rest of the document has.
var titleLine = title.AddParagraph("A structured document");
titleLine.Format.Font.Name = "Liberation Sans";
titleLine.Format.Font.Size = 30;
titleLine.Format.Font.Bold = true;
titleLine.Format.SpaceAfter = Unit.FromPoint(12);
title.AddParagraph(
"Built entirely from the document object model. Nothing here positions anything: the "
+ "renderer decides where every paragraph goes, breaks the pages, numbers them and "
+ "resolves the cross-references.").Style = "Caption";
// ----- section two: the contents -----
var contents = report.AddSection();
contents.PageSetup.TopMargin = Unit.FromCentimeter(2.5);
// Primary is what every page of the section gets. FirstPage and EvenPage override it where
// they apply, and a reader sees whichever is most specific.
contents.PageSetup.DifferentFirstPageHeaderFooter = true;
contents.PageSetup.OddAndEvenPagesHeaderFooter = true;
var runningHead = contents.Headers.Primary.AddParagraph("A structured document");
runningHead.Format.Font.Size = 8;
runningHead.Format.Font.Color = Colors.DimGray;
runningHead.Format.Alignment = ParagraphAlignment.Right;
// Filled because DifferentFirstPageHeaderFooter is set above. Leaving it empty does not
// fall back to Primary - it means the first page of the section has no header at all,
// which on a section this short is every page a reader would look at for one.
var firstHead = contents.Headers.FirstPage.AddParagraph("First page of the section gets this one");
firstHead.Format.Font.Size = 8;
firstHead.Format.Font.Color = Colors.DimGray;
firstHead.Format.Alignment = ParagraphAlignment.Right;
var evenHead = contents.Headers.EvenPage.AddParagraph("Even pages get this one");
evenHead.Format.Font.Size = 8;
evenHead.Format.Font.Color = Colors.DimGray;
var footer = contents.Footers.Primary.AddParagraph();
footer.Format.Alignment = ParagraphAlignment.Center;
footer.Format.Font.Size = 8;
footer.AddText("Page ");
footer.AddPageField();
footer.AddText(" of ");
footer.AddNumPagesField();
contents.AddParagraph("Contents").Style = StyleNames.Heading1;
// The classic PinataLayout table of contents. A PageRefField names a bookmark and is resolved
// to that bookmark's page number when the document is laid out - which is why a TOC cannot
// be written by hand without being wrong the moment anything moves.
(string Bookmark, string Text)[] entries =
{
("chapter-one", "1 Sections and headers"),
("lists", "2 Lists"),
("references", "3 Cross-references and hyperlinks")
};
foreach (var item in entries)
{
var line = contents.AddParagraph();
line.Style = "Entry";
line.AddHyperlink(item.Bookmark).AddText(item.Text);
line.AddTab();
line.AddPageRefField(item.Bookmark);
}
contents.AddParagraph(
"The dots are a tab leader, set on the tab stop the entry's style carries. The page "
+ "numbers are PageRefFields, resolved during layout. The entries are hyperlinks to "
+ "the same bookmarks the numbers point at, so clicking one goes there.").Style = "Caption";
// ----- section three: the body -----
var bodyText = report.AddSection();
bodyText.PageSetup.TopMargin = Unit.FromCentimeter(2.5);
bodyText.PageSetup.DifferentFirstPageHeaderFooter = false;
var bodyHead = bodyText.Headers.Primary.AddParagraph("A structured document");
bodyHead.Format.Font.Size = 8;
bodyHead.Format.Font.Color = Colors.DimGray;
bodyHead.Format.Alignment = ParagraphAlignment.Right;
var bodyFooter = bodyText.Footers.Primary.AddParagraph();
bodyFooter.Format.Alignment = ParagraphAlignment.Center;
bodyFooter.Format.Font.Size = 8;
bodyFooter.AddText("Page ");
bodyFooter.AddPageField();
// A bookmark is invisible and draws nothing. It is a name attached to a point in the flow,
// and it is what a PageRefField and a hyperlink both resolve against.
var chapter = bodyText.AddParagraph();
chapter.Style = StyleNames.Heading1;
chapter.AddBookmark("chapter-one");
chapter.AddText("1 Sections and headers");
bodyText.AddParagraph(
"A section is where a page setup lives, so a document with a landscape appendix or "
+ "different margins for its front matter is a document with more than one section. "
+ "This one is the third: the title page and the contents each have their own.");
bodyText.AddParagraph(
"Headers and footers belong to the section too, and there are three of each. Primary "
+ "is what a page gets; FirstPage replaces it on the section's first page when "
+ "DifferentFirstPageHeaderFooter is set; EvenPage replaces it on even pages when "
+ "OddAndEvenPagesHeaderFooter is. Setting the flags is what makes the other two "
+ "reachable - filling them in without setting the flag does nothing at all.");
// A chapter that starts on a fresh page, which is also what gives the contents table
// three different page numbers to resolve to rather than three copies of one.
bodyText.AddPageBreak();
var listsHead = bodyText.AddParagraph();
listsHead.Style = StyleNames.Heading1;
listsHead.AddBookmark("lists");
listsHead.AddText("2 Lists");
bodyText.AddParagraph(
"ListInfo is the only list support in the library, and it hangs off a paragraph's "
+ "format rather than being a container of its own. Six types, and a flag that says "
+ "whether this paragraph carries on the list above it or starts a new one.");
foreach (var type in new[]
{
ListType.BulletList1, ListType.BulletList2, ListType.BulletList3,
ListType.NumberList1, ListType.NumberList2, ListType.NumberList3
})
{
for (var item = 1; item <= 3; item++)
{
var line = bodyText.AddParagraph(
item == 1 ? $"{type} - first item" : $"{type} - item {item}");
line.Format.SpaceAfter = Unit.FromPoint(1);
line.Format.LeftIndent = Unit.FromCentimeter(1);
line.Format.ListInfo = new ListInfo
{
ListType = type,
// False on the first item of a run and true after it. Getting this wrong is
// what makes a numbered list restart at one halfway down, or carry on from
// the list before it.
ContinuePreviousList = item > 1
};
}
}
bodyText.AddPageBreak();
var refsHead = bodyText.AddParagraph();
refsHead.Style = StyleNames.Heading1;
refsHead.AddBookmark("references");
refsHead.AddText("3 Cross-references and hyperlinks");
var crossRef = bodyText.AddParagraph("The lists above begin on page ");
crossRef.AddPageRefField("lists");
crossRef.AddText(", and this sentence knows that without anybody counting: a PageRefField "
+ "is resolved against its bookmark when the document is laid out.");
var links = bodyText.AddParagraph("A hyperlink comes in three shapes. ");
links.AddHyperlink("chapter-one").AddFormattedText("This one goes to chapter one",
TextFormat.Underline);
links.AddText(", inside the document. ");
links.AddHyperlink("https://github.com/PinataLabs/PdfPinata", HyperlinkType.Web)
.AddFormattedText("This one leaves it", TextFormat.Underline);
links.AddText(", to a URI. A third form points at a file on disk.");
var outlineNote = bodyText.AddParagraph();
outlineNote.Style = "Caption";
outlineNote.AddText(
"Every heading on this page is a bookmark in the PDF's outline panel, and nothing in "
+ "this demo calls Outlines.Add. ParagraphFormat.OutlineLevel on the Heading1 and "
+ "Heading2 styles is what does it, and the tree's shape follows the levels.");
// Section-level fields, which count within the section rather than the document.
var sectionNote = bodyText.AddParagraph("This is section ");
sectionNote.AddSectionField();
sectionNote.AddText(" of the document, and it has ");
sectionNote.AddSectionPagesField();
sectionNote.AddText(" page(s) of its own.");
sectionNote.Style = "Caption";
var renderer = new PdfDocumentRenderer(unicode: true) { Document = report };
renderer.RenderDocument();
// The outline panel is worth opening on arrival, since the whole point of the page above
// is that it filled itself in.
renderer.PdfDocument.PageMode = PdfPageMode.UseOutlines;