JetsonPDF.Wpf 1.1.0

dotnet add package JetsonPDF.Wpf --version 1.1.0
                    
NuGet\Install-Package JetsonPDF.Wpf -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.Wpf" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JetsonPDF.Wpf" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="JetsonPDF.Wpf" />
                    
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.Wpf --version 1.1.0
                    
#r "nuget: JetsonPDF.Wpf, 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.Wpf@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.Wpf&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=JetsonPDF.Wpf&version=1.1.0
                    
Install as a Cake Tool

JetsonPDF.Wpf

WPF integration for JetsonPDF. One package, two pipelines:

  • Authoring (XAML → PDF) — write a <jetsonpdf:XamlDocument> tree of WPF panels and controls; the real WPF layout engine (XamlReader.Parse plus Measure/Arrange) lays it out and JetsonPDF emits a PDF. Types live under JetsonPDF.Wpf.Authoring.
  • Viewer (PDF → XAML) — feed a parsed ReadDocument (from JetsonPDF.Reader) into PdfToXamlConverter and get back WPF XAML that XamlReader.Parse turns into a live visual tree. No native deps, no filesystem cache. Types live under JetsonPDF.Wpf.

Authoring example:

using JetsonPDF.Wpf.Authoring;

byte[] pdf = XamlToPdfConverter.Convert("""
    <jetsonpdf:XamlDocument
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025"
        PageSize="Letter" Title="Hello" Author="Me">
        <StackPanel Margin="40">
            <TextBlock Text="Hello, PDF" FontSize="32" FontWeight="Bold"/>
            <TextBlock Text="Built with JetsonPDF.Wpf authoring." Margin="0,8,0,0"/>
        </StackPanel>
    </jetsonpdf:XamlDocument>
    """);

File.WriteAllBytes("hello.pdf", pdf);

Viewer example:

using System.Windows.Markup;
using JetsonPDF.Reading;
using JetsonPDF.Wpf;

ReadDocument doc = PdfReader.Read(File.ReadAllBytes("hello.pdf"));
// PdfToXamlConverter is async because the shared code path also serves
// OpenSilver (which decodes JPEG via a JS promise). On WPF every
// await completes synchronously, so blocking the Task is safe.
string xaml = PdfToXamlConverter.ConvertAsync(doc).GetAwaiter().GetResult();
var visual = (UIElement)XamlReader.Parse(xaml);

// Hand the visual to any WPF host — Window.Content, ScrollViewer.Content,
// HwndHost child, etc.

Windows-only (net8.0-windows + WPF). For OpenSilver in the browser, use JetsonPDF.OpenSilver, which shares the same authoring dialect and viewer-emitter source files.

Contents


Overview

JetsonPDF.Wpf is the WPF-bound high-level wrapper around JetsonPDF.Writer and JetsonPDF.Reader. Two independent pipelines ship in the same package:

Pipeline Direction Entry point Namespace
Authoring XAML → PDF bytes XamlToPdfConverter.Convert(xaml) JetsonPDF.Wpf.Authoring
Viewer ReadDocument → XAML string PdfToXamlConverter.ConvertAsync(doc) JetsonPDF.Wpf

Authoring relies on WPF's own layout engine — XamlReader.Parse instantiates the tree, the converter calls Measure/Arrange at the chosen page dimensions, then walks the arranged visuals and emits PDF drawing operations. All the relative-layout machinery (Grid rows/columns, StackPanel stacking, DockPanel docking, Border padding) comes for free because WPF runs it. The walker only has to read the arranged rectangles and the per-element brush / font / geometry data.

The viewer pipeline is the inverse: a parsed ReadDocument becomes a string of XAML that any XamlReader.Parse call turns back into a live visual tree. The output is a top-level StackPanel containing one Canvas per page with absolute-positioned TextBlock, Image, Path, and form-widget children.

