JetsonPDF.Flow 1.1.0

dotnet add package JetsonPDF.Flow --version 1.1.0
                    
NuGet\Install-Package JetsonPDF.Flow -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JetsonPDF.Flow" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JetsonPDF.Flow" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="JetsonPDF.Flow" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JetsonPDF.Flow --version 1.1.0
                    
#r "nuget: JetsonPDF.Flow, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JetsonPDF.Flow@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JetsonPDF.Flow&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=JetsonPDF.Flow&version=1.1.0
                    
Install as a Cake Tool

JetsonPDF.Flow

Word-like retained-mode DOM for building PDFs on top of JetsonPDF. The shape follows System.Windows.Documents.FlowDocument — a mutable Section / Paragraph / Run / Table tree with Add* methods and settable properties — but emits PDFs through JetsonPDF.Writer on top of the JetsonPDF.Fluent layout engine. No WPF dependency, no STA constraint.

using JetsonPDF;
using JetsonPDF.Flow;

var doc = new FlowDocument { Title = "Hello", Author = "You" };

var section = doc.AddSection();
section.PageSize = PageSize.Letter;
section.PageMargins = new Margins(72);

section.Body.Add(new Paragraph("Hello, JetsonPDF.Flow!")
    { Style = ParagraphStyle.Heading1 });
section.Body.Add(new Paragraph("A retained-mode DOM for PDF generation."));

doc.Save("hello.pdf");

Contents


Overview

JetsonPDF.Flow is one of three high-level wrappers around the low-level JetsonPDF.Writer API:

Wrapper Style Best for
JetsonPDF.Flow Mutable retained-mode DOM (Section/Paragraph/Run) Word-shaped documents: reports, letters, articles, anything assembled by code from a data model and saved at the end.
JetsonPDF.Fluent Immutable QuestPDF-style chain (d.Page(p => p.Content().Column(c => ...))) Code-first invoices, dashboards, statements — anything you'd describe as "lay out these blocks".
JetsonPDF.Writer Imperative low-level (page.DrawText, page.DrawPath, …) Direct control over every operator, custom appearance streams, anything outside the layout primitives.

Flow's idiom is the OOXML / WPF FlowDocument one: build a tree, mutate it, save it. Methods use the Add* verb (AddSection, AddText, AddStyle); settable properties (Paragraph.Style, Section.PageSize, RunProperties.Bold) carry formatting. The renderer translates the tree into a Fluent document under the hood and reuses Fluent's two-pass renderer, so multi-page documents, page numbers, and cross-references resolve to the final page count without a second authoring pass.

Quick start

Add a project reference to JetsonPDF.Flow and using JetsonPDF.Flow;. Construct a FlowDocument, append sections, push block items into their bodies, then call Save:

using JetsonPDF;
using JetsonPDF.Flow;

var doc = new FlowDocument
{
    Title = "First Document",
    Author = "You",
    DefaultRunProperties = new RunProperties
    {
        FontFamily = "Helvetica",
        FontSize = 11,
        Color = Color.Black,
    },
};

var s = doc.AddSection();
s.PageSize = PageSize.A4;
s.PageMargins = new Margins(54);

s.Body.Add(new Paragraph("Welcome") { Style = ParagraphStyle.Heading1 });
s.Body.Add(new Paragraph("This is a paragraph of body text."));

doc.Save("first.pdf");

Core concepts

Type Role
FlowDocument Root of the tree. Holds Sections, Styles, DefaultRunProperties, Hyphenator, and the Save entry points.
Section One block-flow region with its own PageSize, PageMargins, Header, Footer, and Body. Each section starts on a fresh page.
HeaderFooter Per-section header/footer — a list of BlockItems redrawn on every page.
BlockItem Base of Paragraph / Table / BlockImage / PageBreak / TableOfContents / EndnoteList / CommentList.
Paragraph A sequence of Runs plus paragraph-level formatting (alignment, indent, spacing, pagination flags).
Run Base of TextRun / LineBreakRun / TabRun / PageNumberRun / PageCountRun / PageReferenceRun / InlineImageRun / FootnoteRun / EndnoteRun.
ParagraphProperties / RunProperties Cascading paragraph and character formatting. RunProperties fields are nullable — unset values inherit from the paragraph default, then from the document default.
ParagraphStyle Built-in preset (Heading1..3, Body, Caption, Quote).
NamedStyle User-defined style accessible by string name, with optional BasedOn chain.
ListMarker Decorates a paragraph as a numbered or bulleted list item.
DropCap Decorates a paragraph with a large first letter and wrapped body.
AnchoredImage Image floated to one side of a paragraph; body wraps around it.
Hyphenator Optional Knuth-Liang hyphenator; inserts U+00AD breakpoints into text runs.

