Recursive AI

Migrating from QuestPDF

All onepdf docs

OnePdf.Layout generates documents from code in the same style as QuestPDF: you describe pages with nested delegates, chain decorators on a place that holds one child, and fill it with text, a column, a table and so on. This guide maps the concepts a QuestPDF program uses to their OnePdf.Layout counterparts and lists what differs and what is missing. It is based on QuestPDF's public documentation (features overview and the pages it links to); check those for details of QuestPDF's behavior. The names here are OnePDF's own, so this is a map, not a drop-in replacement: expect to rename calls, not to rethink the structure.

OnePdf.Layout ships as the package RecursiveAI.OnePdf.Layout (see layout.md).

What is the same

  • A document is composed in a delegate, once; layout and drawing happen at save time.
  • Each page has a size, margins and named slots for header, content, footer, background and foreground.
  • A slot takes decorators (padding, border, background, alignment, size limits) and one child.
  • Column, Row, Layers, Table and List take delegates that add items.
  • Reusable pieces are classes that implement a one-method interface.
  • Page numbers, hyperlinks, internal links, page breaks and keep-together rules exist, and a layout that cannot fit throws an exception that names the element path.
  • Units are points, with helpers for millimetres, centimetres and inches.

Concept map

QuestPDF conceptOnePDF.Layout
Compose a documentLayoutDocument.Compose(doc => ...), or PdfDocument.Compose(...)
Generate a file / stream / byteslayout.Save(path), layout.Save(stream, PdfSaveOptions), layout.ToBytes()
Page with size and margindoc.Page(page => { page.Size(PageSize.A4); page.Margin(2.Cm()); ... })
Header / content / footer / background / foreground slotspage.Header(), Content(), Footer(), Background(), Foreground()
Different first or even page headerFirstPageHeader(), EvenPageHeader() (and the footers)
Padding, border, background, rounded cornersPadding, Border, Background, Rounded (per-side variants, Start/End for direction-aware sides)
Width / height, min / maxWidth, Height, MinWidth, MaxHeight and so on
Aspect ratioAspectRatio
AlignmentAlignLeft, AlignCenter, AlignRight, AlignTop, AlignMiddle, AlignBottom, plus AlignStart/AlignEnd
Extend to the available spaceExpandHeight() (AlignMiddle and AlignBottom imply it)
Column, RowColumn with Item(); Row with ConstantItem, RelativeItem, AutoItem
Layers (watermark, letterhead)Layers with Layer() and PrimaryLayer()
Table, spans, repeating header and footerTable: Columns, HeaderRow, Row, FooterRow, cell.Span(columns, rows)
ListsList: Bulleted, Numbered, custom markers
ImageImage(bytes or path) with FitWidth, FitHeight, Contain, Cover, Stretch (JPEG and PNG)
Canvas-style drawingCanvas(height, surface => ...)
Rotate, scale, offsetRotate, Scale, Translate
Prevent page break, ensure space, page breakKeepTogether, EnsureSpace, PageBreak (and KeepWithNext)
Text, spans, stylesText(...) with FontSize, Bold, FontColor, ...; Text(t => t.Span(...))
Page numberst.CurrentPageNumber(), t.TotalPages(), t.SectionPages() inside Text(t => ...)
Hyperlinkslot.Hyperlink(uri)
Section and internal linksAnchor(id), LinkToAnchor(id), Bookmark(title, level)
ComponentsIComponent with Compose(Slot slot); slot.Component(instance) or slot.Component<T>()
Content directiondoc.RightToLeft(), page.RightToLeft(), slot.RightToLeft()
Debug area, debug pointerdoc.Debug() draws every element's box and name; slot.Name("...") labels an element in errors
Document metadata, PDF/Adoc.Title(...), doc.Language(...); PDF/A and encryption through Save(stream, PdfSaveOptions)

A page, side by side

A header, a body with a table and a footer with page numbers, written the OnePDF way:

