JetsonPDF.Writer 1.1.0

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

JetsonPDF.Writer

The low-level PDF authoring API. Builds PDF documents byte by byte against ISO 32000-2 (PDF 2.0). Headless, no native dependencies, with explicit control over fonts, colour spaces, content-stream operators, encryption, conformance, and signatures.

using JetsonPDF;

var doc = new Document
{
    Title = "Hello",
    Author = "Me",
};

var page = doc.AddPage(PageSize.Letter);
page.DrawText("Hello, world!", new Font(FontFamily.Helvetica, 24), x: 72, y: 720);
page.DrawRectangle(72, 600, 200, 80, stroke: Color.Black, fill: Color.RedLight);

doc.Save("hello.pdf");

If you'd rather think in containers, decorators, and retained DOM, look at JetsonPDF.Fluent (QuestPDF-style) or JetsonPDF.Flow (Word-like retained DOM). Both compose on top of this writer.

Contents


Overview

JetsonPDF.Writer is the engine that produces PDF bytes. Everything in the JetsonPDF stack — the JetsonPDF.Fluent container API, the JetsonPDF.Flow retained DOM, the WPF / OpenSilver authoring converters — ends up calling into this assembly to actually emit a content stream.

The model is direct: a Document owns a list of Pages, each Page has a content stream you populate with draw calls (DrawText, DrawRectangle, DrawPath, DrawImage, DrawShading, …) plus collections of widgets, annotations, layers, and structure elements. When you call Save, the writer serialises objects, builds the cross-reference table, optionally packs into object streams (PDF 2.0 layout), and emits the file.

The same instance can also open an existing PDF for incremental editing (Document.Open(path)), letting you replace page contents, stamp Form XObjects, mutate the page tree, or update form values without rewriting unchanged objects.

Quick start

using JetsonPDF;

var doc = new Document
{
    Title = "First Document",
    Author = "You",
    PdfVersion = "2.0",      // emit a PDF 2.0 header
    UseObjectStreams = true, // §7.5.7 / §7.5.8 — compact PDF 2.0 layout
};

var page = doc.AddPage(PageSize.A4);
var font = new Font(FontFamily.Helvetica, 12);

page.DrawText("Hello, A4 world!", font, x: 72, y: 800);
page.DrawLine(72, 790, 523, 790, Color.Black);
page.DrawRectangle(72, 700, 200, 60,
    stroke: Color.Black, fill: Color.FromBytes(255, 240, 200));

doc.Save("first.pdf");

Core concepts

Type Role
Document Top-level builder. Carries metadata, catalog entries, and the list of Pages. Has AddPage / Save / Open.
Page One page in the document. Content-stream draw calls plus per-page collections (widgets, annotations, layers, structure elements).
PageSize Width / height in points. Static factories (PageSize.Letter, PageSize.A4, …) and a (double w, double h) constructor.
Font Family + style + size — built either from the standard 14 (FontFamily.Helvetica, …) or from an EmbeddedFontFace.
EmbeddedFontFace A loaded TrueType / OpenType face. FromFile / FromBytes. Embedded as a CID composite (Identity-H) with full Unicode coverage.
Color Stroke / fill colour. Device factories (Rgb / Gray / Cmyk), FromBytes, InSpace(colorSpace, …) for non-device.
ColorSpace Device singletons plus CIE-based and special-space factories (CalGray, CalRgb, Lab, Separation, DeviceN, NChannel).
Path A built-up vector path — MoveTo, LineTo, CurveTo, AddRectangle, AddEllipse, AddCircle, Close.
Image A raster image XObject. FromFile / FromPng / FromJpeg / FromInlineItem.
ImageMask A 1-bit stencil drawn with a caller-supplied colour.
FormXObject A reusable content stream (§8.10). Build once, paint many times via Page.DrawForm.
TilingPattern A repeating pattern cell (§8.7.3). Coloured (PaintType 1) or uncoloured stencil (PaintType 2).
Shading Axial / radial / function-based gradients (§8.7.4).
Annotation Base class for every PDF annotation (link, text-markup, FreeText, geometric, …).
Widget (in JetsonPDF.Forms) Base class for AcroForm field widgets — TextField, CheckBox, RadioGroup, PushButton, ComboBox, ListBox, SignatureField, BarcodeField.