The general flow is: mutate the tree, call Save. The renderer composes a cascade (document defaultStyle preset → BasedOn chain → paragraph properties → run properties) every time it lays out a run; you can override any level without touching the others.

Sections

A section owns page setup and the three slots Word users expect:

var section = doc.AddSection();
section.PageSize = PageSize.Letter;          // or any JetsonPDF.PageSize
section.PageMargins = new Margins(72);       // 1 inch all sides
// or: new Margins(left, top, right, bottom)
// or: new Margins(horizontal, vertical)

section.Header.Body.Add(new Paragraph("Acme Corp")
    { Alignment = TextAlignment.Right });

section.Footer.Body.Add(new Paragraph(p =>
{
    p.AddText("Page ");
    p.AddPageNumber();
    p.AddText(" of ");
    p.AddPageCount();
}) { Alignment = TextAlignment.Center });

section.Body.Add(new Paragraph("First paragraph."));
section.Body.Add(new Paragraph("Second paragraph."));

Every Section you add starts on a fresh page; the body auto-paginates at paragraph, line, and table-row boundaries. A document can have multiple sections — each is an independent pagination unit with its own size/margins/header/footer/column layout. Page-number placeholders count across all sections, so a 3-page section followed by a 2-page section sees PageCount = 5.

Margins is a record struct:

new Margins(72)                              // 72pt all sides
new Margins(horizontal: 54, vertical: 72)    // matching pairs
new Margins(left: 72, top: 54, right: 72, bottom: 72)
Margins.OneInch                              // shorthand for new Margins(72)
Margins.Zero

Paragraphs and runs

A paragraph is a sequence of Runs plus paragraph-level formatting. The common shorthands:

// Single-run paragraph from a literal string.
new Paragraph("Just some text.");

// Multi-run paragraph via the fluent builder.
new Paragraph(p =>
{
    p.AddText("Mixed ");
    p.AddText("bold").Bold();
    p.AddText(" and ");
    p.AddText("italic").Italic();
    p.AddText(" and ").FontSize(14);
    p.AddText("colored").Color(Color.FromBytes(180, 30, 30));
    p.AddText(" runs.");
});

RunHandle (returned from every AddX call) chains:

Method Effect
.Bold(bool = true) Sets RunProperties.Bold.
.Italic(bool = true) Sets RunProperties.Italic.
.FontSize(double) Sets the run's font size in points.
.FontFamily(string) Picks a family from the document's font registry.
.Color(Color) Foreground color.
.Underline(bool = true) Draws an underline through the run's glyphs.
.Strikethrough(bool = true) Draws a strike-through.
.Highlight(Color) Fills a colored rectangle behind the glyphs.
.Link(string uri) Wraps the run in an external hyperlink.
.SectionLink(string anchor) Wraps the run in an internal cross-reference.
.Comment(string author, string body) Anchors a review comment.
.AsInsertion(string? author) Tracked-changes insertion (underline + author color).
.AsDeletion(string? author) Tracked-changes deletion (strikethrough + author color).

Builder methods cover every run type:

new Paragraph(p =>
{
    p.AddText("inline image: ");
    p.AddImage(myImage, width: 24, height: 24);  // sits on baseline
    p.AddText(" — and a ");
    p.AddTab();                                   // jump to next tab stop
    p.AddText("hard");
    p.AddLineBreak();                             // line break, same paragraph
    p.AddText("page ");
    p.AddPageNumber();                            // 1-based current page
    p.AddText(" of ");
    p.AddPageCount();                             // total page count
    p.AddText(" (see ");
    p.AddPageReference("intro");                  // "page N" for bookmark
    p.AddText(")");
});

