JetsonPDF.OpenSilver 1.1.0

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

JetsonPDF.OpenSilver

OpenSilver integration for JetsonPDF. Authors the same xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025" dialect as JetsonPDF.Wpf, but the layout pass runs on the OpenSilver runtime, so the same XAML can compile to a PDF from inside a WebAssembly app, an Edge WebView2 simulator, or a Playwright-driven Chromium CLI. A second component, PdfToTiffBrowserConverter, rasterises PDFs to multipage TIFFs entirely in the browser — no server round-trip, no native image library.

using JetsonPDF.OpenSilver.Authoring;

string xaml = """
<jetsonpdf:Document xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025"
                    Title="OpenSilver demo">
  <jetsonpdf:Document.Pages>
    <jetsonpdf:Page Width="612" Height="792">
      <Canvas>
        <Rectangle Canvas.Left="72" Canvas.Top="72" Width="144" Height="72"
                   Fill="#1E88E5"/>
        <TextBlock Canvas.Left="72" Canvas.Top="160" Text="Hello from OpenSilver"
                   FontFamily="Helvetica" FontSize="18"/>
      </Canvas>
    </jetsonpdf:Page>
  </jetsonpdf:Document.Pages>
</jetsonpdf:Document>
""";

byte[] pdf = await XamlToPdfConverter.ConvertAsync(xaml);
// or stream-based:
//   await XamlToPdfConverter.ConvertAsync(xaml, outputStream, options);

The result is snapshot-equivalent to what JetsonPDF.Wpf produces from the same XAML — both walkers feed the same runtime-neutral DocumentSnapshot into JetsonPDF.XamlToPdfConverter.Core.ConverterCore.

Contents


Overview

JetsonPDF.OpenSilver is the second runtime adapter on top of the shared authoring pipeline. The first adapter, JetsonPDF.Wpf.Authoring, drives PDF generation from a XAML tree through a live WPF layout pass — useful on Windows desktop, tied to STA threads and net8.0-windows. JetsonPDF.OpenSilver covers the browser / web / cross-platform scenario: pure netstandard2.0, OpenSilver 3.2+, no WPF, no STA constraint.

Both adapters consume the same authoring XAML, share the same set of authoring types (via the shared JetsonPDF.XamlToPdfConverter.Authoring projitems), and produce the same DocumentSnapshot shape — so a PDF generated by the WPF adapter and one generated by the OpenSilver adapter from the same input XAML differ only in the runtime that walked the visual tree. PDF emission lives in JetsonPDF.XamlToPdfConverter.Core and is runtime-neutral.

The OpenSilver pipeline is asynchronous (Task<byte[]>-returning) because OpenSilver's BitmapImage loading goes through data: URIs and HttpClient resolved on the browser event loop; the WPF pipeline is synchronous because PNG encoding runs in-process on raw pixel buffers.

Public API