Every draw call on Page wraps itself in a q / Q save-restore so graphics state (fill colour, line width, transforms) does not leak between calls. Explicit graphics-state management is also exposed via PushGraphicsState / PopGraphicsState / Transform.

Document setup

var doc = new Document
{
    Title = "Annual Report",
    Author = "Acme Inc.",
    Subject = "FY2025 results",
    Keywords = "annual, finance",
    Creator = "report-generator",
    Producer = "JetsonPDF",

    PdfVersion = "2.0",
    UseObjectStreams = true,   // ObjStm + XRef stream layout (§7.5.7)

    Language = "en-US",        // /Lang on the catalog
    PageLayout = PageLayout.TwoColumnLeft,
    PageMode   = PageMode.UseOutlines,

    ViewerPreferences = new ViewerPreferences
    {
        CenterWindow = true,
        DisplayDocTitle = true,
        Direction = ReadingDirection.LeftToRight,
    },

    OpenAction = Destination.FitEntire(pageIndex: 0),
};

PdfVersion controls only the header banner. UseObjectStreams switches between classic xref / trailer syntax and the PDF 2.0 compact cross-reference-stream layout.

Text and fonts

Font takes a family (one of the 14 standard PostScript fonts) or an EmbeddedFontFace loaded from a TTF/OTF file, plus a point size. Standard fonts use WinAnsi (Windows-1252) encoding; embedded fonts use Identity-H composite encoding so any Unicode codepoint the font supplies renders.

// One of the 14 standard fonts — no embedding, available in every viewer.
var helv = new Font(FontFamily.Helvetica, 12);
var bold = new Font(FontFamily.Helvetica, 14, FontStyle.Bold);
var mono = new Font(FontFamily.Courier, 10);

// Embedded TrueType with full Unicode coverage.
var inter = EmbeddedFontFace.FromFile(@"C:\fonts\Inter-Regular.ttf");
var face = new Font(inter, 12);

page.DrawText("Standard 14 Helvetica", helv, x: 72, y: 750);
page.DrawText("Inter with Unicode → ← arrows", face, x: 72, y: 730);

// Coloured text — applied inside a q/Q scope.
page.DrawText("Tagline", bold, 72, 700, Color.Rgb(0.1, 0.3, 0.7));

// Measure: real metrics from AFM (standard 14) or hmtx (embedded TTF).
double width = helv.MeasureString("How wide am I?");

// Layout a wrapping paragraph in a column.
page.DrawTextBlock(
    "A long block of text that should wrap at word boundaries within " +
    "the given column width. Paragraph breaks use \\n.",
    helv, x: 72, y: 670, width: 450,
    alignment: TextAlignment.Justify, lineHeight: 14);

For custom glyph shapes that don't warrant a full TrueType embed, use a Type3Font:

var t3 = new Type3Font();
t3.DefineGlyph((byte)'A', advance: 600,
    g => g.DrawRectangle(0, 0, 500, 600, fill: Color.Black));
var iconFont = new Font(t3, 12);
page.DrawText("A", iconFont, 100, 700);

For text that doesn't round-trip cleanly to Unicode (ligatures, custom symbols), wrap the visible glyphs in an /ActualText span so extractors and assistive tech see the canonical string:

page.DrawTextWithActualText(visibleText: "ffi", actualText: "ffi",
    font: helv, x: 72, y: 640);

Vector graphics

All vector drawing goes through PDF's path-construction operators (m l c re h) and one of the paint operators (f f* S B B*). The high-level Page API wraps these:

// Stroked line.
page.DrawLine(72, 700, 540, 700, Color.Black, strokeWidth: 0.5);

// Stroked + filled rectangle.
page.DrawRectangle(72, 600, 200, 60,
    stroke: Color.Black, fill: Color.RedLight, strokeWidth: 1.0);

// Ellipse / circle (inscribed in the bounding box).
page.DrawEllipse(cx: 300, cy: 500, rx: 80, ry: 40,
    stroke: Color.Blue, fill: Color.FromBytes(220, 230, 255));

// Hand-built path with fill rule.
var star = new Path()
    .MoveTo(300, 720)
    .LineTo(320, 770)
    .LineTo(370, 770)
    .LineTo(330, 800)
    .LineTo(345, 850)
    .LineTo(300, 820)
    .Close();