The non-AddText runs can also be constructed directly and dropped into Paragraph.Runs if you'd rather not use the builder:

var p = new Paragraph();
p.Runs.Add(new TextRun("Page "));
p.Runs.Add(new PageNumberRun());
p.Runs.Add(new TextRun(" of "));
p.Runs.Add(new PageCountRun());

Paragraph-level properties

Either set them on the Paragraph directly (it forwards to ParagraphProperties) or on the Properties object:

new Paragraph("Justified prose with first-line indent.")
{
    Alignment = TextAlignment.Justify,
    LineSpacing = 1.4,
    SpaceBefore = 6,
    SpaceAfter = 6,
    LeftIndent = 18,
    RightIndent = 18,
    FirstLineIndent = 24,
};
Property Default Meaning
Alignment Left Left / Center / Right / Justify.
SpaceBefore / SpaceAfter 0 Whitespace above/below the paragraph, in points.
LineSpacing 1.2 Line-height multiplier (1.2 = 120% of font size).
LeftIndent / RightIndent 0 Indent on every line, in points.
FirstLineIndent 0 Extra indent on the first line only.
PageBreakBefore false Force a fresh page before this paragraph.
KeepLinesTogether false Don't split the paragraph across pages.
KeepWithNext false Keep this paragraph and the next block on the same page.
TabStops empty Tab stops in increasing Position order.

Paragraph styles

ParagraphStyle is a built-in preset that the cascade applies after the document defaults and before any per-paragraph overrides:

new Paragraph("Heading 1") { Style = ParagraphStyle.Heading1 };
new Paragraph("Heading 2") { Style = ParagraphStyle.Heading2 };
new Paragraph("Heading 3") { Style = ParagraphStyle.Heading3 };
new Paragraph("Body text") { Style = ParagraphStyle.Body };
new Paragraph("A caption underneath an image.") { Style = ParagraphStyle.Caption };
new Paragraph("A pulled-out quotation.") { Style = ParagraphStyle.Quote };
Style Run defaults Paragraph defaults
Heading1 Bold, 22pt SpaceBefore 18, SpaceAfter 8, KeepWithNext
Heading2 Bold, 16pt SpaceBefore 14, SpaceAfter 6, KeepWithNext
Heading3 Bold, 13pt SpaceBefore 10, SpaceAfter 4, KeepWithNext
Caption Italic, 9pt SpaceAfter 4
Quote Italic LeftIndent 24, RightIndent 24, SpaceBefore/After 6
Body (document defaults) (none)

Heading paragraphs are auto-bookmarked into the PDF outline when FlowDocument.AutomaticOutline is true (the default). Manual Paragraph.Bookmark = "anchor" entries are honored either way.

Named styles

User-defined styles register on the document and reference by string name. They support BasedOn inheritance:

doc.AddStyle("Callout", configure: s =>
{
    s.RunProperties.Italic = true;
    s.RunProperties.FontSize = 10;
    s.RunProperties.Color = Color.FromBytes(60, 60, 60);
    s.ParagraphProperties.LeftIndent = 24;
    s.ParagraphProperties.RightIndent = 24;
});

doc.AddStyle("CalloutImportant", basedOn: "Callout", configure: s =>
{
    s.RunProperties.Bold = true;
    s.RunProperties.Color = Color.FromBytes(180, 30, 30);
});

section.Body.Add(new Paragraph("This is a regular callout.")
    { StyleName = "Callout" });

section.Body.Add(new Paragraph("Important — review before sign-off.")
    { StyleName = "CalloutImportant" });

A paragraph can carry both Style (a built-in preset) and StyleName (a named style); both apply, with the cascade walking document default → preset → BasedOn chain (root-to-leaf) → paragraph properties → run properties. Cycles in the BasedOn graph throw at render time.

Lists

Decorate a paragraph as a list item via ListMarker:

section.Body.Add(new Paragraph("First item")
    { ListMarker = new ListMarker(ListKind.Bullet) });
section.Body.Add(new Paragraph("Second item")
    { ListMarker = new ListMarker(ListKind.Bullet) });

section.Body.Add(new Paragraph("Step 1")
    { ListMarker = new ListMarker(ListKind.Number) });