Member Purpose
JetsonPDF.OpenSilver.Authoring.XamlToPdfConverter.ConvertAsync(string xaml, XamlToPdfCoreOptions? options = null) -> Task<byte[]> Parse the XAML, run Measure/Arrange via the OpenSilver runtime, walk the visual tree into a snapshot, and feed it to ConverterCore.Convert. Returns PDF bytes.
JetsonPDF.OpenSilver.Authoring.XamlToPdfConverter.ConvertAsync(string xaml, Stream output, XamlToPdfCoreOptions? options = null) -> Task Same pipeline; writes the bytes to a stream. Doesn't dispose the stream.
JetsonPDF.OpenSilver.Authoring.OpenSilverTreeWalker Implements IXamlTreeWalker. Exposes the static HostContainer slot used to parent the parsed tree to a live Panel for accurate Measure/Arrange in a WASM host (see Hosting).
JetsonPDF.XamlAuthoring.IXamlTreeWalker Shared adapter contract — DocumentSnapshot WalkToSnapshots(string xaml, XamlToPdfCoreOptions options). WPF has VisualTreeWalker; OpenSilver has OpenSilverTreeWalker.
JetsonPDF.OpenSilver.Base64ImageExtension {jetsonpdf:Base64Image Data='…'} markup extension. Wraps the payload in a data: URI so OpenSilver's browser-backed BitmapImage can decode it natively.
JetsonPDF.OpenSilver.IJpegPixelDecoder / IJpxPixelDecoder Optional hooks for decoding JPEG / JPEG 2000 bytes to raw RGB pixels. Plug one in when you need JPEG+SMask alpha compositing (see JPEG + SMask alpha).
JetsonPDF.OpenSilver.JpegPixelResult Decoder return shape: byte[] Rgb (R,G,B,…), int Width, int Height.
JetsonPDF.OpenSilver.OpenSilverImageDecoders Process-wide registration: Jpeg (defaults to DefaultBrowserJpegDecoder.Instance) and Jpx (no default).
JetsonPDF.OpenSilver.DefaultBrowserJpegDecoder.Instance Built-in JPEG decoder that bridges to the browser's createImageBitmap via the OpenSilver Interop layer. Falls back silently in non-browser hosts.
JetsonPDF.OpenSilver.PdfToTiffBrowserConverter.ConvertAsync(byte[] pdfBytes, Panel host, TiffWriteOptions? options = null, IProgress<TiffConversionProgress>? progress = null) -> Task<byte[]> Rasterise every page of a PDF to a multipage TIFF — entirely in WebAssembly (see Browser-side PDF to TIFF).
JetsonPDF.OpenSilver.TiffViewer / TiffViewerSource Live UserControl + markup extension for displaying a TIFF byte array (or any frame) inside an OpenSilver page.
JetsonPDF.OpenSilver.WidgetActions Attached behaviour that wires WidgetAction clicks to default browser behaviour (window.open, window.print, reset-form).

The XAML namespace is registered as xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025" and resolves both:

  • JetsonPDF.OpenSilver.Authoring — containers, Document, Page, Form.* attached properties, annotation classes, PaginatedTable, JetsonPageContext, PageNumberExtension, PageCountExtension (shipped via the shared authoring projitems).
  • JetsonPDF.OpenSilver — the runtime-side hooks: Base64ImageExtension, the image-decoder slots, the TIFF viewer.

Hosting

OpenSilver's layout is partly DOM-driven; calling Measure / Arrange on a detached element resolves to zero metrics inside the WASM runtime. Production hosts parent the parsed authoring tree to a hidden host Canvas for the duration of the walk:


<Grid>
    
    <Canvas x:Name="HostCanvas"
            IsHitTestVisible="False"
            Opacity="0"
            Width="0" Height="0"/>
</Grid>
public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();
        Loaded += (_, _) =>
        {
            // Wire the walker to a hidden Canvas in the live visual tree.
            OpenSilverTreeWalker.HostContainer = HostCanvas;
        };
    }

    private async Task<byte[]> BuildPdfAsync(string xaml)
    {
        return await XamlToPdfConverter.ConvertAsync(xaml);
    }
}

After HostContainer is set, the walker attaches each parsed root, runs Measure / Arrange / UpdateLayout, walks the visual tree, and detaches before returning. If HostContainer is left null (Simulator without a bridge, detached unit tests), the walker falls back to detached layout — some metrics may resolve to zero, but the API still completes.

The companion host scaffold lives at ../JetsonPDF.OpenSilver.Sample.Host/: a UserControl-based shell with .Browser (WebAssembly) and .Simulator (WebView2) flavours, plus a [JSInvokable] JS bridge that exposes window.__jetsonpdfConvert(xaml) to Playwright-style callers.

Supported XAML surface

The OpenSilver walker handles the full authoring dialect that the WPF integration does — with two caveats noted under Differences from JetsonPDF.Wpf. All emission goes through runtime-neutral snapshots, so anything the core emitter knows how to draw is reachable from OpenSilver.

Document structure

<jetsonpdf:Document> is the multi-page root; <jetsonpdf:Page> carries per-page width/height/landscape; document-level fields cover metadata, named destinations, outline (bookmarks), page labels, OCG layers, and a declarative conformance flag.