page.DrawPath(star, stroke: Color.Black, fill: Color.Rgb(1, 0.85, 0),
    fillRule: FillRule.EvenOdd);

Direct CTM control for transforms the wrappers don't cover (rotation, shear, scale around an arbitrary origin):

page.PushGraphicsState();
page.Transform(Math.Cos(0.4),  Math.Sin(0.4),
               -Math.Sin(0.4), Math.Cos(0.4),
               300, 400);                       // rotate ~23° around (300, 400)
page.DrawRectangle(0, 0, 100, 40, fill: Color.Black);
page.PopGraphicsState();

Images

// Auto-detect format from magic bytes.
var logo = Image.FromFile("logo.png");

// Explicit format. Both accept an optional ICC profile.
var photo = Image.FromJpeg(File.ReadAllBytes("photo.jpg"));
var alpha = Image.FromPng(File.ReadAllBytes("alpha.png"));

page.DrawImage(logo,  x: 72, y: 700, width: 100, height: 100);
page.DrawImage(photo, x: 200, y: 700, width: 200, height: 150);

PNG decoding covers all bit depths (1/2/4/8/16-bit), Adam7 interlacing, an embedded ICC profile, and per-pixel alpha via a soft mask. JPEG is embedded verbatim (the JPEG bytes are the DCT-encoded payload PDF expects). JBIG2 and CCITT G3/G4 images can be embedded via the reader-side inline image bridge (Image.FromInlineItem).

A 1-bit stencil mask is painted by supplying the colour at paint time:

var mask = new ImageMask(width: 32, height: 32, maskBits: bits);
page.DrawImageMask(mask, x: 100, y: 100, width: 32, height: 32,
    color: Color.Rgb(0.2, 0.6, 0.2));

Colour and colour spaces

Color covers the three device families plus arbitrary CIE-based or N-colorant spaces.

Color.Black; Color.White; Color.Red; Color.Green; Color.Blue;

Color.Gray(0.5);                       // DeviceGray
Color.Rgb(0.2, 0.4, 0.6);              // DeviceRGB
Color.Cmyk(0, 0.4, 0.7, 0.1);          // DeviceCMYK
Color.FromBytes(255, 240, 200);        // 8-bit RGB shorthand

// Non-device — pass a ColorSpace plus components.
var cal = ColorSpace.CalRgb(gamma: new[] { 2.2, 2.2, 2.2 });
Color.InSpace(cal, 0.4, 0.5, 0.6);

var lab = ColorSpace.Lab();
Color.InSpace(lab, 50, 20, -10);       // L* = 50, a* = 20, b* = -10

// Single-colorant spot (PANTONE etc.).
var pantone485 = ColorSpace.Separation(
    "PANTONE 485 C",
    alternate: ColorSpace.DeviceCmyk,
    tintAtFullStrength: new[] { 0.0, 0.95, 1.0, 0.0 });
Color.InSpace(pantone485, 0.85);       // 85% strength

// Multi-colorant DeviceN / NChannel.
var hexachrome = ColorSpace.DeviceN(
    colorantNames: new[] { "Cyan", "Magenta", "Yellow", "Orange", "Green" },
    alternate: ColorSpace.DeviceCmyk,
    tintsAtFullStrength: new[]
    {
        new[] { 1.0, 0, 0, 0 }, new[] { 0, 1.0, 0, 0 }, new[] { 0, 0, 1.0, 0 },
        new[] { 0, 0.5, 1.0, 0 }, new[] { 1.0, 0, 1.0, 0 },
    });

When a draw call sees a non-device Color, the writer registers the colour space under /Resources/ColorSpace on the page and emits the appropriate /CSn cs … scn operator sequence.

Patterns and shadings

Tiling patterns build a small content stream that the viewer repeats:

var pattern = new TilingPattern(
    bboxWidth: 40, bboxHeight: 40,
    xStep: 40, yStep: 40,
    paintType: PaintType.Colored);

pattern.DrawLine(0, 0, 40, 40, Color.Rgb(0.7, 0.7, 0.7), strokeWidth: 1);
pattern.DrawLine(40, 0, 0, 40, Color.Rgb(0.7, 0.7, 0.7), strokeWidth: 1);

// Repeat across a rectangle on the page.
page.FillRectangleWithPattern(72, 400, 450, 200, pattern);