section.Body.Add(new Paragraph("Step 2")
    { ListMarker = new ListMarker(ListKind.Number) });
section.Body.Add(new Paragraph("Step 3")
    { ListMarker = new ListMarker(ListKind.Number) });
ListKind Marker
Bullet (or ListMarker.GlyphOverride)
Number 1., 2., 3., …
LowerAlpha a., b., …
UpperAlpha A., B., …
LowerRoman i., ii., …
UpperRoman I., II., …

Nested lists use the Level field — each level has its own counter and indent depth:

new ListMarker(ListKind.Number, level: 0)     // 1., 2., 3.
new ListMarker(ListKind.LowerAlpha, level: 1) // a., b., c.
new ListMarker(ListKind.LowerRoman, level: 2) // i., ii., iii.

The renderer prepends the marker to the paragraph as a styled run, follows it with a tab to the body indent, and configures a hanging indent so wrapped lines align under the marker's right edge. Counters reset when a non-list paragraph appears between two list items, when Kind changes, or when RestartNumbering is set on a marker.

GlyphOverride swaps the auto-generated marker for a custom string — useful for custom bullets:

new ListMarker(ListKind.Bullet) { GlyphOverride = "▸" };
new ListMarker(ListKind.Bullet) { GlyphOverride = "★" };

Tables

Block-level tables with relative-weight columns, repeating header rows, column span, row span, and per-page auto-pagination:

var table = new Table
{
    CellBorderWidth = 0.5,
    CellBorderColor = Color.FromBytes(160, 160, 160),
    CellPadding = 6,
    RepeatHeader = true,
};
table.Columns.AddRange(new[] { 3.0, 1.0, 1.0 });  // 3:1:1 widths

// Header row
var header = new TableRow();
header.Cells.Add(new TableCell("Item"));
header.Cells.Add(new TableCell("Qty"));
header.Cells.Add(new TableCell("Total"));
table.Header.Add(header);

// Body rows
foreach (var line in lines)
{
    var row = new TableRow();
    row.Cells.Add(new TableCell(line.Name));
    row.Cells.Add(new TableCell(line.Qty.ToString())
        { Background = Color.FromBytes(245, 245, 245) });
    row.Cells.Add(new TableCell($"${line.Total:F2}"));
    table.Body.Add(row);
}

section.Body.Add(table);

A cell's body is a list of BlockItems — you can host multiple paragraphs, nested tables, or images:

var cell = new TableCell();
cell.Children.Add(new Paragraph("Description") { Style = ParagraphStyle.Caption });
cell.Children.Add(new Paragraph(p =>
    p.AddText("Body text with ").AddText("emphasis").Italic()));
cell.ColumnSpan = 2;

Column span widens a cell; row span makes a cell occupy multiple rows (auto-flow tracks occupancy, so subsequent cells skip the cells reserved by the span). Row-spanned regions are atomic — they never split across pages; if the block doesn't fit on the current page the entire span pushes to the next page.

Header rows redraw at the top of every page the table touches when RepeatHeader is true (the default).

Images

Block-level images sit on their own line and align within the slot:

var image = Image.FromFile("chart.png");

section.Body.Add(new BlockImage(image)
{
    Width = 360,
    Height = 240,
    Alignment = BlockAlignment.Center,
});

Width/Height are optional — leaving them null uses the image's natural pixel dimensions. BlockAlignment is Left, Center, or Right.

Inline images sit inside a paragraph's text flow with their bottom edge on the baseline (see Paragraphs and runs):

new Paragraph(p =>
{
    p.AddText("Status: ");
    p.AddImage(greenDot, width: 10, height: 10);
    p.AddText(" Healthy");
});

Anchored (floated) images wrap paragraph body text around the image:

section.Body.Add(new Paragraph(longProseText)
{
    FloatImage = new AnchoredImage(portrait, width: 120, height: 160)
    {
        Side = AnchorSide.Left,   // or Right
        GapAfter = 8,
    },
});

The image renders at the top-left or top-right of the paragraph frame; lines whose top falls within the image's height shift away from it. The wrap is single-paragraph — if the paragraph ends before the image's height is consumed, surrounding paragraphs render flush full-width below.

Drop caps and tab stops

