JetsonPDF.Flow
1.1.0
dotnet add package JetsonPDF.Flow --version 1.1.0
NuGet\Install-Package JetsonPDF.Flow -Version 1.1.0
<PackageReference Include="JetsonPDF.Flow" Version="1.1.0" />
<PackageVersion Include="JetsonPDF.Flow" Version="1.1.0" />
<PackageReference Include="JetsonPDF.Flow" />
paket add JetsonPDF.Flow --version 1.1.0
#r "nuget: JetsonPDF.Flow, 1.1.0"
#:package JetsonPDF.Flow@1.1.0
#addin nuget:?package=JetsonPDF.Flow&version=1.1.0
#tool nuget:?package=JetsonPDF.Flow&version=1.1.0
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
- Quick start
- Core concepts
- Sections
- Paragraphs and runs
- Paragraph styles
- Named styles
- Lists
- Tables
- Images
- Drop caps and tab stops
- Pagination control
- Headers and footers
- Links, bookmarks, and cross-references
- Footnotes and endnotes
- Comments and tracked changes
- Table of contents
- Multi-column sections
- Hyphenation
- Fonts
- Saving
- Limitations
- When to pick Flow
- Targets
- License
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 default → Style 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).
Links, bookmarks, and cross-references
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 = falseis 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 = 80reserves 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. TabRunwithout paragraphTabStopsrenders 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 viaConfigureFontsif 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
/Textor/StrikeOutannotations — 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.0netstandard2.0net462
License
MIT.
| Product | Versions 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. |
-
.NETFramework 4.6.2
- JetsonPDF.Common (>= 1.1.0)
- JetsonPDF.Fluent (>= 1.1.0)
- JetsonPDF.Writer (>= 1.1.0)
- Microsoft.Bcl.HashCode (>= 6.0.0)
- System.Buffers (>= 4.5.1)
- System.Memory (>= 4.5.5)
- System.ValueTuple (>= 4.5.0)
-
.NETStandard 2.0
- JetsonPDF.Common (>= 1.1.0)
- JetsonPDF.Fluent (>= 1.1.0)
- JetsonPDF.Writer (>= 1.1.0)
- Microsoft.Bcl.HashCode (>= 6.0.0)
- System.Buffers (>= 4.5.1)
- System.Memory (>= 4.5.5)
-
net8.0
- JetsonPDF.Common (>= 1.1.0)
- JetsonPDF.Fluent (>= 1.1.0)
- JetsonPDF.Writer (>= 1.1.0)
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 |