Uncoloured patterns (PaintType 2) supply the paint colour at fill time — the cell renders as a stencil:

var dots = new TilingPattern(20, 20, 20, 20, PaintType.Uncolored);
dots.DrawEllipse(10, 10, 4, 4, fill: Color.Black);

page.FillRectangleWithPattern(72, 300, 450, 60, dots, Color.Rgb(0.2, 0.5, 0.8));

Smooth gradients use Shading. Axial, radial, and function-based (Type 1, 2, 3) are supported on write; mesh shadings (4–7) are read-side only.

// Two-stop axial gradient diagonally across a rectangle.
var grad = Shading.Axial(
    x0: 72, y0: 200, x1: 522, y1: 100,
    startColor: Color.Rgb(0.1, 0.3, 0.8),
    endColor:   Color.Rgb(0.9, 0.9, 0.2));

page.FillRectangleWithShading(72, 100, 450, 100, grad);

// Multi-stop radial gradient.
var spot = Shading.Radial(
    x0: 300, y0: 600, r0: 0,
    x1: 300, y1: 600, r1: 80,
    stops: new[]
    {
        new KeyValuePair<double, Color>(0.0, Color.White),
        new KeyValuePair<double, Color>(0.5, Color.Rgb(1, 0.5, 0)),
        new KeyValuePair<double, Color>(1.0, Color.Black),
    });
page.DrawShading(spot);

Form XObjects

A FormXObject is a reusable content stream — perfect for repeated watermarks, logos, or page headers. The same instance shared across pages becomes a single XObject in the output.

var stamp = new FormXObject(width: 200, height: 60);
stamp.DrawRectangle(0, 0, 200, 60, fill: Color.Rgb(0.95, 0.95, 0.95));
stamp.DrawText("DRAFT", new Font(FontFamily.Helvetica, 24, FontStyle.Bold),
    x: 20, y: 18, Color.Rgb(0.8, 0.1, 0.1));

foreach (var p in doc.Pages)
    p.DrawForm(stamp, x: 200, y: 700, scaleX: 1.0, scaleY: 1.0);

Annotations

Annotations live in the page's /Annots array alongside form widgets. Every subtype derives from Annotation (in JetsonPDF.Annotations).

using JetsonPDF.Annotations;

// Clickable URL.
page.AddLink(x: 72, y: 700, width: 100, height: 16,
    uri: "https://example.com");

// Highlight over text drawn at (72, 670).
var quad = Quad.OverText(helv, x: 72, baselineY: 670, text: "highlighted");
page.AddAnnotation(new TextMarkupAnnotation(TextMarkupKind.Highlight,
    new[] { quad }) { Color = Color.Rgb(1, 1, 0) });

// FreeText sticky-note style annotation.
page.AddAnnotation(new FreeTextAnnotation(
    x: 400, y: 700, width: 130, height: 40,
    text: "Reviewer comment", font: helv));

// Stamp annotation with a Form XObject appearance.
page.AddAnnotation(new StampAnnotation(72, 600, 100, 30,
    name: "Approved", appearance: stamp));

// Geometric markups — Line / Square / Circle / Polygon / PolyLine / Ink.
page.AddAnnotation(new LineAnnotation(x1: 72, y1: 500, x2: 200, y2: 500)
{
    StrokeColor = Color.Red,
    BorderWidth = 2,
    StartEnding = LineEnding.Circle,
    EndEnding = LineEnding.OpenArrow,
});

page.AddAnnotation(new PolygonAnnotation(new (double, double)[]
{
    (300, 500), (350, 540), (400, 500), (350, 460),
}) { FillColor = Color.FromBytes(220, 230, 255) });

// File attachment — embed a payload as an indirect file spec.
var ef = new EmbeddedFile("data.json", File.ReadAllBytes("data.json"));
page.AddAnnotation(new FileAttachmentAnnotation(72, 400, 16, 16, ef)
{
    Contents = "Source data",
});

// Other supported subtypes: CaretAnnotation, PopupAnnotation,
// SoundAnnotation, RedactionAnnotation, InkAnnotation.

AcroForm widgets

Form fields are added through Page factories and surface as both the page's /Annots entries and the document's /AcroForm /Fields array.

using JetsonPDF.Forms;

var page = doc.AddPage(PageSize.Letter);
var font = new Font(FontFamily.Helvetica, 11);