<jetsonpdf:Document xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025"
                    Title="Quarterly report"
                    Author="Contoso, Inc.">
  <jetsonpdf:Document.NamedDestinations>
    <jetsonpdf:NamedDestination Name="intro" PageIndex="0"/>
    <jetsonpdf:NamedDestination Name="ch1"   PageIndex="2"/>
  </jetsonpdf:Document.NamedDestinations>

  <jetsonpdf:Document.Outline>
    <jetsonpdf:OutlineItem Title="Introduction" NamedDestination="intro" Bold="True"/>
    <jetsonpdf:OutlineItem Title="Chapter 1"    NamedDestination="ch1"/>
  </jetsonpdf:Document.Outline>

  <jetsonpdf:Document.PageLabels>
    <jetsonpdf:PageLabelRange StartPage="0" Style="LowerRoman"/>
    <jetsonpdf:PageLabelRange StartPage="2" Style="DecimalArabic"/>
  </jetsonpdf:Document.PageLabels>

  <jetsonpdf:Document.Pages>
    <jetsonpdf:Page Width="612" Height="792">
      
    </jetsonpdf:Page>
    <jetsonpdf:Page Landscape="True">
      
    </jetsonpdf:Page>
  </jetsonpdf:Document.Pages>
</jetsonpdf:Document>

A bare-root flavour (Canvas / Grid as the document root) is also supported for single-page documents; the walker pulls the page size from XamlToPdfCoreOptions.DefaultPageSize (Letter by default).

Shapes

Pure-vector primitives: Rectangle (with RadiusX / RadiusY corners), Ellipse, Line, Border, Image, and Path with the full geometry zoo — LineGeometry, RectangleGeometry, EllipseGeometry, and PathGeometry with LineSegment, BezierSegment, QuadraticBezierSegment, PolyLineSegment, PolyBezierSegment, PolyQuadraticBezierSegment, and ArcSegment.

<Canvas>
  <Rectangle Canvas.Left="40" Canvas.Top="40"
             Width="200" Height="80"
             RadiusX="8" RadiusY="8"
             Fill="#FF1E88E5" Stroke="#FF0D47A1" StrokeThickness="2"/>

  <Ellipse Canvas.Left="280" Canvas.Top="40"
           Width="80" Height="80"
           Fill="#FFFFD600"/>

  <Path Canvas.Left="40" Canvas.Top="160"
        Stroke="Black" StrokeThickness="1.5" Fill="#22FF0000">
    <Path.Data>
      <PathGeometry>
        <PathFigure StartPoint="0,0" IsClosed="True">
          <LineSegment Point="120,0"/>
          <BezierSegment Point1="180,40" Point2="180,80" Point3="120,120"/>
          <LineSegment Point="0,120"/>
        </PathFigure>
      </PathGeometry>
    </Path.Data>
  </Path>
</Canvas>

Brushes — SolidColorBrush, LinearGradientBrush (axial), and RadialGradientBrush — all emit through the same shading-aware path as WPF; pattern brushes are not supported in v1.

Text

TextBlock with FontFamily / FontSize / FontStyle / FontWeight / Foreground, TextWrapping, TextAlignment, and mixed-style <Run> inlines.

<TextBlock Canvas.Left="40" Canvas.Top="40"
           Width="400"
           FontFamily="Helvetica" FontSize="14"
           TextWrapping="Wrap" TextAlignment="Justify">
  This is a paragraph with
  <Run FontStyle="Italic">mixed</Run>
  <Run FontWeight="Bold">styles</Run> per run, wrapped at word
  boundaries by OpenSilver's layout pass.
</TextBlock>

Glyph metrics come from the same AFM tables the WPF walker uses, so baseline / ascent / descent are identical across the two adapters. Text positions reflect the OpenSilver-computed line layout — the runtime is the source of truth.

Annotations

Ten annotation subtypes, each as a first-class authoring element:

  • Link — URI or named-destination jump, with the click rect derived from the element's arranged bounds (or from an explicit Target reference to another element in the tree).
  • TextMarkup (Highlight, Underline, StrikeOut, Squiggly) — when Target binds to a TextBlock plus optional StartIndex / Length, quads are derived from FormattedText.BuildHighlightGeometry so the highlight tracks the actual rendered glyphs.
  • FreeText — inline text annotation with its own font + colour.
  • Stamp — named or custom stamp.
  • Square, Circle, Line, Polygon, PolyLine, Ink — geometric annotations with stroke / fill / opacity.
