JetsonPDF.Forms 1.1.0

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

JetsonPDF.Forms

Open an existing PDF, discover its AcroForm fields, modify them, and save back through a single incremental-update layer. Built on top of JetsonPDF.Reader and JetsonPDF.Writer, with no WPF dependency and no STA-thread constraint.

using JetsonPDF;
using JetsonPDF.Forms;

using var form = Form.Open("input.pdf");

foreach (var field in form.Fields)
    Console.WriteLine($"{field.Name}: {field.Type} = {field.Value}");

form.Fields["Name"].SetText("Jordan");
form.Fields["IsActive"].SetChecked(true);
form.Fields["State"].SetChoice("CA");
form.Fields["Photo"].SetImage(Image.FromFile("signature.jpg"));

form.Save("output.pdf");

Contents


Overview

JetsonPDF.Forms is the form-filling wrapper around the low-level JetsonPDF.Reader and JetsonPDF.Writer APIs. Where JetsonPDF.Fluent builds documents from scratch and JetsonPDF.Wpf.Authoring drives generation from a XAML tree, JetsonPDF.Forms covers the third scenario: take an existing PDF (authored by anyone in anything), inspect its AcroForm widgets, change their values, and save the result.

The package never rewrites the whole file. Every save layers a single incremental update (ISO 32000-2 §7.5.6) onto the original bytes, which means:

  • The original PDF's signed regions, structure tree, page tree, and content streams are bit-for-bit preserved.
  • Repeated saves of the same Form instance produce byte-identical output.
  • Encrypted PDFs round-trip without re-keying — new objects are written under the source file's existing encryption key.

Quick start

Add a project reference to JetsonPDF.Forms and the usings:

using JetsonPDF;
using JetsonPDF.Forms;

Open a PDF (path, Stream, or byte[]), enumerate the discovered fields, queue mutations, and save:

using var form = Form.Open("application.pdf");

foreach (var field in form.Fields)
{
    string options = field.Options.Count > 0
        ? $"  options: [{string.Join(", ", field.Options.Select(o => o.Export))}]"
        : "";
    Console.WriteLine($"  {field.Name,-20} {field.Type,-10} = {field.Value}{options}");
}

form.Fields["FullName"].SetText("Jordan Duerksen");
form.Fields["Subscribe"].SetChecked(true);
form.Fields["Region"].SetChoice("NA");
form.Fields["Sizes"].SetChoices(new[] { "S", "M" });
form.Fields["Plan"].SelectRadio("Annual");

form.Save("application-filled.pdf");

Password-protected source documents are opened with the password overload:

using var form = Form.Open("encrypted.pdf", password: "hunter2");

The same overload exists for byte[] and Stream inputs.

Core concepts

Type Role
Form Top-level handle. Opens a source PDF, exposes Fields, queues mutations, and writes the result. IDisposable.
Form.Fields Read-only collection of every discovered field, by qualified name and by enumeration.
FormField A single AcroForm field. Carries metadata (Name, Type, Value, IsSigned, Options, MaxLength, IsReadOnly, IsRequired, Rect, PageIndex) and the type-safe setters.
FormFieldType Discriminator enum (Text, CheckBox, Radio, PushButton, ComboBox, ListBox, Signature, Unknown).
ImageAlignment Placement enum for SetImage (Center, TopLeft, TopRight, BottomLeft, BottomRight, Stretch).
SaveMode Save strategy. Only SingleLayer ships in v1; the parameter is there so the caller's intent is explicit.

Field grouping. One FormField represents one qualified field name. A radio group whose member widgets all share /T = "Plan" and live under one parent dictionary is exposed as a single field whose SelectRadio("Annual") flips every member widget's /AS to the right state in one call. Field names are joined through the /Parent chain with periods, matching ISO 32000-2 §12.7.4.2 (e.g. "address.city").

The chain idiom: every FormField setter returns this, so calls compose:

form.Fields["EmployeeName"]
    .SetText("Jordan")
    .Clear()                  // overrides the SetText — last write wins
    .SetText("Jordan D.");    // final queued value