Shared source files live under JetsonPDF.XamlToPdfConverter.Authoring, JetsonPDF.XamlToPdfConverter.Core, and JetsonPDF.PdfToXamlConverter.Shared; the same XAML dialect and emitter logic also drives the OpenSilver package.

Authoring (XAML → PDF)

<jetsonpdf:XamlDocument> and <jetsonpdf:XamlPage>

XamlDocument is the preferred root. It carries PDF-wide metadata (page size, landscape, title, author) and exposes collections for outline / named destinations / page labels / layers / conformance. Any other FrameworkElement works too — the converter will size it against the options' default page size when its Width/Height are unset.

<jetsonpdf:XamlDocument
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025"
    PageSize="A4" Landscape="False"
    Title="Quarterly Report" Author="Acme Co.">
    
    <Grid Margin="50">
        <TextBlock Text="Hello, A4 world" FontSize="28"/>
    </Grid>
</jetsonpdf:XamlDocument>
Property Type Default Notes
PageSize JetsonPageSize Letter Letter, Legal, Tabloid, A3, A4, A5.
Landscape bool False Swaps width and height of the named size.
PageWidth / PageHeight double (DIP) NaN Custom dimensions; override PageSize when both are set.
Title / Author string? null Emitted as /Info /Title and /Info /Author.
Pages ObservableCollection<XamlPage> empty Populate for explicit multi-page.
Outline Collection<XamlOutlineItem> empty Bookmark tree (/Outlines).
NamedDestinations Collection<NamedDestination> empty /Names /Dests.
PageLabels Collection<XamlPageLabel> empty /PageLabels (§12.4.2).
Layers Collection<Layer> empty Optional content groups (§8.11).
Conformance Conformance None PDF/A or PDF/UA declaration.

XamlPage is a ContentControl that you drop into XamlDocument.Pages. It re-declares the four size properties via AddOwner, so a per-page override wins over the document-wide setting (locally set values via ReadLocalValue — unset properties inherit):

<jetsonpdf:XamlDocument PageSize="Letter">
    <jetsonpdf:XamlDocument.Pages>
        
        <jetsonpdf:XamlPage PageSize="A3" Landscape="True">
            <Grid><TextBlock Text="Cover" FontSize="96"/></Grid>
        </jetsonpdf:XamlPage>

        
        <jetsonpdf:XamlPage>
            <StackPanel Margin="50">
                <TextBlock Text="Body" FontSize="20"/>
            </StackPanel>
        </jetsonpdf:XamlPage>
    </jetsonpdf:XamlDocument.Pages>
</jetsonpdf:XamlDocument>

Layout panels

Anything WPF can lay out, the converter can emit. Layout is run by WPF; the emitter only reads the arranged geometry.

Panel Supported usage
Canvas Absolute positioning via Canvas.Left / Canvas.Top.
StackPanel Vertical or horizontal stack with Orientation.
Grid Full ColumnDefinitions / RowDefinitions including *, Auto, fixed.
DockPanel Standard DockPanel.Dock="Left/Right/Top/Bottom" plus LastChildFill.
Border Padding, border thickness, border brush, background, corner radius.
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="120"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Border Grid.Column="0" Padding="8" Background="#EEEEEE" CornerRadius="4">
        <TextBlock Text="Label"/>
    </Border>
    <Border Grid.Column="1" Padding="8" BorderBrush="#888888" BorderThickness="1">
        <TextBlock Text="Value"/>
    </Border>
</Grid>

Shapes

Rectangle, Ellipse, Line, and Path map to PDF path operators. Stroke, fill, stroke thickness, dash array, and dash offset all round-trip.