// Text field with several common knobs.
var name = page.AddTextField("name", x: 72, y: 700, width: 300, height: 20, font);
name.MaxLength = 60;
name.Value = "Jane Doe";
name.Alignment = FieldAlignment.Left;
name.IsMultiline = false;
name.IsRequired = true;

// Check box.
page.AddCheckBox("agree", x: 72, y: 670, width: 14, height: 14)
   .IsChecked = true;

// Radio group — share a single field name across mutually-exclusive options.
var radio = page.AddRadioGroup("size");
radio.Add("S", x: 72, y: 640, width: 14, height: 14);
radio.Add("M", x: 92, y: 640, width: 14, height: 14);
radio.Add("L", x: 112, y: 640, width: 14, height: 14);
radio.SelectedValue = "M";

// Combo box / list box.
var region = page.AddComboBox("region", 72, 600, 200, 18, font);
region.AddOption("NA"); region.AddOption("EMEA"); region.AddOption("APAC");
region.SelectedValue = "NA";

// Push button with a Named action.
var print = page.AddPushButton("print", 72, 560, 80, 22, font, caption: "Print");
print.Action = new NamedAction("Print");

// Signature field — empty placeholder for the Signer to fill.
page.AddSignatureField("sig", x: 380, y: 560, width: 160, height: 40);

// 2D barcode — bring your own encoder (ZXing, QRCoder, …) for the matrix.
bool[,] modules = MyEncoder.QrCode("https://example.com/order/123");
page.AddBarcodeField("barcode", 380, 480, 60, 60, BarcodeSymbology.QrCode, modules);

Widget-level extras: per-field /AA triggers via widget.AdditionalActions (keystroke, format, validate, calculate for Tx; mouse / focus for any annotation), widget.FieldLock for static post-sign field locking, and SignatureField.SeedValue for /SV seed values. For fields that compute from other fields, push the dependents into doc.CalculationOrder in order — the AcroForm dictionary's /CO will reflect that.

Encryption

doc.Encryption = new EncryptionOptions
{
    UserPassword = "openme",
    OwnerPassword = "fullcontrol",
    Algorithm = EncryptionAlgorithm.Aes256,    // PDF 2.0 strength
    Permissions = Permissions.AllowPrinting    // bitwise OR of allowed ops
                | Permissions.AllowCopying,
};

Supported algorithms cover the full standard-security-handler spectrum:

Algorithm Notes
RC4_40 Legacy V=1, R=2. 40-bit RC4.
RC4_128 V=2, R=3. 128-bit RC4.
Aes128 V=4, R=4. AES-128 with CBC + PKCS#5. Default.
Aes256 V=5, R=6. AES-256 — PDF 2.0 strength.

Encryption is incompatible with object streams at present; the writer will fall back to classic xref / trailer syntax when both are set.

Document metadata

Outlines (bookmarks)

var intro = doc.Outlines.Add("Introduction", Destination.FitEntire(0));
intro.Bold = true; intro.Expanded = true;
intro.AddChild("Background", Destination.Xyz(0, top: 720));
intro.AddChild("Scope",      Destination.Xyz(0, top: 480));

doc.Outlines.Add("Chapter 1", Destination.FitEntire(1));
doc.Outlines.Add("Chapter 2", Destination.FitEntire(3));

Named destinations

doc.NamedDestinations["intro"] = Destination.FitEntire(0);
doc.NamedDestinations["data-table"] = Destination.Xyz(2, left: 72, top: 720);

// Outline items and link annotations can reference them by name.
doc.Outlines.Add("Jump to data", "data-table");

Page labels

doc.PageLabels.Add(new PageLabelRange(startPageIndex: 0,
    style: PageLabelStyle.LowerRoman));                  // i, ii, iii, iv
doc.PageLabels.Add(new PageLabelRange(startPageIndex: 4,
    style: PageLabelStyle.DecimalArabic));               // 1, 2, 3, …
doc.PageLabels.Add(new PageLabelRange(startPageIndex: 48,
    style: PageLabelStyle.DecimalArabic, prefix: "A-")); // A-1, A-2, …

Optional content / layers

var design = new OptionalContentGroup("Design") { VisibleByDefault = true };
var notes  = new OptionalContentGroup("Reviewer notes") { VisibleByDefault = false };
doc.OptionalContentGroups.Add(design);
doc.OptionalContentGroups.Add(notes);