The latest mutation for a given field name wins. Form stores pending mutations in a dictionary keyed by name, so repeated setters on the same field naturally collapse to one operation.

Field types

FormFieldType is derived from the PDF /FT entry plus the relevant /Ff sub-flags:

FormFieldType /FT Flag bits Notes
Text /Tx Single- or multi-line text input. MaxLength carries /MaxLen.
CheckBox /Btn neither Pushbutton (bit 16) nor Radio (bit 15) On-state name is read from the widget's /AP /N (any key other than Off).
Radio /Btn Radio bit (15) Multi-widget group; SelectRadio flips each member's /AS.
PushButton /Btn Pushbutton bit (16) No persistent value. Accepts SetImage only.
ComboBox /Ch Combo bit (17) Accepts SetText (editable text) and SetChoice.
ListBox /Ch no Combo bit SetChoice (single-select) or SetChoices (multi-select if bit 21 is set).
Signature /Sig Discoverable but mutation-restricted (see Signature fields).
Unknown A malformed field dictionary or a /FT value the library doesn't recognise.

Reading fields

FormField exposes everything the discovery walk found:

foreach (var field in form.Fields)
{
    Console.WriteLine(field.Name);          // "address.city"
    Console.WriteLine(field.Type);          // FormFieldType.Text
    Console.WriteLine(field.Value);         // "" or the source /V as text
    Console.WriteLine(field.MaxLength);     // /MaxLen, or null
    Console.WriteLine(field.IsReadOnly);    // /Ff bit 1 set
    Console.WriteLine(field.IsRequired);    // /Ff bit 2 set
    Console.WriteLine(field.IsSigned);      // signature widgets only
    Console.WriteLine(field.Rect);          // (x, y, w, h) in PDF user space
    Console.WriteLine(field.PageIndex);     // 0-based page index of the first widget
}

Options exposes the /Opt array of a combo or list-box as (Export, Display) pairs:

foreach (var (export, display) in form.Fields["Region"].Options)
    Console.WriteLine($"  {export} → {display}");

Value reflects pending mutations as well as the source. A field that has had SetText("hello") queued returns "hello" from Value even though Save() hasn't been called yet.

Form.Fields supports [name], Contains, TryGet, and iteration:

if (form.Fields.Contains("LegacyField"))
    form.Fields["LegacyField"].Clear();

if (form.Fields.TryGet("OptionalField", out var optional))
    optional.SetText("…");

IsReadOnly is a viewer hint, not a runtime constraint — mutating a read-only field still works. Set it back to read-only at the source if you need that behaviour preserved on the output.

Mutating fields

Every mutation queues a pending operation on the parent Form and runs at Save() time. Mutations validate the field's type immediately and throw if the operation isn't valid for the target.

form.Fields["Name"]       .SetText("Jordan");
form.Fields["IsAdmin"]    .SetChecked(true);
form.Fields["IsAdmin"]    .Toggle();              // flips the queued value
form.Fields["Plan"]       .SelectRadio("Annual"); // multi-widget group
form.Fields["Region"]     .SetChoice("NA");       // single-select
form.Fields["Sizes"]      .SetChoices(new[] { "S", "M" }); // multi-select
form.Fields["LegacyData"] .Clear();               // per-field reset
form.Clear();                                     // reset every value field
Mutation Valid field types Throws when
SetText(string) Text, ComboBox called on CheckBox/Radio/PushButton/ListBox/Signature/Unknown
SetChecked(bool) CheckBox called on any other type
Toggle() CheckBox called on any other type
SelectRadio(string) Radio called on any other type
SetChoice(string) ComboBox, ListBox the export value isn't in Options
SetChoices(IEnumerable<string>) ListBox (multi-select only) the field is single-select, or any value isn't in Options
Clear() (field) any non-button, non-signature silently no-ops on PushButton/Signature
Form.Clear() resets every non-button, non-signature field
SetImage(...) Text, PushButton, unsigned Signature called on a signed signature or any other type