<Canvas>
  <TextBlock x:Name="HelloText"
             Canvas.Left="40" Canvas.Top="40"
             FontFamily="Helvetica" FontSize="16"
             Text="Click here for more information."/>

  <jetsonpdf:Link Target="{Binding ElementName=HelloText}"
                  Uri="https://example.com"/>

  <jetsonpdf:TextMarkup Subtype="Highlight"
                        Target="{Binding ElementName=HelloText}"
                        StartIndex="6" Length="4"
                        Color="#80FFD600"/>
</Canvas>

Cross-page Target references resolve through a prepass that records every element's PDF-space coordinates before the snapshot walk — a Link on page 1 can target a TextBlock on page 4 and the click rect lands in the right place.

Form widgets

TextBox, CheckBox, ComboBox, ListBox, and Button become AcroForm fields when tagged with jetsonpdf:Form.FieldName="…". The walker drops the control's chrome children once it's marked — the viewer (Acrobat, Foxit, the OpenSilver-rendered preview) paints the widget itself.

<Canvas>
  <TextBlock Canvas.Left="40" Canvas.Top="40" Text="Name"/>
  <TextBox   Canvas.Left="40" Canvas.Top="60"
             Width="240" Height="22"
             jetsonpdf:Form.FieldName="name"
             jetsonpdf:Form.MaxLength="50"/>

  <CheckBox  Canvas.Left="40" Canvas.Top="100"
             Width="16" Height="16"
             jetsonpdf:Form.FieldName="agree"
             IsChecked="True"/>

  <ComboBox  Canvas.Left="40" Canvas.Top="130"
             Width="160" Height="22"
             jetsonpdf:Form.FieldName="region">
    <ComboBoxItem Content="NA"/>
    <ComboBoxItem Content="EMEA"/>
    <ComboBoxItem Content="APAC"/>
  </ComboBox>

  <Button    Canvas.Left="40" Canvas.Top="170"
             Width="120" Height="24"
             Content="Submit"
             jetsonpdf:Form.FieldName="submit"
             jetsonpdf:Form.Action="Print"/>
</Canvas>

Form.IsMultiline, Form.IsPassword, and Form.Action (URI / Print / Reset) tune the emitted widget.

Page-context markup extensions

{jetsonpdf:PageNumber} and {jetsonpdf:PageCount} resolve against JetsonPageContext, which the walker stamps onto each page's DataContext before Measure. The bindings are OneWay so they re-evaluate per page during multi-page rendering (relevant when PaginatedTable expands a single source page into multiple PDF pages).

<jetsonpdf:Page Width="612" Height="792">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    
    <ContentPresenter Grid.Row="0" Content="{Binding Body}"/>

    <TextBlock Grid.Row="1"
               HorizontalAlignment="Center"
               FontFamily="Helvetica" FontSize="10">
      <Run Text="Page "/>
      <Run Text="{jetsonpdf:PageNumber}"/>
      <Run Text=" of "/>
      <Run Text="{jetsonpdf:PageCount}"/>
    </TextBlock>
  </Grid>
</jetsonpdf:Page>

Run.Text is not a DependencyProperty under OpenSilver, so the walker runs a sentinel-substitution pre-pass before Measure that swaps in the final string. Layout therefore reflects the actual page-number digits (no worst-case width reservation).

UIElement.Effect rasterisation

UIElement.Effect (DropShadow, Blur, custom ShaderEffect) is rasterised via WriteableBitmap.Render and embedded as a PNG image. The descendants of the effected subtree are skipped after rasterisation so the baked pixels aren't double-painted.

<Border Canvas.Left="40" Canvas.Top="40"
        Width="240" Height="120"
        Background="White" CornerRadius="8">
  <Border.Effect>
    <DropShadowEffect BlurRadius="8" ShadowDepth="4"
                      Color="#80000000"/>
  </Border.Effect>
  <TextBlock Margin="20" Text="Soft-shadowed card"/>
</Border>