<Canvas>
    <Rectangle Canvas.Left="50" Canvas.Top="50" Width="120" Height="60"
               Stroke="Navy" StrokeThickness="2" Fill="#FFFAEA"/>
    <Ellipse   Canvas.Left="200" Canvas.Top="50" Width="80"  Height="80"
               Stroke="#FF5050" StrokeThickness="1.5"/>
    <Line X1="50" Y1="160" X2="280" Y2="160" Stroke="#999999" StrokeThickness="1"/>
    <Path Stroke="DarkGreen" StrokeThickness="2" Fill="#E0F2E5"
          Data="M 50,200 L 100,180 L 150,210 C 170,250 200,250 230,210 Z"/>
</Canvas>

Path.Data supports the full WPF Path mini-language plus all PathSegment subclasses: LineSegment, BezierSegment, QuadraticBezierSegment, PolyLineSegment, PolyBezierSegment, PolyQuadraticBezierSegment, and ArcSegment (flattened to cubic Béziers by the path flattener).

Text

TextBlock maps to PDF text-showing operators. Font family resolution falls back through the registered standard 14 (Helvetica / Times-Roman / Courier / Symbol / ZapfDingbats and their style variants) when the requested family isn't available as a TrueType / OpenType file on the system.

<StackPanel>
    <TextBlock Text="Heading"
               FontFamily="Times New Roman" FontSize="24"
               FontWeight="Bold" Foreground="#1A2C5C"/>
    <TextBlock FontFamily="Helvetica" FontSize="11" Margin="0,8,0,0">
        <Run Text="Mixed " FontWeight="Bold"/>
        <Run Text="runs " FontStyle="Italic"/>
        <Run Text="with per-run styling."/>
    </TextBlock>
</StackPanel>

Supported on TextBlock / Run: FontFamily, FontSize, FontWeight, FontStyle, Foreground, TextAlignment, LineHeight, TextWrapping.

Images

<Image> accepts any WPF ImageSource. The most common shapes:


<Image Source="C:\art\logo.png" Width="120"/>


<Image Source="pack://application:,,,/Resources/seal.png" Width="80"/>


<Image xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025"
       Source="{jetsonpdf:Base64Image Data=iVBORw0KGgo...}"
       Width="48" Height="48"/>

Base64ImageExtension decodes the base-64 string into a frozen BitmapImage at parse time. WPF's built-in ImageSourceConverter doesn't accept data: URIs, so this extension fills that gap without touching the filesystem.

Annotations

Annotations live in JetsonPDF.Wpf.Authoring and derive from the abstract AnnotationElement (which is a FrameworkElement, so it participates in WPF layout and you can place it via Canvas.Left/Top + Width/Height).

Element PDF subtype Notes
LinkAnnotation /Link Uri, NamedDestination, or Target (x:Reference to an element).
TextMarkupAnnotation /Highlight / /Underline / /StrikeOut / /Squiggly Target to a TextBlock builds quads via FormattedText.BuildHighlightGeometry.
FreeTextAnnotation /FreeText Self-contained text note with font / colour / alignment.
StampAnnotation /Stamp StampName (e.g. Approved, Draft, Confidential).
SquareAnnotation / CircleAnnotation /Square / /Circle Rect-based with stroke / fill / border width.
LineAnnotation /Line X1/Y1/X2/Y2 plus optional StartEnding / EndEnding.
PolygonAnnotation / PolyLineAnnotation /Polygon / /PolyLine Vertices PointCollection.
InkAnnotation /Ink Strokes — collection of PointCollection per stroke.
<Canvas>
    <TextBlock x:Name="Para1" Canvas.Left="50" Canvas.Top="100" Width="400"
               TextWrapping="Wrap" FontSize="11"
               Text="The quick brown fox jumps over the lazy dog."/>

    
    <jetsonpdf:TextMarkupAnnotation
        Kind="Highlight" Color="#80FFEA00"
        Target="{x:Reference Para1}" StartIndex="4" Length="5"/>

    
    <jetsonpdf:LinkAnnotation Canvas.Left="50" Canvas.Top="160"
                              Width="200" Height="18"
                              Uri="https://example.com"/>

    
    <jetsonpdf:StampAnnotation Canvas.Left="350" Canvas.Top="50"
                               Width="120" Height="40" StampName="Approved"/>

    
    <jetsonpdf:InkAnnotation StrokeColor="#0066CC" BorderWidth="1.5">
        <jetsonpdf:InkAnnotation.Strokes>
            <jetsonpdf:Points Data="50,300 80,290 120,310 160,295"/>
            <jetsonpdf:Points Data="50,330 200,330"/>
        </jetsonpdf:InkAnnotation.Strokes>
    </jetsonpdf:InkAnnotation>