SetChecked(true) discovers the widget's on-state name automatically from its /AP /N dictionary (any key other than Off). If the widget has no appearance dictionary, the library falls back to Yes.

SelectRadio writes the named state to the field root's /V and flips every member widget's /AS to either that name (if the widget's /AP /N carries it) or Off (otherwise).

SetChoice and SetChoices validate the export values up-front against the field's declared Options. An unknown value throws with a message listing every available export:

Field 'Region' has no option with export value 'XX'. Available: NA, EMEA, APAC.

Image stamping

SetImage bakes a Form XObject into the widget's normal-state appearance (/AP /N). Aspect ratio is preserved by default; six alignment modes plus Stretch cover the common cases.

var photo = Image.FromFile("headshot.jpg");

// Default — Center, 2pt padding, ReadOnly bit set for text widgets.
form.Fields["Photo"].SetImage(photo);

// Custom alignment + padding.
form.Fields["Logo"].SetImage(photo,
    alignment: ImageAlignment.TopRight,
    padding: 4);

// Stretch (aspect ratio ignored).
form.Fields["Background"].SetImage(photo, alignment: ImageAlignment.Stretch);

// Keep a text field editable after stamping.
form.Fields["EditablePhoto"].SetImage(photo, markReadOnly: false);

ImageAlignment values:

Value Effect
Center Centered horizontally + vertically, aspect-preserved (default).
TopLeft Pinned to the upper-left of the padded rect, aspect-preserved.
TopRight Pinned to the upper-right, aspect-preserved.
BottomLeft Pinned to the lower-left, aspect-preserved.
BottomRight Pinned to the lower-right, aspect-preserved.
Stretch Fills the padded rect; aspect ratio ignored.

The markReadOnly quirk

Acrobat treats an empty-/V text widget's /AP /N as a stale cache and refuses to draw it until the widget gets focus — without intervention, a stamped image only appears after the user clicks the field. To work around this, SetImage on a Text field defaults to setting the widget's ReadOnly bit (/Ff bit 1) and clearing the text-specific interactivity bits (Multiline, Password, FileSelect, DoNotSpellCheck, DoNotScroll, Comb, RichText). The field stays a /FT /Tx widget for round-trip compatibility; only the interactivity flags change.

Pass markReadOnly: false to keep the field editable. The stamped image will appear after the widget receives focus in Acrobat (other viewers render it immediately).

SetImage matrix

Field type Allowed? markReadOnly default Effect
Text true /Ff ReadOnly bit set; Acrobat draws /AP /N statically.
Text + markReadOnly: false false Field stays editable; Acrobat only renders the image after focus.
PushButton flag ignored Push buttons have no "edit" mode; the click action keeps firing.
Signature (unsigned) flag ignored ReadOnly is never set — the user must still be able to invoke Sign.
Signature (signed) ✗ throws The cryptographic byte range covers /AP; any change breaks the seal.
CheckBox / Radio / ComboBox / ListBox ✗ throws Type-gated; use SetChecked / SelectRadio / SetChoice.

Stamping an image also clears /V on the field root so the AcroForm appearance-regen pass doesn't overpaint the stamp. The save layer suppresses the catalog's /NeedAppearances flag whenever any image stamp is present — otherwise viewers regenerate every widget's appearance, including the freshly baked photo, and the image vanishes.

Signature fields

/FT /Sig widgets are discoverable but heavily restricted. IsSigned distinguishes a widget that already carries a populated signature dictionary in /V from an empty placeholder waiting to be signed:

foreach (var field in form.Fields.Where(f => f.Type == FormFieldType.Signature))
{
    Console.WriteLine($"{field.Name} — signed: {field.IsSigned}");
}
Widget state Allowed mutations Why
Unsigned /FT /Sig SetImage only Stamping /AP /N doesn't touch any signed byte range — there isn't one yet.
Signed /FT /Sig none Any change to /V, /AP, or /AS falls inside the byte range covered by /Contents and invalidates the cryptographic seal.

Calling SetText / SetChecked / SelectRadio / SetChoice / SetImage on a signed signature throws InvalidOperationException immediately:

SetText is not valid for signature field 'sig1'. Use SetImage to stamp a
visual into an unsigned signature widget; all other mutations would
invalidate a signed field.

Form.Clear() skips both push buttons and signatures by design.

Saving

Form.Save has four overloads: parameterless byte[], mode-selecting byte[], file path, and Stream:

byte[] bytes = form.Save();              // SaveMode.SingleLayer (default)
byte[] bytes = form.Save(SaveMode.SingleLayer);
form.Save("output.pdf");
form.Save(outputStream);

The only SaveMode in v1 is SingleLayer. The parameter is there so the caller's intent is explicit — future modes (e.g. full rewrite, hybrid) would slot in here without breaking the signature.

Single-layer save guarantee

Within one Form instance, repeated Save() calls produce byte-identical output. Each save:

  1. Builds a fresh modified-objects map from the pending-mutations dictionary.
  2. Clones every touched field root and widget shallowly off the original parse tree so mutations don't accumulate across saves.
  3. Allocates new object numbers strictly past the source file's highest existing number for any newly-introduced XObjects (image streams, appearance forms).
  4. Emits one incremental-update section on top of the original raw bytes.

This avoids the file-bloat and stale-appearance failure modes that naive incremental editors fall into. To demonstrate:

form.Fields["Name"].SetText("Jordan");
form.Fields["Photo"].SetImage(img);

byte[] a = form.Save();
byte[] b = form.Save();
Debug.Assert(a.AsSpan().SequenceEqual(b));   // always passes

When no mutations have been queued, Save() returns a clone of the original raw bytes — no incremental update is appended.

Encrypted PDFs

Password-protected source PDFs are supported directly:

using var form = Form.Open("encrypted.pdf", password: "hunter2");
form.Fields["Name"].SetText("Jordan");
form.Save("encrypted-filled.pdf");

The incremental update encrypts newly added objects under the source file's existing encryption key (V2/V4 RC4, V4/V5 AES-128, V5 AES-256 — whichever the source uses). No re-keying happens; the output's /Encrypt dictionary is the same one the source carried, just with new objects covered by the same key material.

Limitations

  • Field creation and deletion are out of scope. This package edits existing AcroForm fields. To author a brand-new form from scratch, use the fluent API's .AsTextField / .AsCheckBox / .AsComboBox / .AsListBox / .AsPushButton helpers in JetsonPDF.Fluent, or the lower-level PdfWidget types in JetsonPDF.Writer.
  • No /RV rich text. SetText writes plain /V UTF-16BE text strings. Rich-text values (/RV XHTML) on the source are preserved as-is on the field root but aren't produced or updated by mutations.
  • Signed signatures reject every mutation. Even SetImage throws on a signed /FT /Sig widget. This is by design — modifying /AP /N falls inside the byte range covered by /Contents and invalidates the seal.
  • Custom appearance scripts aren't preserved. When a mutation needs the AcroForm regen pass (SetText, SetChoice, Clear on a text field), any baked /AP on the affected widgets is stripped so the viewer regenerates the appearance from /V + /DA. The behaviour matches Adobe Acrobat's own form-fill output.
  • /NeedAppearances is suppressed when any image stamp is present in the same save. Otherwise viewers regenerate every widget's appearance (including the photo) and the stamped image vanishes. Non-image mutations in the same save still work in viewers that re-render text/check fields from /V directly (Adobe Acrobat and Microsoft Edge both do); strict viewers may render a stale appearance for the non-image fields until the file is re-saved through them.
  • Widget appearance for radios and checkboxes uses whatever the source file declared. The library writes only /V and /AS; the existing /AP dictionary remains in place.

Targets

  • net8.0
  • netstandard2.0
  • net462

License

MIT.

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

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 38 6/6/2026
1.0.0 91 5/23/2026
0.2.0-preview 90 5/23/2026
0.1.0-preview 94 5/17/2026