WriteableBitmap.Render requires a live DOM under WASM — see Differences from JetsonPDF.Wpf. In detached / Simulator hosts without HostContainer wired, the rasterisation soft-fails and the image is omitted from the snapshot (layout still completes).

Base64 images

{jetsonpdf:Base64Image Data='…'} is the canonical way to embed an image without going through OpenSilver's resource loader. The extension wraps the payload in a data: URI with the MIME type sniffed from the first base64 characters (PNG iVBORw0KGgo, JPEG /9j/, GIF R0lGOD); the browser-backed BitmapImage decodes the URI natively.

<Image Canvas.Left="40" Canvas.Top="40"
       Width="200" Height="120"
       Source="{jetsonpdf:Base64Image Data='iVBORw0KGgoAAAANSUhEUg…'}"/>

Same XAML surface as the WPF Base64ImageExtension, so an authoring document moves between adapters unchanged.

Differences from JetsonPDF.Wpf

The two adapters consume identical XAML and produce snapshot-equivalent PDFs, but the runtime constraints differ in two practical ways:

  1. Async API. XamlToPdfConverter.ConvertAsync returns Task<byte[]> because OpenSilver image loading is asynchronous — BitmapImage.UriSource resolves through data: URIs or HttpClient on the browser event loop, and the JPEG-bridge decoder bridges through a JS Promise. The WPF adapter's Convert is synchronous because PngBitmapEncoder works on raw bitmap pixels in-process.

  2. WriteableBitmap.Render needs a live DOM. Effect rasterisation (DropShadow / Blur / ShaderEffect) and effect-driven Image baking go through WriteableBitmap.Render, which under WASM needs the element parented to a live host Canvas. Set OpenSilverTreeWalker.HostContainer to a Canvas already in Application.Current's visual tree. In the Simulator or in unit tests that don't supply a host, rasterisation soft-fails and the image is omitted from the snapshot — layout still completes.

The shared authoring projitems guarantee that authoring types (Document, Page, Form.*, the annotation classes, PageContextSentinel, …) are literally the same code on both sides, just compiled into different namespaces.

JPEG + SMask alpha

A PDF can pair a JPEG image with a soft-mask (/SMask) alpha channel. The browser's <img> element decodes the JPEG natively but ignores the external alpha, so the alpha has to be merged into the pixels before the image is handed to the renderer. The WPF adapter does this via JpegBitmapDecoder; OpenSilver targets netstandard2.0 and has no managed JPEG decoder in the BCL (even .NET 8/9 doesn't ship one), so we bridge to the browser's own JPEG decoder via createImageBitmap plus an offscreen canvas.

OpenSilverImageDecoders.Jpeg defaults to DefaultBrowserJpegDecoder.Instance, so JPEG+SMask alpha composites correctly out of the box inside any modern browser host (WebAssembly, Edge WebView2, Playwright Chromium). In non-browser hosts (the Simulator before the JS bridge is up, unit tests, server-side conversion) the default decoder catches the missing-Interop exception, emits a one-time stderr warning, and returns null — the previous alpha-drop pass-through kicks in. No exceptions, no behaviour regression.

To swap in a managed decoder (better throughput when a document has many large JPEG+SMask images — the default round-trips ~33 MB of base64 RGB through the JS bridge for a 4K image):

using JetsonPDF.OpenSilver;

OpenSilverImageDecoders.Jpeg = new MyJpegDecoder();
OpenSilverImageDecoders.Jpx  = new MyJpxDecoder();   // JPEG 2000, same pattern

internal sealed class MyJpegDecoder : IJpegPixelDecoder
{
    public Task<JpegPixelResult?> TryDecodeRgb24Async(
        byte[] jpegBytes, CancellationToken ct = default)
    {
        // Wrap your decoder of choice (SkiaSharp, ImageSharp, libjpeg port, …).
        // Output: width * height * 3 bytes of R,G,B,R,G,B,…. Return null on
        // failure — the pipeline will fall back to alpha-drop pass-through.
        var (rgb, width, height) = DecodeJpeg(jpegBytes);
        return Task.FromResult<JpegPixelResult?>(
            new JpegPixelResult(rgb, width, height));
    }
}

