Declarative Document-Building API
PdfGenerator.CreateDocument builds a PDF directly from C# — no HTML, no CSS strings — for callers who want a code-first, fluent API instead of authoring markup. It’s built entirely on PeachPDF’s own HTML/CSS layout engine (flexbox for rows and columns, the real table layout algorithm for tables, the same list-marker and running-header/footer machinery HTML documents use), so a value like PdfLength.Points(12) or PdfColor.FromRgb(200, 0, 0) resolves through exactly the same length/color parsing PeachPDF already uses for HTML — this is a different way to construct a document, not a second rendering engine. For the underlying HTML/CSS feature set this API is built on, see HTML & CSS Support; for everyday HTML-string rendering, see Usage Examples.
All examples assume:
using PeachPDF;
using PeachPDF.Layout;
Contents
- Quick start
- Pages: size, margin, background
- Containers: padding, border, background, corner radius, shadow
- Class, Id, Tag, and a document-level stylesheet
- Text and rich text
- Images
- HTML fragments and slots
- Hyperlinks and bookmarks
- Rows and columns
- Tables
- Lists
- Headers, footers, and page numbers
- Merging documents
- Adding existing PDFs
- Known v1 limitations
Quick start
var generator = new PdfGenerator();
var document = await generator.CreateDocument(doc =>
{
doc.Page(page =>
{
page.Size(PageSize.A4);
page.Margin(20);
page.Content(container =>
{
container
.Padding(20)
.Border(1, PdfColor.FromHex("#CCCCCC"))
.CornerRadius(8)
.Column(column =>
{
column.Spacing(10);
column.Item().Text("Hello, declarative PeachPDF!");
column.Item().Text(t =>
{
t.Span("Bold red text").FontColor(PdfColor.Red).Bold();
});
});
});
});
});
var stream = new MemoryStream();
document.Save(stream);
doc.Page(...) can be called more than once to append further pages, and PdfGenerator.AddPages adds more pages to a PeachPdfDocument you already have (mirroring AddPdfPages on the HTML side). A document built with no PdfGenerateConfig at all defaults to A4 with 20pt margins; pass one to CreateDocument/AddPages for PDF metadata, PDF/A conformance, tagged PDF, compression, or a custom network loader — every PdfGenerateConfig option that isn’t specific to HTML parsing applies here too (PDF/A conformance, file attachments and ZUGFeRD / Factur-X e-invoices among them, set on the config exactly as for an HTML document).
Pages: size, margin, background
page.Size(PageSize.Letter); // a named page size...
page.Size(PdfLength.Millimeters(210), PdfLength.Millimeters(297)); // ...or an explicit one
page.Orientation(PageOrientation.Landscape);
page.Margin(20); // all four sides
page.MarginTop(30); // one side at a time; also MarginBottom/Left/Right/Horizontal/Vertical
page.Background(PdfColor.White);
page.DefaultTextStyle(style => style.FontFamily("Georgia", "serif").FontSize(11));
Any geometry a page doesn’t set falls back to the document’s own PdfGenerateConfig, the same way an HTML document’s @page rule falls back to it.
Containers: padding, border, background, corner radius, shadow
IContainer is the one composable box used everywhere content is placed — a page’s content area, a Row/Column item, a table cell. Every decorator wraps the current box and returns the new, innermost container, so they chain:
container
.Padding(16)
.Border(2, PdfColor.FromRgb(30, 30, 30))
.CornerRadius(12)
.Background(PdfColor.FromRgb(245, 245, 245))
.BackgroundLinearGradient(45, PdfColor.FromHex("#FFDEE9"), PdfColor.FromHex("#B5FFFC"))
.Shadow(new PdfBoxShadow(PdfColor.FromArgb(60, 0, 0, 0), offsetX: 0, offsetY: 4, blur: 12))
.Width(300)
.Text("A card");
BorderLinearGradient(width, angleDegrees, stops) paints a gradient border instead of a solid one. Lengths accept a bare number (points) or an explicit unit — PdfLength.Pixels(16), PdfLength.Inches(1), PdfLength.Percent(50), PdfLength.Em(1.5) — resolved through PeachPDF’s own CSS length parser.
LineHorizontal(thickness, color)/LineVertical(thickness, color) place a simple rule — a solid-filled line by default, or dashed: true for a dashed one:
container.LineHorizontal(1, PdfColor.FromHex("#BBBBBB"), dashed: true);
Class, Id, Tag, and a document-level stylesheet
Class/Id/Tag tag a container the same way an HTML element’s class/id/tag name would, so a stylesheet
attached to the whole document (IDocumentBuilder.Stylesheet) can target it with a real CSS selector:
var stylesheet = await generator.ParseStyleSheet("""
.card { border: 1px solid #ccc; }
.card .title { font-weight: 700; }
#hero { background: #f5f5f5; }
""");
var document = await generator.CreateDocument(doc =>
{
doc.Stylesheet(stylesheet);
doc.Page(page =>
{
page.Content(container =>
{
container.Id("hero").Class("card").Column(column =>
{
column.Item().Class("title").Text("Card title");
});
});
});
});
Selector matching is real and general - class, id, compound, descendant, attribute, and :not()/:is()-style
selectors all work, the same selector engine HTML rendering uses. Tag(name) renames a container’s own internal
tag (every anonymous container defaults to a synthetic "div", an image to "img", a hyperlink to "a") so a
bare type selector like li { ... } can target it precisely - without a rename, a type/universal selector still
matches this API’s own internal structure (div/img/a), so prefer class/id/Tag for predictable targeting.
An explicit builder call always outranks a plain (non-!important) stylesheet rule for the same property - the
same precedence an inline style="" attribute has over an author stylesheet - but a matching !important rule
still wins, matching ordinary CSS:
// stylesheet: .card { padding: 8pt; }
container.Class("card").Padding(20); // ends up 20pt - the explicit call wins
IDocumentBuilder.Stylesheet can be called anywhere in the document-building callback (Page only records its
own handler; every page’s content tree, and this stylesheet’s effect, are built afterward) - only the last call
is kept. Class/Id/Tag have no visual effect at all when no stylesheet is attached; they’re a stable hook a
document can add before it needs styling.
A stylesheet’s @font-face/@property/@font-palette-values rules take effect too - fonts register, custom
properties resolve through var() - with the same precedence rule extended to @page: a base @page rule’s
margin/size only fills in whatever the page’s own Margin*/Size call left unset, ranking between an
explicit call and the document’s PdfGenerateConfig default. PageName(name) sets the CSS page property
(CSS 2.1 §13.2) so a document-level @page name { ... } rule can target this point in the flow onward:
var stylesheet = await generator.ParseStyleSheet("@page chapter { size: landscape; }");
column.Item().PageName("chapter").Text("A landscape chapter starts here.");
Stamping pages of an existing PDF
IDocumentBuilder.OverlayFile and UnderlayFile draw pages of an existing PDF over or under the document’s own pages - a watermark, a “DRAFT” stamp, a letterhead, a form background:
var document = await generator.CreateDocument(doc =>
{
// The letterhead sits behind every page's own content.
doc.UnderlayFile(layer => layer.File("letterhead.pdf").SourcePages("1").RepeatSourcePages("1"));
// A stamp on the last page only.
doc.OverlayFile(layer => layer.File(stampBytes).TargetPages("z"));
doc.Page(page => page.Content(container => container.Text("...")));
});
File takes a path, the bytes of a PDF, or a stream. Page numbers are one-based and a range is a comma-separated list of pages and spans - "1", "1-3,5", "2-z" - where z is the last page and a backwards span ("z-1") runs backwards.
TargetPagesnames the pages of the finished document that receive the layer (every page when unset). It is read against the page count of the whole document, sozis the last page whicheverPagecall produced it.SourcePagesnames the file’s pages to use (every page of the file when unset). The target pages take them in order: the first target page the first source page, the second the second, and so on.RepeatSourcePagesnames the file pages to cycle through onceSourcePagesruns out. Without it the layer stops there, and the remaining target pages get none.SourcePages("1")withRepeatSourcePages("1")puts the file’s first page on every target page.
Layers apply in the order they were added, and an underlay is drawn before the page’s own background and content. A layer page is drawn into the new document (its text, paths, clipping, colour and transparency become ordinary content of the new page, with fonts re-embedded and the text keeping its text map), not copied object for object; it is clipped to its own page box and placed at the top-left of the page it lands on. Images (JPEG, JPEG 2000, CCITT fax, JBIG2 and the Flate/LZW/run-length family, with stencil, colour-key and soft masks) axial, radial and mesh gradients, and luminosity and alpha soft masks come across too. What the importer cannot draw yet - and fonts that are not embedded as a TrueType, OpenType, CFF or Type 1 program - is left out of the layer, so check a layer file renders as you expect. A file protected with AES (128 or 256 bit) and the empty user password (the usual “no editing or copying” protection) is opened; one that needs a password to open, or that uses RC4, throws NotSupportedException. A file that is not a PDF throws InvalidDataException, and a malformed range throws FormatException, both before any page is drawn.
A layer also brings the navigation of the pages it draws. Their links (a web address, or a link to another page of the file) are added to the page they land on, a link to a page of the file leading to the page that page was drawn on; a link to a page that was not drawn is left out. The file’s named destinations that lead to a drawn page, and its outline entries for drawn pages (with the entries they nest under, kept as plain headings), join the document’s own; the outline comes after the document’s own entries. The file’s title, author, subject, keywords and language fill whichever of those the document does not set itself, and its attached files are attached to the document (not for PDF/A-1 and PDF/A-2, which do not allow attachments). A file’s PDF/A or PDF/X claim is never taken over, since the document’s content is not the file’s: the document conforms to the level its own PdfAConformance and PdfXConformance ask for. The one marker that is used is the file’s output intent, for a document that was not given an output intent profile of its own and is not building to PDF/A (which brings its own): a document building to PDF/X needs one, and any other document then records what its colours are meant for, without claiming a level. A layer page’s interactive form fields (text fields, multi-line text fields, check boxes, radio buttons, drop-downs, list boxes, push buttons and signature fields) are added to the page as real fields, drawn with the appearance the file gave them; a page drawn more than once gets its fields again under a numbered name (name_2). Its other annotations (highlights, underlines, notes, stamps, drawings, shapes) are added as real annotations with their contents, colours, opacity, border width, marked regions, ink strokes and appearance, so they can still be selected and edited in a reader; one with no appearance stream, one marked hidden, and pop-up windows are left out. Actions are not carried: a push button does nothing, and a signature field comes across unsigned (the signature would no longer match the new document). When the document is tagged (EnableTaggedPdf), the logical structure of a layer’s pages comes across too: the headings, paragraphs, lists, tables and figures the file tagged (with a figure’s alternative text) join the document’s structure tree, using the standard type a custom tag’s role map names, and everything the file did not tag is marked as an artifact so the layer adds no untagged content. Each element keeps its language, actual text, title and attributes (a table header’s scope, a cell’s spans), a link’s structure element is tied to its annotation, and the elements are in the order of the file’s structure tree rather than the order they are drawn in. Elements with no marked content or link on the page, class maps and element identifiers are not carried. In an untagged document nothing is marked.
The same importer is available on its own as PdfPageSource in PeachDrawing.PDF.
Text and rich text
container.Text("Plain paragraph text.");
container.Text(t =>
{
t.Alignment(TextAlignment.Justify);
t.Span("Regular, ");
t.Span("bold").Bold();
t.Span(", and ");
t.Span("italic red").Italic().FontColor(PdfColor.Red);
t.Span(" text, plus ");
t.Span("underlined").Underline().DecorationColor(PdfColor.Blue).DecorationStyle(PdfTextDecorationStyle.Wavy);
t.Span(" and a ");
t.Span("superscript").Superscript();
t.Span(" footnote marker.");
});
ITextStyle (shared by DefaultTextStyle and every span) also covers FontWeight(int) (a raw 1–1000 weight, not just Bold()), LetterSpacing/WordSpacing, LineHeight, Subscript(), FontFeature(tag, enabled) for OpenType features, Direction(PdfTextDirection.Rtl) for right-to-left paragraphs (or PdfTextDirection.Auto to detect it from the span’s own text — the same first-strong-character detection dir="auto" uses on the HTML side), and BreakAnywhere(). A container itself has ParagraphFirstLineIndentation, ParagraphSpacing, and ClampLines(n, ellipsis) to truncate a block after a fixed number of lines, marking the cut with the usual “…” by default, a custom string (e.g. ClampLines(2, "… [continued]")), or nothing at all (ClampLines(2, "")). ITextSpanContainer.Element(content => ...) inserts arbitrary container content — an image, a styled box — inline between spans.
Images
container.Image(imageBytes); // byte[] - raster or SVG, detected automatically
container.Image(stream); // Stream, read fully
container.Image(new Uri("https://example.com/logo.png"));
container.Image(@"C:\assets\logo.png"); // local file path
// Reuse the same resolved image in more than one place - decoded once, not once per placement:
var logo = PdfImage.FromFile(@"C:\assets\logo.png");
container.Image(logo);
otherContainer.Image(logo);
Image(byte[])/Image(Stream) decode directly into the image the container places (no data: URI round trip); Image(Uri)/Image(string filePath) still load lazily, over the network or from disk. PdfImage.FromFile/FromBytes/FromStream load once and cache the decoded result, so placing the same PdfImage instance in more than one container (even across pages) decodes its source only the first time.
An image placed with Image sizes itself from its own intrinsic dimensions (the raster’s pixel size, or an SVG’s width/height/viewBox) unless the image itself - not the container it’s chained onto - is given an explicit size; Width/Height called before Image/Svg sizes the container the image sits in, which the image doesn’t automatically stretch to fill (ordinary CSS replaced-element sizing - the same as an <img> with no width/height of its own inside a sized <div>). To make image content fill a container of a known size, generate or author it at that size directly, or use the dynamic overloads below, which do fill by default.
Standalone SVG
container.Svg(svgMarkup); // string
container.Svg(stream); // Stream, read fully
container.Svg(svgBytes); // byte[]
Parses SVG markup directly into a real vector image, painted with PeachPDF’s own SVG renderer - the same one an inline <svg> or <img src="x.svg"> already uses on the HTML side, so anything on the SVG feature matrix works here too.
Dynamic (size-aware) content
container.Width(300).Height(160).Image(size =>
{
// size.Width/size.Height are the container's own resolved point size - generate content
// at exactly that size instead of a guessed fixed one.
return RenderChartAsPng((int)size.Width, (int)size.Height);
});
container.Width(300).Height(160).Svg(size => RenderChartAsSvgMarkup(size.Width, size.Height));
Image(Func<PdfSize, byte[]>)/Svg(Func<PdfSize, string>) run their callback once layout knows the container’s resolved size, and - unlike the other overloads above - fill that container by default (width: 100%; height: 100%), so the generated content is sized to fit rather than needing its own explicit dimensions. The container itself still needs a definite resolved size for this to mean anything: an explicit Width/Height, or an ancestor that already has one (the page content area, a table cell with a definite column width, a Row/Column item with an explicit size); an indefinite container throws InvalidOperationException rather than generating content at a garbled or zero size. Each callback runs exactly once, even across the layout engine’s own internal convergence passes.
HTML fragments and slots
Html(markup) parses an HTML fragment (through the same parser and cascade the HTML-string rendering path
uses) and splices it into a container’s tree in place:
container.Html("""
<table>
<tr><td>Row 1</td><td>Data</td></tr>
<tr><td>Row 2</td><td>Data</td></tr>
</table>
""");
Unlike every other terminal method here, a fragment can contain more than one top-level element - real HTML
semantics apply throughout (UA default styles, an anonymous-table fixup for a bare <tr>, the fragment’s own
<style> tag). Pass a stylesheet for the fragment’s own class/id-driven styling to cascade against:
var cardStyles = await generator.ParseStyleSheet(".highlight { background: yellow; }");
container.Html("""<p class="highlight">Important</p>""", cardStyles);
A <link rel="stylesheet"> inside the fragment is never loaded (there is no document context to resolve it
through at this point in building the tree) - author a <style> tag, or pass CSS via the stylesheet parameter,
instead. Stream/byte[] overloads exist alongside the string one, matching Svg’s own shape.
Slots
A fragment can declare replaceable <slot> points, Web-Components style:
container.Html(
"""<div class="card"><slot name="body">Loading…</slot></div>""",
onSlot: (slot, slotContainer) =>
{
if (slot.Name == "body")
{
slotContainer.Text("Real content, filled in from C#.");
}
});
onSlot fires once per <slot> element in the fragment (in document order, regardless of name), with a
SlotContext (its Name and Attributes) and an IContainer positioned to replace it - populate it the same
way any other container is populated. A slot the callback leaves untouched (including when onSlot is null
altogether) keeps its own fallback content - whatever markup it contained - exactly as authored.
Hyperlinks and bookmarks
container.Hyperlink("https://peachpdf.net/").Text("Visit our site");
container.Bookmark("Chapter One", level: 1).Text("Chapter One");
Hyperlink produces a real PDF link annotation; Bookmark adds a real PDF outline (sidebar) entry, at the given nesting level (1 = top level) — both reuse the same generic link/bookmark machinery an HTML <a href>/heading already gets.
Rows and columns
Backed by PeachPDF’s flexbox layout engine:
container.Row(row =>
{
row.Spacing(8);
row.Item().Width(80).Height(40).Background(PdfColor.Red);
row.Item().Grow().Height(40).Background(PdfColor.Green); // fills remaining space
row.Item().Width(80).Height(40).Background(PdfColor.Blue);
});
container.Column(column =>
{
column.Spacing(6);
column.Item().Text("First line");
column.Item().Text("Second line");
});
Grow(ratio) maps to flex-grow; combine it with an explicit Width/Height on sibling items for a “fixed items plus one flexible item” layout.
AlignLeft()/AlignCenter()/AlignRight() position a container within its parent (with auto margins, so give it a Width narrower than the parent - or leave it auto-width, and it shrinks to its content). They work on both Row and Column items. Text inside a container is aligned separately, with Alignment(...) on the text (t.Alignment(TextAlignment.Right)).
Tables
Backed by the real CSS table layout algorithm, including colspan/rowspan:
container.Table(table =>
{
table.Columns(columns =>
{
columns.FixedColumn(80); // an absolute width
columns.RelativeColumn(2); // shares the remaining width, weighted against other relative columns
columns.RelativeColumn(1);
});
table.Header(header =>
{
header.Cell().Text("ID");
header.Cell().Text("Name");
header.Cell().Text("Amount");
});
table.Row(row =>
{
row.Cell().Text("1");
row.Cell(columnSpan: 2).Text("A row spanning two columns");
});
});
To right-align a column of amounts, align the cells’ text — row.Cell().AlignRight().Text("3.20") (or Text(t => { t.Alignment(TextAlignment.Right); t.Span("3.20"); })). Margins do not apply to a table cell, so on a cell AlignLeft()/AlignCenter()/AlignRight() align the cell’s content instead of positioning it.
Header/Footer repeat on every physical page the table spans, exactly like a real <thead>/<tfoot>.
Lists
Backed by PeachPDF’s own list-style-type/list-style-position/list-style-image support, which already covers dozens of numbering systems:
container.OrderedList(list =>
{
list.Item().Text("First");
list.Item().Text("Second");
}, PdfListMarkerType.UpperRoman); // → I., II., ...
container.UnorderedList(list =>
{
list.Position(PdfListMarkerPosition.Inside);
list.Item().Text("Bulleted");
}, PdfListMarkerType.Square);
IListDescriptor.MarkerImage(...) uses an image as every item’s marker instead of a bullet/number; MarkerText("→ ") uses a literal custom marker string.
Headers, footers, and page numbers
page.Header(header =>
{
header.Text("My Document");
});
page.Footer(footer =>
{
footer.Text(t =>
{
t.Alignment(TextAlignment.Center);
t.Span("Page ");
t.CurrentPageNumber();
t.Span(" of ");
t.TotalPages();
});
});
Header/Footer content repeats at the top/bottom of every physical page the page’s own content paginates across (the same position: running() + @page margin-box mechanism an HTML document uses for repeating headers). CurrentPageNumber()/TotalPages() only resolve inside Header/Footer content — a page number has no meaning in ordinary flowing content.
Sectioned page numbers
A page count scoped to a named part of the document, rather than the whole thing - “Page 2 of 5” within a chapter, not the whole report:
container.Column(column =>
{
column.Item().Text("Cover / table of contents...");
column.Item().BeginPageNumberOfSection("chapter1").Text("Chapter 1");
column.Item().Text("Chapter 1 content, spanning several pages...");
column.Item().EndPageNumberOfSection("chapter1").Text("End of Chapter 1.");
column.Item().Text("Appendix...");
});
page.Footer(footer => footer.Text(t =>
{
t.Span("Page ");
t.PageNumberWithinSection("chapter1");
t.Span(" of ");
t.TotalPagesWithinSection("chapter1");
}));
BeginPageNumberOfSection(id)/EndPageNumberOfSection(id) are decorators, like Bookmark - they tag whatever container they’re chained onto (rather than placing content of their own), so chain one before that container’s own terminal content and it can go anywhere in a page’s normal content flow (not just Header/Footer). PageNumberWithinSection(id)/TotalPagesWithinSection(id) then resolve against the physical pages that pair of tagged containers land on, the same way CurrentPageNumber()/TotalPages() do - only inside Header/Footer content, and continuously renumbered per document page. The current-page-within-section number is deliberately unclamped outside the section itself: it climbs by exactly one per document page throughout the whole document, so it reads non-positive before the section starts and past the total after it ends - a footer that only wants to show it inside the section’s own pages should condition on that itself. An id with no matching BeginPageNumberOfSection call (a typo, or a forgotten call) resolves to 1 rather than throwing, the same fallback an unresolved target-counter() reference already uses.
Merging documents
DocumentMerge writes several declarative documents into one PDF. Each part is the callback that builds one document, exactly what CreateDocument takes:
static Action<IDocumentBuilder> Report(string title) => doc => doc.Page(page =>
{
page.Size(420, 595).Margin(30);
page.Header(h => h.Text(title));
page.Content(c => c.Text("..."));
page.Footer(f => f.Text(t => { t.CurrentPageNumber(); t.Span(" of "); t.TotalPages(); }));
});
var merge = DocumentMerge.Of(Report("Q1"), Report("Q2")).UseContinuousPageNumbers();
var document = await generator.CreateMergedDocument(merge);
AddMergedPages(document, merge) appends a merge to a document that already has pages, the way AddPages does for one document.
UseOriginalPageNumbers()(the default) lets each part number its own pages: two parts of 2 and 3 pages read 1, 2, 1, 2, 3, each footer with its own total.UseContinuousPageNumbers()makes the parts one run: the same parts read 1, 2, 3, 4, 5, andTotalPages()is 5 in every footer.
Numbering the parts as one run needs every page count before any page is drawn, so the parts are laid out once to count and again to draw, and each callback runs twice: build the document from the callback’s own state, not from anything that must happen once. Only the page numbers a document prints change (counter(page), counter(pages) and target-counter(..., page)); @page rules (:first, :left, :right) still select by a page’s place in its own part, so each part’s cover page is its own :first page. A part with several Page calls numbers them as one run in both modes, where a document built by CreateDocument alone restarts the numbering at each Page call. File layers added inside a part (OverlayFile, UnderlayFile) read their target pages against the merged document, so "1" is the first page of the first part.
The PdfGenerateConfig passed to CreateMergedDocument applies to every part. To put PDF files that already exist in the document, see Adding existing PDFs below; to work on the saved files themselves, use PdfOperation from PeachDrawing.PDF.
Adding existing PDFs
Append and Prepend on the document builder write the pages of an existing PDF into the document being generated, as a section like a Page call is. The sections come out in the order of the calls:
var document = await generator.CreateDocument(doc =>
{
doc.Append(File.ReadAllBytes("cover.pdf")); // first: nothing comes before it
doc.Page(page =>
{
page.Size(PageSize.A4);
page.Margin(20);
page.Content(c => c.Text("The permit"));
});
doc.Append(File.ReadAllBytes("regulations.pdf")); // last
});
Prepend puts the PDF before everything else wherever it is called (several Prepend calls keep their order). Append and Prepend take the bytes of a file, a Stream, or a PdfOperation, which chooses pages (TakePages("2-5")), joins several files (MergeFile) or stamps a layer on them (OverlayFile) before they are added. A file that needs a password takes it as the password argument.
- The pages become part of the document, so its
PdfGenerateConfigcovers the whole file: PDF/A or PDF/X conformance, encryption, linearization and the metadata are the document’s own. The added pages are not tagged, so a document that claims an accessible level (PDF/UA, PDF/A-1a, 2a or 3a) does not get tags for them. An operation that encrypts, linearizes, attaches files or extends the metadata is refused, since those belong to the whole file; set them in the config. The title, author and the other document information of an added file are not taken, nor are its attachments. - The links, outline, named destinations, form fields and annotations of an added file are carried across and lead to the pages they now are on. A form field whose name an earlier added file already uses is renamed (
total,total_2). Named destinations of an added file keep their names, so one that shares a name with an anchor of the generated pages (anida link jumps to) is ambiguous: avoid reusing the same names. - The
modeargument chooses how the pages are taken.PdfCopyMode.Redraw(the default) draws each page again into the document, so it follows the document’s conformance level and colour settings;PdfCopyMode.ObjectCopycopies the page objects as they are, which keeps the file’s own content byte for byte (its fonts, images and text stay exactly as they were) but does not bring it into line with the document’s PDF/A or PDF/X profile. UseContinuousPageNumbers()on the builder numbers the generated pages as one run with the added ones (every generated section is laid out twice, once to count and once to draw): with a two-page PDF before a one-page document the footer reads “3 of 3”. Without it the generated pages number from 1 and count only themselves. The numbers a document prints change, and@pagerules (:firstand the like) still select by a page’s place among the generated pages.- File layers (
OverlayFile,UnderlayFile) read their target pages against the whole document and are drawn on the generated pages only; the pages added from other PDFs are left as they are.
A DocumentMerge part can use Append and Prepend too. With UseContinuousPageNumbers() on the merge the added pages are counted in the run; with original numbering they count only if the part’s builder called UseContinuousPageNumbers() itself.
Known v1 limitations
- No
Placeholderprototyping element — unlike every other terminal method here, a placeholder has no CSS mapping at all, so it needs genuinely new paint code rather than a wrapper over an existing property. - No per-image compression/DPI override, or
ShrinkToFit/ScaleToPageSizefor a declarative document — each of these needs a re-run of the whole builder callback that a hand-built tree has no equivalent for; document-wide settings onPdfGenerateConfig(DownscaleImages,PixelsPerInch, …) still apply. - A bare type or universal selector in a document-level stylesheet matches this API’s own internal box structure — every anonymous container defaults to a synthetic
"div"tag (img/afor images/hyperlinks) unless renamed withTag(...), so a rule likediv { ... }or* { ... }matches broadly; prefer class/id selectors, orTag(...), for predictable targeting. - No vertical writing modes for the declarative API — a document-level stylesheet’s
writing-moderesolves onto individual boxes’ computed style but the document root stays fixed athorizontal-tbregardless (a separate, much larger feature than this one, and orthogonal to it). - A
<link rel="stylesheet">inside anHtml(...)fragment is never loaded — there is no document context yet at the point a fragment is spliced in to resolve a network/relative URL through; author a<style>tag, or pass CSS viaHtml’s own stylesheet parameter, instead. - A
float: footnoteelement inside anHtml(...)fragment renders as ordinary inline content rather than being detached into a footnote area — footnote detachment needs a real document context the fragment doesn’t have at splice time.