using (page.BeginLayer(design)) page.DrawText("Visible by default", helv, 72, 500);
using (page.BeginLayer(notes))  page.DrawText("Hidden by default",  helv, 72, 480);

Embedded files

var spec = new EmbeddedFile("invoice.xml",
    bytes: File.ReadAllBytes("invoice.xml"),
    mimeType: "text/xml");

doc.AssociatedFiles.Add(new AssociatedFile(spec, AfRelationship.Source));

Document-level /AF is the load-bearing entry for Factur-X / ZUGFeRD PDF/A-3 invoices.

Raw XMP

doc.XmpMetadata = File.ReadAllText("custom-xmp.xml");

When Conformance is set and XmpMetadata is left null, the writer auto-builds a packet with the appropriate pdfaid / pdfuaid namespaces.

Piece info

doc.PieceInfo["MyApp"] = new PieceInfoEntry
{
    LastModified = DateTimeOffset.UtcNow,
    Private = new Dictionary<string, string>
    {
        ["DesignerVersion"] = "1.4.2",
    },
};

Tagged PDF / structure tree

Set MarkAsTagged = true and wrap content in BeginTag blocks. Nested calls produce nested structure elements; the document gains a real /StructTreeRoot.

doc.MarkAsTagged = true;
doc.Language = "en-US";
doc.RoleMap["MyHeading"] = "H1";

using (var section = page.BeginTag("Sect"))
{
    section.Title = "Introduction";
    section.Language = "en-US";

    using (var h = page.BeginTag("H1"))
    {
        h.ActualText = "Introduction";
        page.DrawText("Introduction", boldFont, 72, 750);
    }

    using (var p = page.BeginTag("P"))
        page.DrawTextBlock(introBody, helv, 72, 720, width: 450);

    using (var fig = page.BeginTag("Figure"))
    {
        fig.Alt = "Sales by region";
        page.DrawImage(chart, 72, 500, 450, 200);
    }
}

Element-level attributes go through PdfStructureAttributes. Shared attribute bundles can live in doc.ClassMap and be referenced from element.ClassNames instead of repeating the same attrs on every element.

Annotations can be wired into the tree as /OBJR children:

var link = page.AddLink(72, 400, 100, 16, "https://example.com");
using (var p = page.BeginTag("P"))
    p.AnnotationRefs.Add(link);

Measurements and viewports

Geometric annotations and page-level viewports can carry real-world unit metadata so a viewer can show measurements on hover.

// Page-level viewport (e.g. a map covering part of the page).
var rl = PdfMeasure.Rectilinear(
    scaleRatio: "1 in = 10 ft",
    numberFormats: new[] { PdfNumberFormat.Linear(unitLabel: "ft", precision: 1) });

page.Viewports.Add(new Viewport(
    bbox: (x: 72, y: 100, width: 450, height: 400),
    name: "Site plan",
    measure: rl));

// Per-annotation /Measure (length on a line).
var line = new LineAnnotation(72, 50, 450, 50) { Measure = rl };
page.AddAnnotation(line);

Geospatial measurement (EPSG-coded or WKT-coded coordinate systems) goes through PdfCoordinateSystem and PdfMeasure.Geospatial.

Conformance

doc.Conformance = Conformance.PdfA2a | Conformance.PdfUA1;
doc.Language = "en-US";
doc.OutputIntent = OutputIntent.SrgbIec61966();

// Find issues without throwing.
foreach (var issue in doc.Validate())
    Console.WriteLine($"{issue.Severity} [{issue.Code}] {issue.Message}");

// Or have Save throw on any error.
doc.ThrowOnConformanceError = true;
doc.Save("compliant.pdf");

Supported conformance declarations:

Flag Standard
PdfA1b ISO 19005-1 Level B
PdfA2b / PdfA2a / PdfA2u ISO 19005-2 Level B / A / U
PdfA3b / PdfA3a / PdfA3u ISO 19005-3 Level B / A / U
PdfUA1 ISO 14289-1 (accessible PDF, PDF 1.7)
PdfUA2 ISO 14289-2 (accessible PDF, PDF 2.0)