To disable JPEG+SMask compositing entirely (and accept the alpha-drop pass-through):

OpenSilverImageDecoders.Jpeg = null;

JPX (JPEG 2000) has no default decoder because browsers don't decode JP2 portably (Safari only). Register your own if you need it.

Async API note

Because the default decoder bridges through a JS Promise, IJpegPixelDecoder.TryDecodeRgb24Async is async and PdfToXamlConverter.Convert is now PdfToXamlConverter.ConvertAsync(...) -> Task<string>. The WPF flavour returns a synchronously-completed Task (no JS bridge involved), so blocking on the result via .GetAwaiter().GetResult() is safe in WPF; in OpenSilver, await it.

Browser-side PDF to TIFF

PdfToTiffBrowserConverter rasterises every page of a PDF to a multipage TIFF without a server round-trip or a native image library. The whole pipeline runs in WebAssembly:

  1. JetsonPDF.Reader.Reader.Load parses the PDF.
  2. For each page, PdfToXamlConverter.ConvertPageAsync turns it into OpenSilver XAML.
  3. The parsed root is mounted into a hidden host Panel already in the live visual tree.
  4. The host element's DOM is snapshotted via html2canvas (fetched from a CDN on first use).
  5. Raster images are composited onto the canvas directly through an overlay path, because html2canvas can't reliably snapshot OpenSilver's asynchronously-loaded <Image> elements.
  6. RGBA pixels are read back through CanvasRenderingContext2D.getImageData.
  7. Frames are encoded through JetsonPDF.Tiff.TiffWriter.
using JetsonPDF.OpenSilver;
using JetsonPDF.Tiff;

// HostCanvas is an empty Panel already parented in the live visual tree.
byte[] tiff = await PdfToTiffBrowserConverter.ConvertAsync(
    pdfBytes,
    HostCanvas,
    new TiffWriteOptions { Compression = TiffCompression.Deflate },
    progress: new Progress<TiffConversionProgress>(
        p => StatusText.Text = p.Description));

// Display via JetsonPDF.Tiff:
var decoded = TiffImage.Decode(tiff);
PreviewImage.Source = decoded.Frames[0].ToDataUri();

Key constraints:

  • The host panel must already be in Application.Current's visual tree. OpenSilver's layout is DOM-driven; a detached panel resolves to zero metrics and the converter would emit blank pages. The converter sizes the panel per page, mounts the parsed XAML, snapshots, and clears it. The panel's prior width/height/opacity/hit-test state is restored even if an exception unwinds out of the loop.
  • Raster images are composited onto the captured canvas directly, because html2canvas can't reliably snapshot OpenSilver's asynchronously-loaded <Image> elements. The XAML emitted by PdfToXamlConverter has <Image> elements stripped out before rendering — the overlay path is the single source of raster images.
  • ListBox option clipping is reproduced by capping each option's MaxHeight so the option stack fits inside the widget rect. html2canvas doesn't honour the live ScrollViewer clip.
  • Progress. The optional IProgress<TiffConversionProgress> sink reports each TiffConversionStageStartingConvertingXaml / Rendering per page → Encoding per frame → Completed. Suitable for driving a ProgressBar.

Browsers can't natively display TIFF, so pair this with JetsonPDF.Tiff's TiffImage.Decode + ToDataUri() to show the result in an <Image>.

Architecture