Drop cap — the first character renders large at the top-left and the next few lines wrap around it:

section.Body.Add(new Paragraph(openingProse)
{
    DropCap = new DropCap
    {
        LineSpan = 3,         // glyph occupies 3 body lines
        FontSizeMultiplier = 4.0,
        Color = Color.FromBytes(120, 0, 0),
        GapAfter = 6,
    },
});

Tab stops live on a paragraph's TabStops list (sorted by position). Each TabRun advances to the next stop greater than the current x:

new Paragraph(p =>
{
    p.AddText("Left");
    p.AddTab();
    p.AddText("Centered");
    p.AddTab();
    p.AddText("Right");
})
{
    TabStops =
    {
        new TabStop(0,   TabAlignment.Left),
        new TabStop(216, TabAlignment.Center),
        new TabStop(432, TabAlignment.Right),
    },
};

TabAlignment.Left (the default) starts the following run at the stop; Center centers the following run on the stop; Right right-aligns the following run so it ends at the stop.

Pagination control

Three boolean flags on Paragraph (also reachable via Properties):

Flag Behaviour
PageBreakBefore Force a fresh page before this paragraph (no-op if it would already start one).
KeepLinesTogether Never split this paragraph across pages — push it whole if it doesn't fit.
KeepWithNext Keep this paragraph and its next sibling block on the same page.
section.Body.Add(new Paragraph("Section 2")
    { Style = ParagraphStyle.Heading1, PageBreakBefore = true });
section.Body.Add(new Paragraph("This caption stays with its image.")
    { KeepWithNext = true });
section.Body.Add(new BlockImage(diagram) { Alignment = BlockAlignment.Center });
section.Body.Add(new Paragraph(shortSummary) { KeepLinesTogether = true });

For an explicit, content-free page break drop a PageBreak block:

section.Body.Add(new PageBreak());
section.Body.Add(new Paragraph("Starts on a new page.")
    { Style = ParagraphStyle.Heading2 });

Heading1 / Heading2 / Heading3 set KeepWithNext = true in their presets so a heading at the bottom of a page pushes to the next page along with the first body paragraph that follows it.

Headers and footers

Per-section headers and footers are lists of block items redrawn on every page of the section. Mix any block types — paragraphs, tables, images:

section.Header.Body.Add(new Paragraph(p =>
{
    p.AddText("Acme Corp — Q3 Report").Italic();
}) { Alignment = TextAlignment.Right });

section.Footer.Body.Add(new Paragraph(p =>
{
    p.AddText("Page ");
    p.AddPageNumber();
    p.AddText(" of ");
    p.AddPageCount();
}) { Alignment = TextAlignment.Center });

PageNumberRun and PageCountRun go through Fluent's deferred-text mechanism: at measure time they reserve worst-case 6-digit width; the actual digits are stamped after the document has finished laying out so wrapping doesn't shift between pages. PageCount is the total across all sections in the document, not per-section.

Headers and footers always span the full content width (they are not columnised even when the section uses multiple columns).

Three primitives:

// External hyperlink — wraps the run in a clickable region.
new Paragraph(p => p.AddText("Visit the docs").Link("https://example.com/docs"));

// Named-destination anchor — registered at paragraph's top-left when drawn.
section.Body.Add(new Paragraph("Introduction")
    { Style = ParagraphStyle.Heading1, Bookmark = "intro" });

// Internal cross-reference — jumps to a bookmarked paragraph.
new Paragraph(p =>
{
    p.AddText("As discussed in ");
    p.AddText("the introduction").SectionLink("intro");
    p.AddText(" (page ");
    p.AddPageReference("intro");   // resolves to the bookmark's page number
    p.AddText(").");
});

When FlowDocument.AutomaticOutline is true (default), Heading1/Heading2/ Heading3 paragraphs are auto-bookmarked and contributed to the PDF outline at their nesting depth. Manual Bookmark = "..." values are honored too, and never conflict — manual entries appear under the closest heading they fall after.

Footnotes and endnotes

Footnotes render in a reserved strip at the bottom of the page where the inline marker appears. The owning section must set FootnoteAreaHeight to a positive value for note bodies to render:

var section = doc.AddSection();
section.FootnoteAreaHeight = 80;   // reserve 80pt at page bottom