Setting a flag controls the catalog flag, the XMP identifier namespaces, and viewer-pref auto-population (DisplayDocTitle for PDF/UA). Content validation is opt-in via Validate() — it covers high-impact rules (embedded fonts, /Lang, tag tree, output intent, …) without claiming veraPDF parity.

Edit mode and incremental updates

Document.Open wraps an existing parsed PDF. Save then emits an incremental update (§7.5.6) — the original bytes are preserved verbatim and a trailing section appends whatever you mutated.

var doc = Document.Open("input.pdf");

// Patch metadata.
doc.Title = "New title";
doc.Author = "Editor";

// Fill form values by fully-qualified field name.
doc.Forms["customer.name"] = "Jane Doe";
doc.Forms["agree"] = "Yes";

// Page-tree mutations.
doc.RotatePage(0, degrees: 90);
doc.MovePage(fromIndex: 2, toIndex: 0);
doc.RemovePage(5);

// Stamp a Form XObject on an existing page.
doc.StampPage(pageIndex: 0, form: watermark, x: 200, y: 700);

// Replace a page's content stream while preserving its /MediaBox + /Annots.
doc.ReplacePageContents(pageIndex: 1, draw: page =>
{
    page.DrawText("Replaced page", new Font(FontFamily.Helvetica, 24), 72, 700);
});

// Append new pages.
var added = doc.AddPage(PageSize.Letter);
added.DrawText("Appended page", new Font(FontFamily.Helvetica, 12), 72, 720);

doc.Save("input-edited.pdf");

For metadata-only updates, the standalone IncrementalUpdater is a thinner entry point that doesn't need a full Document:

var upd = IncrementalUpdater.Open("input.pdf");
upd.Title = "Final";
upd.Producer = "MyApp 2.0";
upd.Save("input-meta.pdf");

Edit mode also exposes doc.Source — the full reader DOM — so you can walk page items and combine reads with writes (echo + replace, find + highlight, etc.):

var read = doc.Source!.Pages[0];
foreach (var item in read.Items.OfType<JetsonPDF.Reading.PageTextItem>())
{
    if (item.Text == "OLD")
        doc.ReplacePageContents(0, p => p.EchoTextItem(item, "NEW", helv));
}

Linearization

Linearizer.Linearize(bytes) rewrites a saved PDF into the Annex F fast-web-view layout — first-page bytes up front, then a Primary Hint Stream carrying real Page Offset and Shared Object hint tables, then the remaining pages, then shared objects, with a trailing /Prev-chained xref.

byte[] saved = doc.Save();                // in-memory
byte[] linear = Linearizer.Linearize(saved);
File.WriteAllBytes("fast-web.pdf", linear);

Caveats: classic xref / trailer output only (xref-stream input is accepted), and encrypted input is not supported (the linearizer needs plaintext object bodies).

Digital signatures

Signer.Sign produces an invisible PKCS#7 detached signature via an incremental update. The signature widget is appended to the document's /AcroForm. Re-running on the output produces a second incremental update — multi-signature documents work by chaining.

using var cert = new X509Certificate2("signer.pfx", "password");

byte[] saved = doc.Save();
byte[] signed = Signer.Sign(saved, new SignatureOptions(cert)
{
    Reason = "I am the author",
    Location = "Seattle, WA",
    ContactInfo = "hello@example.com",
    HashAlgorithm = HashAlgorithmName.SHA256,
    DocMdpPermission = DocMdpPermission.FormFilling,    // certification sig
});
File.WriteAllBytes("signed.pdf", signed);

For long-term-validation (PAdES-LTA) flows, layer a document timestamp and a /DSS Document Security Store on top:

// 1. RFC 3161 timestamp via a TSA client delegate (HTTP factory provided).
byte[] withTs = Timestamper.AddDocumentTimestamp(signed,
    new TimestampOptions
    {
        Client = Timestamper.CreateHttpClient(tsaUrl: "https://freetsa.org/tsr"),
        HashAlgorithm = HashAlgorithmName.SHA256,
    });

// 2. Bundle the validation material (signer certs, CRLs, OCSP responses)
//    into a /DSS dict + per-signature /VRI sub-dicts.
var dss = new DocumentSecurityStore();
dss.Certificates.Add(signerCertBytes);
dss.Certificates.Add(intermediateCertBytes);
dss.OcspResponses.Add(ocspBytes);
dss.Vri[signatureHex] = new VriEntry { /* … */ };