+---------------------------------------------------+
|  Your OpenSilver app (Browser or Simulator)       |
|                                                   |
|  XamlToPdfConverter.ConvertAsync(xaml)            |
|        |                                          |
|        v                                          |
|  +-------------------------+                      |
|  | OpenSilverTreeWalker    |  IXamlTreeWalker     |
|  |  - XamlReader.Load      |                      |
|  |  - HostContainer attach |                      |
|  |  - Measure/Arrange      |                      |
|  |  - VisualTreeHelper     |                      |
|  +-------------------------+                      |
|        |                                          |
|        | DocumentSnapshot                         |
|        v                                          |
|  +-------------------------+                      |
|  |  ConverterCore.Convert  |  runtime-neutral     |
|  |  + PdfEmitter           |  (also used by WPF)  |
|  +-------------------------+                      |
|        |                                          |
|        v                                          |
|        byte[] PDF                                 |
+---------------------------------------------------+
  • The shared JetsonPDF.XamlToPdfConverter.Authoring.projitems ships the authoring types (Document, Page, Form, the annotation classes, PageContextSentinel, PaginatedTable, the markup extensions, …) into both JetsonPDF.OpenSilver.Authoring and JetsonPDF.Wpf.Authoring. Under OPENSILVER (defined in this project's csproj), they compile against OpenSilver primitives; otherwise against WPF.
  • OpenSilverTreeWalker is OpenSilver-specific: it uses VisualTreeHelper.GetChildrenCount against DependencyObject because Silverlight has no System.Windows.Media.Visual, and it has to manage the host-container parenting required by WASM layout.
  • Snapshot types (DocumentSnapshot, PageSnapshot, every *Snapshot) and PDF emission live in JetsonPDF.XamlToPdfConverter.Core. Runtime-neutral.

Hosting projects

src/JetsonPDF.OpenSilver.Sample.Host/ ships a reference shell for embedding the converter in your own app:

Project Purpose
JetsonPDF.OpenSilver.Sample.Host A UserControl-based host. Sets up OpenSilverTreeWalker.HostContainer, exposes a [JSInvokable] JS bridge window.__jetsonpdfConvert(xaml) that Playwright (or any JS driver) can call to drive the converter from outside the page, and wires the WidgetActions defaults.
JetsonPDF.OpenSilver.Sample.Host.Browser The WebAssembly host — produces a static-file bundle that you can serve from any web server (no .NET runtime on the server).
JetsonPDF.OpenSilver.Sample.Host.Simulator The WebView2 simulator — for stepping through the host in Visual Studio.

Copy the structure into your own solution to bootstrap a converter front end; or call the converter directly from your app's Loaded handler if you don't need the JS bridge.

Limitations

  • HostContainer is required for accurate layout. OpenSilver's WASM runtime computes Measure/Arrange against the DOM; an unparented element resolves to zero metrics. Set OpenSilverTreeWalker.HostContainer to a hidden Canvas (or any Panel) inside Application.Current's visual tree before calling ConvertAsync. Without it the API still completes, but text positions and image sizes may be zero.

  • WriteableBitmap.Render requires a live DOM. Effect-rasterised subtrees (DropShadow / Blur / ShaderEffect) and effect-driven image baking soft-fail in detached / Simulator hosts. The PDF is still produced; the effected pixels are omitted from the snapshot.

  • JPEG 2000 (JPX) has no default decoder. Browsers don't decode JP2 portably (Safari only). Register an IJpxPixelDecoder via OpenSilverImageDecoders.Jpx if your input PDFs use JPX images with alpha; otherwise the alpha-drop pass-through applies.

  • JPEG+SMask alpha through the default decoder is bandwidth-heavy. The browser bridge round-trips ~33 MB of base64 RGB for a 4K image. Register a managed decoder via OpenSilverImageDecoders.Jpeg for documents with many large JPEG+SMask pairs.

  • PdfToTiffBrowserConverter fidelity ceiling is html2canvas. Text and vector shapes inherit OpenSilver's DOM rendering quality; complex CSS effects, custom shaders, or anything outside html2canvas's supported subset won't reproduce. Raster images are composited via the overlay path so they're pixel-accurate.

  • One PaginatedTable per source page (v1). Multi-page expansion of a single source XamlPage is supported, but a page with multiple PaginatedTable instances throws. The same restriction applies to WPF.

  • Pattern brushes are not supported. Solid / axial gradient / radial gradient brushes round-trip; tiling-pattern brushes are not in the authoring surface.

Targets

  • netstandard2.0
  • OpenSilver 3.2+
  • Defines OPENSILVER for code that conditionally specialises.

NuGet

dotnet add package JetsonPDF.OpenSilver

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 was computed.  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 was computed.  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 131 6/6/2026
1.0.0 106 5/23/2026
0.2.0-preview 108 5/23/2026
0.1.0-preview 102 5/17/2026