section.Body.Add(new Paragraph(p =>
{
    p.AddText("Numbers are unaudited");
    p.AddFootnote("Audit completes in Q4.");
    p.AddText(".");
}));

Endnotes accumulate per-document and render wherever you place an EndnoteList block — typically at the end of the last section:

section.Body.Add(new Paragraph(p =>
{
    p.AddText("See the appendix");
    p.AddEndnote("Detailed methodology — Appendix A.");
    p.AddText(" for methodology.");
}));

// Later in the document:
section.Body.Add(new EndnoteList
{
    Title = "Notes",
    TitleStyle = ParagraphStyle.Heading2,
});

EndnoteList expands at render time into one paragraph per registered endnote in document order. Both FootnoteRun and EndnoteRun render their inline marker as a small superscript-style number.

Comments and tracked changes

RunHandle.Comment(author, body) anchors a review comment to a run. The text gets a subtle yellow highlight; the (author, body) pair registers into the document's CommentList block (if present):

new Paragraph(p =>
{
    p.AddText("Revenue grew 18% year over year")
        .Comment("Reviewer A", "Confirm with finance before publish.");
    p.AddText(".");
});

section.Body.Add(new CommentList
{
    Title = "Review Comments",
    TitleStyle = ParagraphStyle.Heading2,
});

Tracked changes:

new Paragraph(p =>
{
    p.AddText("The new ").AsInsertion("Alice");
    p.AddText("old ").AsDeletion("Bob");
    p.AddText("policy is effective immediately.");
});

AsInsertion underlines and tints with an author-derived color; AsDeletion strikes through with the same color scheme. Author name picks a stable palette color (null falls back to a default review color).

Table of contents

Drop a TableOfContents block anywhere and the renderer replaces it at render time with one paragraph per heading: heading title, tab to the right margin, deferred page number. Each entry is wrapped in a SectionLink so clicking jumps to the heading:

cover.Body.Add(new Paragraph("Contents") { Style = ParagraphStyle.Heading1 });
cover.Body.Add(new TableOfContents
{
    Title = null,            // omit the auto-title, we drew our own
    MaxLevel = 3,            // include Heading1/2/3
    LevelIndent = 18,        // points of indent per level
});

Both the heading entries and the page-number cell go through the deferred-text mechanism, so TOC page numbers always reflect the final layout — no second authoring pass needed.

Multi-column sections

Newspaper-style columns on a section's body:

var section = doc.AddSection();
section.ColumnCount = 2;
section.ColumnGap = 18;          // gap between columns, in points
section.Body.Add(new Paragraph(longProse));

Headers and footers always span the full content width and aren't columnised. ColumnCount = 1 (the default) skips the column logic.

Hyphenation

Set FlowDocument.Hyphenator to enable Knuth-Liang hyphenation on every TextRun in the document. The hyphenator inserts U+00AD (SOFT HYPHEN) at allowed breakpoints; the line-breaker prefers those points over hard character breaks when a word has to split across lines:

doc.Hyphenator = JetsonPDF.Flow.Hyphenation.Hyphenator.EnglishDefault();

The default English hyphenator is a small, conservative starter set covering common prefixes and suffixes (.un3, .re1, 1tion, 1ment, 1ness, …). For production-quality hyphenation, load the full TeX ushyph.tex patterns:

var h = new JetsonPDF.Flow.Hyphenation.Hyphenator
{
    MinPrefix = 2,
    MinSuffix = 3,
    MinWordLength = 5,
};
foreach (var pattern in File.ReadAllLines("ushyph.tex"))
    h.AddPattern(pattern.Trim());
doc.Hyphenator = h;

AddPattern accepts the standard TeX pattern syntax: letters lowercase, digits encode priority at the surrounding gap, . anchors word boundaries. Higher digits win at each gap; odd values mark legal breakpoints. MinPrefix / MinSuffix / MinWordLength clamp where in a word the hyphenator is allowed to break.

Fonts

FlowDocument.DefaultRunProperties.FontFamily is a string. Standard 14 PDF font aliases are recognised by the underlying Fluent FontRegistry:

Family string Resolves to
"Helvetica", "Arial" Helvetica
"Times-Roman", "Times Roman", "Times", "Times New Roman" Times Roman
"Courier", "Courier New" Courier
"Symbol" Symbol
"ZapfDingbats", "Zapf Dingbats" ZapfDingbats

Register custom TrueType fonts via FlowDocument.ConfigureFonts:

doc.ConfigureFonts = reg =>
{
    reg.RegisterFromFile("Inter", JetsonPDF.FontStyle.Regular,
        @"C:\fonts\Inter-Regular.ttf");
    reg.RegisterFromFile("Inter", JetsonPDF.FontStyle.Bold,
        @"C:\fonts\Inter-Bold.ttf");
    reg.RegisterFromFile("Inter", JetsonPDF.FontStyle.Italic,
        @"C:\fonts\Inter-Italic.ttf");
};

doc.DefaultRunProperties = new RunProperties
{
    FontFamily = "Inter",
    FontSize = 11,
    Color = Color.Black,
};

If a styled face (Bold/Italic) isn't registered but Regular is, the Regular face is used as a fallback.

Saving

Three overloads on FlowDocument:

doc.Save("report.pdf");                       // write to a file path
doc.Save(httpResponse.Body);                  // write to any Stream
byte[] bytes = doc.Save();                    // serialize in-memory

You can also append the rendered pages to an existing PDF (e.g. one opened in edit mode via JetsonPDF.Document.Open):

using var target = JetsonPDF.Document.Open("existing.pdf");
doc.AppendTo(target);   // pages are appended as an incremental update
target.Save("merged.pdf");

AppendTo does not copy the Flow document's Title/Author into the target — only the rendered pages.

Limitations

  • Continuous (mid-page) section breaks. Section.StartsOnNewPage = false is rejected at render time — every section starts on a fresh page in v1.
  • Floating image wrap is single-paragraph. If the paragraph ends before the image's height is consumed, the surrounding paragraphs render flush full-width below — they don't continue the wrap.
  • Footnote area height is fixed. Setting FootnoteAreaHeight = 80 reserves exactly that strip on every page of the section; if the combined footnote bodies exceed it, the bottom-most footnotes are truncated. Variable-height footnote areas aren't supported yet.
  • TabRun without paragraph TabStops renders four spaces. Tab-stop alignment (Left/Center/Right) is honored when the paragraph carries explicit stops; falling back without stops emits the four-space stand-in.
  • Standard 14 fonts use WinAnsi (Windows-1252) encoding. Unicode characters outside Latin-1 + CP1252 extras render as ?. Register a TrueType font with full Unicode coverage via ConfigureFonts if your text needs non-Latin scripts, em dashes outside CP1252, arrows, etc.
  • Comments and tracked changes are visual only. They draw the highlight/underline/strikethrough into the page content stream rather than emitting PDF /Text or /StrikeOut annotations — they look the same in a viewer but aren't queryable as comment annotations.
  • The default English Hyphenator.EnglishDefault() is conservative. Words with no recognized morphology pass through untouched. For production-quality hyphenation, load the full TeX pattern table.

When to pick Flow

Want Pick
A Word-shaped document built from a data model — letters, reports, articles, books, anything with sections, headings, paragraphs, tables, footnotes, a TOC. JetsonPDF.Flow
A code-first invoice / dashboard / statement where the layout is built top-down with chained decorators. JetsonPDF.Fluent
A XAML-authored document with WPF-style layout (Grid/StackPanel/DockPanel, data binding, styles). JetsonPDF.Wpf.Authoring
Direct control over every PDF operator — custom appearance streams, soft masks, raw color spaces, things the layout primitives don't expose. JetsonPDF.Writer

Flow shines when the content is doing the talking — text that should flow naturally, headings that pick their style, lists that number themselves, page numbers that update themselves, cross-references that resolve themselves. Fluent shines when the layout is doing the talking — precise column widths, fixed-position headers, dashboards. Use them together when it makes sense: a Flow body inside a Fluent shell, or vice versa via AppendTo.

Targets

  • net8.0
  • netstandard2.0
  • net462

License

MIT.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 32 6/6/2026
1.0.0 99 5/23/2026
0.2.0-preview 91 5/23/2026
0.1.0-preview 88 5/17/2026