</Canvas>

The <jetsonpdf:Points> markup extension wraps a delimited point list and materialises a PointCollection. It exists for parity with OpenSilver, whose XAML parser doesn't accept multi-point literals inside a <PointCollection> element directly — using <jetsonpdf:Points> makes the same XAML render on both runtimes.

Form widgets via Form.* attached properties

Standard WPF input controls become AcroForm widgets when you set jetsonpdf:Form.FieldName on them. The converter reads the arranged bounds and skips the control's visual children, so the PDF carries the widget rect and not the rendered chrome underneath.

Host control Field type /Ff flags honoured
TextBox /FT /Tx MaxLength, IsMultiline, IsPassword (and WPF's AcceptsReturn, IsReadOnly).
CheckBox /FT /Btn Checked = Yes, unchecked = Off.
ComboBox /FT /Ch (Combo) IsEditable/Edit flag, Items, SelectedValue.
ListBox /FT /Ch SelectionMode, Items, SelectedItems.
Button /FT /Btn (Pushbutton) Form.Action → PDF action (URI / Named / ResetForm).
<StackPanel Margin="50" xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025">

    <TextBlock Text="Name"/>
    <TextBox Width="280" Height="22"
             jetsonpdf:Form.FieldName="applicant.name"
             jetsonpdf:Form.MaxLength="64"/>

    <TextBlock Text="Comments" Margin="0,10,0,0"/>
    <TextBox Width="280" Height="80" AcceptsReturn="True"
             jetsonpdf:Form.FieldName="applicant.comments"
             jetsonpdf:Form.IsMultiline="True"/>

    <CheckBox Content="I agree" Margin="0,10,0,0"
              jetsonpdf:Form.FieldName="applicant.agreed" IsChecked="True"/>

    <ComboBox Width="160" Margin="0,10,0,0"
              jetsonpdf:Form.FieldName="applicant.region">
        <ComboBoxItem Content="NA"/>
        <ComboBoxItem Content="EMEA"/>
        <ComboBoxItem Content="APAC"/>
    </ComboBox>

    <Button Content="Print" Width="80" Height="24" Margin="0,16,0,0"
            jetsonpdf:Form.FieldName="actions.print"
            jetsonpdf:Form.Action="Print"/>
    <Button Content="Clear" Width="80" Height="24" Margin="0,4,0,0"
            jetsonpdf:Form.FieldName="actions.reset"
            jetsonpdf:Form.Action="Reset"/>
</StackPanel>

Form.Action recognised shorthands:

Value PDF action
"Print" NamedAction("Print") — viewer print dialog.
"Reset" ResetFormAction — clears every field.
http://…, https://…, mailto:… UriAction.
anything else NamedAction(value).

Page-context markup extensions

Inside any element the converter installs a per-page JetsonPageContext on DataContext. Two markup extensions resolve to bindings against it:

Extension Returns
{jetsonpdf:PageNumber} 1-based index of the current page.
{jetsonpdf:PageCount} Total page count for the whole document.
<DockPanel LastChildFill="True">
    <Grid DockPanel.Dock="Bottom" Margin="40,0,40,20">
        <TextBlock TextAlignment="Right" Foreground="#666666">
            <Run Text="XamlPage "/>
            <Run Text="{jetsonpdf:PageNumber}"/>
            <Run Text=" of "/>
            <Run Text="{jetsonpdf:PageCount}"/>
        </TextBlock>
    </Grid>
    <ContentControl></ContentControl>
</DockPanel>

Both extensions support StringFormat:

<TextBlock Text="{jetsonpdf:PageNumber StringFormat='XamlPage {0}'}"/>

Multi-page documents

Populate XamlDocument.Pages and the converter emits one PDF page per entry. Each page is Measured and Arranged at its own dimensions, so per-page overrides work cleanly. The converter sets a fresh JetsonPageContext on each page so {jetsonpdf:PageNumber} / {jetsonpdf:PageCount} resolve to the right values.

<jetsonpdf:XamlDocument PageSize="Letter" Author="Acme">
    <jetsonpdf:XamlDocument.Pages>
        <jetsonpdf:XamlPage>
            <StackPanel Margin="50">
                <TextBlock Text="XamlPage 1 — Introduction" FontSize="20"/>
            </StackPanel>
        </jetsonpdf:XamlPage>
        <jetsonpdf:XamlPage>
            <StackPanel Margin="50">
                <TextBlock Text="XamlPage 2 — Details" FontSize="20"/>
                <TextBlock Text="{jetsonpdf:PageNumber StringFormat='Page {0}'}"
                           Margin="0,40,0,0"/>
            </StackPanel>
        </jetsonpdf:XamlPage>
    </jetsonpdf:XamlDocument.Pages>
</jetsonpdf:XamlDocument>

PaginatedTable

<jetsonpdf:PaginatedTable> is a flowing table primitive: a header row plus an arbitrarily long sequence of rows bound to ItemsSource. When the row count exceeds the arranged slot, the converter expands the host XamlPage into N PDF pages, re-parses the XAML once per slice so {jetsonpdf:PageNumber} bindings stay live, and repeats the header on every spawned page.

<DockPanel LastChildFill="True">
    <TextBlock DockPanel.Dock="Top" Text="Order #1042" FontSize="24"
               Margin="40,40,40,8"
               jetsonpdf:Pagination.HideOnOverflow="True"/>

    <Border DockPanel.Dock="Bottom" Margin="40,8,40,20" Height="30">
        <TextBlock TextAlignment="Right">
            <Run Text="Page "/><Run Text="{jetsonpdf:PageNumber}"/>
            <Run Text=" of "/><Run Text="{jetsonpdf:PageCount}"/>
        </TextBlock>
    </Border>

    <jetsonpdf:PaginatedTable Margin="40,0,40,0"
        ItemsSource="{Binding LineItems}"
        HeaderHeight="28" RowHeight="22"
        HeaderBackground="#1A2C5C" HeaderForeground="White"
        BorderBrush="#888888" BorderThickness="0.5"
        FontFamily="Helvetica" FontSize="11">
        <jetsonpdf:PaginatedTable.Columns>
            <jetsonpdf:PaginatedColumn Header="SKU"   Width="120" Binding="Sku"/>
            <jetsonpdf:PaginatedColumn Header="Name"  Width="*"   Binding="Name"/>
            <jetsonpdf:PaginatedColumn Header="Qty"   Width="80"  Binding="Qty"
                                       TextAlignment="Right" HeaderAlignment="Right"/>
            <jetsonpdf:PaginatedColumn Header="Price" Width="100" Binding="Price"
                                       TextAlignment="Right" HeaderAlignment="Right"/>
        </jetsonpdf:PaginatedTable.Columns>
    </jetsonpdf:PaginatedTable>
</DockPanel>

jetsonpdf:Pagination.HideOnOverflow="True" on any descendant collapses that element on overflow pages so its space is reclaimed by the table — the lead page sees the element as authored, every subsequent slice doesn't.

Outline, named destinations, page labels, layers, conformance

XamlDocument exposes five collections that round-trip into the matching catalog entries.

<jetsonpdf:XamlDocument PageSize="Letter" Conformance="PdfA1b">
    <jetsonpdf:XamlDocument.NamedDestinations>
        <jetsonpdf:NamedDestination Name="intro"   PageIndex="0"/>
        <jetsonpdf:NamedDestination Name="results" PageIndex="3" Mode="Xyz" Left="40" Top="100" Zoom="1.0"/>
    </jetsonpdf:XamlDocument.NamedDestinations>

    <jetsonpdf:XamlDocument.Outline>
        <jetsonpdf:XamlOutlineItem Title="Introduction" NamedDestination="intro" Bold="True" IsExpanded="True">
            <jetsonpdf:XamlOutlineItem.Children>
                <jetsonpdf:XamlOutlineItem Title="Why this report" PageIndex="0"/>
            </jetsonpdf:XamlOutlineItem.Children>
        </jetsonpdf:XamlOutlineItem>
        <jetsonpdf:XamlOutlineItem Title="Results" NamedDestination="results"/>
    </jetsonpdf:XamlDocument.Outline>

    <jetsonpdf:XamlDocument.PageLabels>
        <jetsonpdf:XamlPageLabel StartPage="0" Style="LowerRoman"/>
        <jetsonpdf:XamlPageLabel StartPage="4" Style="DecimalArabic"/>
    </jetsonpdf:XamlDocument.PageLabels>

    <jetsonpdf:XamlDocument.Layers>
        <jetsonpdf:Layer Name="Design" VisibleByDefault="True" Intent="Design"/>
        <jetsonpdf:Layer Name="Notes"  VisibleByDefault="False"/>
    </jetsonpdf:XamlDocument.Layers>

    
    <StackPanel jetsonpdf:XamlDocument.Layer="Notes">
        <TextBlock Text="Reviewer notes" FontStyle="Italic"/>
    </StackPanel>
</jetsonpdf:XamlDocument>

Conformance accepts every flag the writer supports (PdfA1b, PdfA2a, PdfA2b, PdfA2u, PdfA3a, PdfA3b, PdfA3u, PdfUa1, PdfUa2). Setting it stamps the XMP packet and catalog entries but does not validate the rendered content — pair with ConformanceValidator.Validate(doc) or rely on Document.Save with ThrowOnConformanceError to enforce it.

Conversion options

XamlToPdfConverter.Convert(xaml, options) accepts an XamlToPdfOptions:

Property Default Purpose
DefaultPageSize PageSize.Letter Fallback page size when the root has neither explicit dimensions nor a XamlDocument size.
Title / Author null /Info entries; overridden by XamlDocument.Title / Author when both are present.
DefaultFontFamily FontFamily.Helvetica Used when a TextBlock has no FontFamily or specifies an unrecognised family.
DefaultFontSizeDip 16.0 Used when TextBlock has no FontSize (16 DIP ≈ 12 pt, matching WPF).
DefaultTextColor Color.Black Used when Foreground is unset.
RasterizeEffectsDpi 192.0 Rendering DPI for elements with a UIElement.Effect (DropShadow, Blur, custom ShaderEffect). PDF has no native primitive for these, so the subtree is rasterised.

Convenience overloads cover the common shapes:

byte[] bytes = XamlToPdfConverter.Convert(xaml);
byte[] bytes = XamlToPdfConverter.Convert(xaml, new XamlToPdfOptions { Title = "Q1" });
XamlToPdfConverter.Convert(xaml, fileStream);
XamlToPdfConverter.Convert(xaml, fileStream, new XamlToPdfOptions { DefaultPageSize = PageSize.A4 });

STA threading requirement

WPF object creation and layout are single-threaded-apartment only — they will throw if you call XamlReader.Parse or Measure/Arrange from a non-STA thread. The same applies to the authoring converter: it must run on an STA thread.

Console apps and ASP.NET background workers default to MTA. Wrap the call in an STA thread:

static byte[] BuildPdfOnSta(string xaml)
{
    byte[] result = null!;
    var thread = new Thread(() => result = XamlToPdfConverter.Convert(xaml));
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    thread.Join();
    return result;
}

A WPF app's UI dispatcher is already STA, so calling from there is fine — just remember the converter blocks the dispatcher (no await inside, layout runs synchronously). Use Task.Run only after switching to an STA worker.

Viewer (PDF → XAML)

PdfToXamlConverter

using System.Windows.Markup;
using JetsonPDF.Reading;
using JetsonPDF.Wpf;

// 1. Read the PDF into the JetsonPDF object model.
ReadDocument doc = PdfReader.Read(File.ReadAllBytes("report.pdf"));

// 2. Convert to a XAML string.
string xaml = PdfToXamlConverter.ConvertAsync(doc).GetAwaiter().GetResult();

// 3. Parse and host.
var pages = (UIElement)XamlReader.Parse(xaml);
window.Content = new ScrollViewer { Content = pages };

ConvertAsync returns a top-level StackPanel of Canvas pages with absolute-positioned children — TextBlock for text, Image for raster content (PNG / JPEG / TIFF / JBIG2 / JPX decoded by the reader), Path for vectors, Line / Rectangle / Ellipse for primitives, Glyphs for embedded fonts when available, and form widgets (TextBox, CheckBox, ComboBox, ListBox, Button) for AcroForm fields. Annotation links emit transparent Rectangles with the WidgetActions.Action attached property set.

PdfToXamlConverter.ConvertPageAsync(page) produces a standalone Canvas for a single page — useful for paginated UI that virtualises pages on demand.

ReadPage page = doc.Pages[3];
string xaml = await PdfToXamlConverter.ConvertPageAsync(page);

Options on PdfToXamlOptions:

Property Purpose
ShowFormFields Render AcroForm widgets as live WPF controls (default true).
HideContentUnderWidgets When ShowFormFields is on, clip widget rects out of the content layer so producer-painted placeholder borders don't show through.
EmbedFonts Emit embedded fonts as in-memory GlyphTypeface references so Glyphs elements render exactly.

In-memory contract

The viewer pipeline never touches the filesystem. Images are embedded as base-64 BitmapImages via Base64ImageExtension; fonts are loaded from the parsed ReadDocument's in-memory byte buffers. Everything you need to host the output is in the returned XAML string plus the ResourceCache the converter built internally.

This matters for:

  • Server-side rendering where the process has no writable disk.
  • Sandboxed environments where temp paths are off-limits.
  • Cases where you want to ship the XAML to another process / machine and parse it there.

WidgetActions attached behavior

The viewer emits an Action payload on every widget / link with a PDF action. To wire up dispatch, set WidgetActions.Enabled="True" on any ancestor of the parsed XAML:

<ContentControl xmlns:jetsonpdf="clr-namespace:JetsonPDF.Wpf;assembly=JetsonPDF.Wpf"
                jetsonpdf:WidgetActions.Enabled="True">
    
</ContentControl>

Or in code:

WidgetActions.SetEnabled(root, true);
root.AddHandler(WidgetActions.ActionInvokedEvent,
    new EventHandler<WidgetActionEventArgs>((s, e) =>
    {
        if (e.Action is GoToAction goTo)
        {
            // Custom in-document navigation — viewer-specific.
            ScrollTo(e.FieldName, goTo);
            e.Handled = true;
        }
    }));

Default dispatch covers:

Action Default behaviour
UriAction Opens the URI in the OS default browser via Process.Start.
NamedAction("Print") Shows PrintDialog, prints the root visual.
ResetFormAction Clears descendant TextBox / PasswordBox / CheckBox / RadioButton / ComboBox / ListBox controls. Honours FieldNames + ExcludeFields.
SubmitFormAction / JavaScriptAction / GoToAction / other named Bubbles the ActionInvokedEvent without acting. Consumers handle these — they need viewer-specific context.

Triggers wired automatically:

Control Event
ButtonBase (Button, CheckBox, RadioButton) Click
TextBox / PasswordBox LostFocus (matches Acrobat's "on blur" semantics for text fields with /A dicts).
ComboBox / ListBox SelectionChanged (matches PDF "calculate" semantics).
Bare UIElement (e.g. link rectangle) MouseLeftButtonUp

The ap-204 PDF's "Print" and "Clear" buttons exercise the NamedAction("Print") and ResetFormAction paths respectively — both work without any consumer code beyond setting Enabled="True".

Glyphs and negative-advance handling

Embedded fonts round-trip through <Glyphs> elements with explicit per-glyph advances and offsets. WPF's Glyphs control crashes on negative advances that come up in some CJK and complex-script subsets; the viewer pipeline clamps those to a tiny positive epsilon so the page still renders.

PDF → TIFF

To rasterise a PDF to a multipage TIFF, add the separate JetsonPDF.PdfToTiffConverter package. It builds on this package's viewer pipeline — each page is laid out by WPF, captured with RenderTargetBitmap, and encoded by the managed JetsonPDF.Tiff writer (no GDI+):

using JetsonPDF.Tiff;

PdfToTiffConverter.ConvertToFile("input.pdf", "output.tif",
    new PdfToTiffOptions { Dpi = 200 });

Like the authoring pipeline, it must run on an STA thread. See that package's README for the full option surface, per-page output, and progress reporting.

Limitations

  • STA-only. Both pipelines require an STA thread — they use WPF object creation and layout under the hood. Console apps must spin up an STA worker (see STA threading requirement).
  • UIElement.Effect is rasterised. PDF has no native primitive for DropShadowEffect / BlurEffect / custom ShaderEffect. The affected subtree is rendered to a bitmap at RasterizeEffectsDpi (default 192) and embedded. Text selection and vector fidelity are lost inside that subtree.
  • One PaginatedTable per XamlPage (v1). If you need multiple flowing tables per page, split them across separate XamlPages for now.
  • PaginatedColumn.Width="Auto" falls back to 1*. Column-width measurement against cell contents is deferred. Use explicit DIP widths or star weights.
  • Custom font discovery. The converter resolves FontFamily against the installed system fonts and the standard 14 PDF fonts. Pack-URI fonts (pack://...#FamilyName) work; arbitrary stream-loaded fonts that aren't referenced through a normal WPF FontFamily may not embed correctly.
  • Viewer round-trip is read-then-render. PdfToXamlConverter produces XAML for display, not for re-editing back through XamlToPdfConverter. The two pipelines target different schemas (authoring grammar vs. flat Canvas-of-primitives) and the trip is lossy.
  • Annotation preview in WPF. AnnotationElement subclasses have no default OnRender — they're authoring metadata, not visible content. If you want a live preview of annotation rectangles in the WPF tree, subclass and override OnRender.

Targets

  • net8.0-windows

License

MIT.

Product Compatible and additional computed target framework versions.
.NET net8.0-windows7.0 is compatible.  net9.0-windows was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on JetsonPDF.Wpf:

Package Downloads
JetsonPDF.PdfToTiffConverter

Rasterizes a parsed JetsonPDF document to a multipage TIFF. Reuses the JetsonPDF.Wpf PdfToXamlConverter pipeline (XamlReader.Parse + WPF Measure/Arrange) for layout, captures each page via RenderTargetBitmap, and encodes the multipage TIFF through JetsonPDF.Tiff's managed TiffWriter (no GDI+ dependency). Windows-only because of WPF rasterisation (net8.0-windows + WPF); must be invoked from an STA thread.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 121 6/6/2026
1.0.0 114 5/23/2026
0.2.0-preview 105 5/23/2026
0.1.0-preview 110 5/17/2026