C#
LayoutDocument.Compose(doc =>
{
    doc.Title("Invoice 1042").Language("en");
    doc.Page(page =>
    {
        page.Size(PageSize.A4);
        page.Margin(2.Cm());
        page.Header().Text("Acme Corp").FontSize(9).FontColor(Colors.Grey);
        page.Content().Column(col =>
        {
            col.Spacing(8);
            col.Item().Heading(1).Text("Invoice 1042").FontSize(20).Bold();
            col.Item().Table(table =>
            {
                table.Columns(c => { c.Relative(3); c.Constant(70); });
                table.HeaderRow(r => { r.Cell().Text("Item").Bold(); r.Cell().AlignEnd().Text("Price").Bold(); });
                table.Row(r => { r.Cell().Text("Widget"); r.Cell().AlignEnd().Text("12.00"); });
            });
        });
        page.Footer().AlignCenter().Text(t =>
        {
            t.CurrentPageNumber();
            t.Span(" / ");
            t.TotalPages().ReservedDigits(2);
        });
    });
}).Save("invoice.pdf");

If you know QuestPDF, the shape is the same: slots, decorator chains and nested delegates. The differences are in the details below.

Differences that matter first

QuestPDFOnePDF.Layout
Decorator orderEach decorator wraps what follows (see its documentation for details)The same, and neighbouring box decorators are merged into one box when their order allows it. The result is identical either way.
Text alignmentChosen on the textChosen on the text. Text fills the width it is offered, so AlignCenter().Text(...) centres the text itself.
Total pagesPart of the textWidth is reserved before the total is known: ReservedDigits (default 4). Use ReservedDigits(2) in short documents so a centred footer has no gap.
Tagged PDFOpt in for accessibilityOn by default: paragraphs, tables, lists, figures and links are tagged; headers, footers, borders and backgrounds are artifacts. Add Heading(n), AltText, Language; Tagged(false) turns it off.
ShapingFonts and shaping from its native stackOneFont, managed: OpenType GSUB/GPOS, Arabic, Indic, USE and Thai shapers, UAX #9 bidi, UAX #14 line breaking, per-span font fallback.
FontsFonts registered with the librarydoc.Fonts.Add(bytes, family); default Liberation Sans from RecursiveAI.OnePdf.Fonts, embedded as a subset. System fonts are never used unless you add them, so output is the same on every machine.
DependenciesNative binariesPure managed, BCL only, NativeAOT- and trim-compatible
MemorySee its documentationPages are written as they are laid out; 10,000 one-line pages need about 68 MB peak resident (NativeAOT, linux-x64)
OverflowLayout exception with element pathPdfLayoutException with the path (Page[3] > Content > Table > Row 14 > Cell 2 > Image), and the space offered and wanted
PreviewingA companion app with hot reloadNone; render with your own viewer, or with RecursiveAI.OneImage.Pdf

Licensing

QuestPDF is licensed under its Community, Professional and Enterprise licenses; whether you need to pay depends on who you are (for example an organisation's annual gross revenue). Read the current terms on its license selection guide and legal terms page. OnePdf.Layout is MIT OR Apache-2.0 like the rest of OnePDF, with no revenue test and no registration. The font packages are OFL 1.1 and ship their notices.

What is missing

Things QuestPDF's public documentation lists that OnePdf.Layout does not have yet:

  • Shadows and scale-to-fit, flip, unconstrained and shrink wrappers.
  • Inline flow of arbitrary elements (an inlined layout); hyperlinks inside a text span (put Hyperlink on the slot that holds the text instead). Elements embedded in text are supported: see inline objects.
  • Show-once / skip-once / show-if helpers and lazy content. (Multi-column flow and table of contents generation are supported: see layout.) Plain C# if statements and loops in your delegates replace the conditional ones; header-only-on-first-page is FirstPageHeader().
  • A page number inside a paragraph keeps the paragraph on one line; text with page numbers does not wrap.
  • Charts, barcodes and maps (draw them with Canvas or pass the rendered image). SVG is supported: see SVG in layout.
  • A previewer or hot reload.
  • Document operations such as merging and encryption are not part of the layout types, but OnePDF core has them: page operations, writing and Save(stream, PdfSaveOptions).
  • Author, subject and keywords on LayoutDocument (set them on the PdfDocument from ToPdfDocument()).

Moving a component

C#
sealed class Address(string name, string city) : IComponent
{
    public void Compose(Slot slot) => slot.Column(column =>
    {
        column.Item().Text(name).Bold();
        column.Item().Text(city);
    });
}

Use it with slot.Component(new Address("Ada Lovelace", "London")). A component receives the slot it fills, so it chooses its own content and not the decorators around it.