byte[] lta = Dss.AddSecurityStore(withTs, dss);
File.WriteAllBytes("signed-lta.pdf", lta);

SignatureOptions.FieldMdpRestriction and widget.FieldLock / SignatureField.SeedValue give the field-MDP / /Lock / /SV machinery for signature constraints (§12.7 / §12.8).

Save overloads

doc.Save("out.pdf");      // path
doc.Save(stream);         // any writable Stream
byte[] bytes = doc.Save(); // in-memory

In edit mode, every overload emits the original bytes plus an incremental section. In new-document mode, the writer builds the full file from scratch — object stream + xref stream layout when UseObjectStreams, the classic layout otherwise.

ThrowOnConformanceError runs ConformanceValidator first and throws a ConformanceException before writing anything when an error-severity issue is found.

Limitations

  • Encryption + object streams are mutually exclusive. Setting Encryption while UseObjectStreams = true falls back to classic xref / trailer output. PDF 2.0 strictly allows the combination via encrypted xref streams; that wiring is queued.
  • Linearizer accepts xref streams but emits classic xref output. Encrypted input isn't supported — the linearizer needs plaintext object bodies for its BFS closure and hint tables.
  • Standard 14 fonts use WinAnsi (Windows-1252) encoding. Anything outside CP1252 (↑↓→›‹ arrows, em dashes outside CP1252, non-Latin scripts) renders as ?. Embed a TrueType face for full Unicode.
  • Mesh shadings (types 4–7) are read-side only. Writer-side mesh emission is deferred; use Coons / tensor patches via PatchTessellator if you need a discrete tessellation, or stick to axial / radial / function-based for smooth fills.
  • Signature appearances are invisible. A signature widget is added to the AcroForm but has no /AP stream — viewers show their default "signed" chrome. Custom appearance is a FormXObject away but isn't wired into Signer yet.
  • Conformance is declarative, not constructive. Validate() covers high-impact rules but doesn't claim veraPDF parity; the writer trusts the caller to keep within the declared subset.

Targets

  • net8.0
  • netstandard2.0
  • net462

License

MIT.

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

NuGet packages (6)

Showing the top 5 NuGet packages that depend on JetsonPDF.Writer:

Package Downloads
JetsonPDF.XamlToPdfConverter.Core

Runtime-neutral core for the XAML-to-PDF authoring pipeline. Owns the snapshot model, PDF emission, pagination, font/Std14 mapping, and path/geometry flattening. Consumed by the WPF and OpenSilver tree-walker adapters.

JetsonPDF.Fluent

Headless, code-first fluent API for building PDFs with JetsonPDF — QuestPDF-style containers, auto-pagination, dynamic page numbers.

JetsonPDF.Wpf

WPF integration for JetsonPDF. Bundles the XAML-to-PDF authoring pipeline (xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025" dialect, driven by real WPF Measure/Arrange) and the PDF-to-XAML viewer-emitter (turns a parsed ReadDocument into WPF XAML that XamlReader.Parse renders directly). Authoring types live under JetsonPDF.Wpf.Authoring; viewer/markup types live under JetsonPDF.Wpf. Windows-only (net8.0-windows + WPF).

JetsonPDF.Flow

Word-like retained-mode DOM (Section / Paragraph / Run / Table) with auto-pagination, layered on JetsonPDF.Fluent.

JetsonPDF.OpenSilver

OpenSilver integration for JetsonPDF. Bundles both directions of the XAML pipeline. Authoring (XAML→PDF): the same xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025" dialect as the WPF integration, driven by the OpenSilver runtime so the same XAML compiles to a PDF from inside a WebAssembly app, an Edge WebView2 simulator, or a Playwright-driven Chromium CLI. Viewer (PDF→XAML): emits XAML that OpenSilver's XamlReader.Load renders in the browser — text via vector paths (no font cache), images via base64 data URIs, AcroForm widgets as live OpenSilver controls. Walker + image encoder + widget-action dispatch are OpenSilver-specific; emission code, snapshot model, and PDF emission are shared with the WPF flavour. Authoring types live under JetsonPDF.OpenSilver.Authoring; viewer/markup types under JetsonPDF.OpenSilver.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 52 6/6/2026
1.0.0 314 5/23/2026
0.2.0-preview 315 5/23/2026
0.1.0-preview 312 5/17/2026