JetsonPDF.Forms
1.1.0
dotnet add package JetsonPDF.Forms --version 1.1.0
NuGet\Install-Package JetsonPDF.Forms -Version 1.1.0
<PackageReference Include="JetsonPDF.Forms" Version="1.1.0" />
<PackageVersion Include="JetsonPDF.Forms" Version="1.1.0" />
<PackageReference Include="JetsonPDF.Forms" />
paket add JetsonPDF.Forms --version 1.1.0
#r "nuget: JetsonPDF.Forms, 1.1.0"
#:package JetsonPDF.Forms@1.1.0
#addin nuget:?package=JetsonPDF.Forms&version=1.1.0
#tool nuget:?package=JetsonPDF.Forms&version=1.1.0
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
- Quick start
- Core concepts
- Field types
- Reading fields
- Mutating fields
- Image stamping
- Signature fields
- Saving
- Encrypted PDFs
- Limitations
- Targets
- License
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
Forminstance 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:
- Builds a fresh modified-objects map from the pending-mutations dictionary.
- Clones every touched field root and widget shallowly off the original parse tree so mutations don't accumulate across saves.
- Allocates new object numbers strictly past the source file's highest existing number for any newly-introduced XObjects (image streams, appearance forms).
- 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/.AsPushButtonhelpers inJetsonPDF.Fluent, or the lower-levelPdfWidgettypes inJetsonPDF.Writer. - No
/RVrich text.SetTextwrites plain/VUTF-16BE text strings. Rich-text values (/RVXHTML) 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
SetImagethrows on a signed/FT /Sigwidget. This is by design — modifying/AP /Nfalls inside the byte range covered by/Contentsand invalidates the seal. - Custom appearance scripts aren't preserved. When a mutation needs the
AcroForm regen pass (
SetText,SetChoice,Clearon a text field), any baked/APon the affected widgets is stripped so the viewer regenerates the appearance from/V+/DA. The behaviour matches Adobe Acrobat's own form-fill output. /NeedAppearancesis 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/Vdirectly (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
/Vand/AS; the existing/APdictionary remains in place.
Targets
net8.0netstandard2.0net462
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.6.2
- JetsonPDF.Common (>= 1.1.0)
- JetsonPDF.Reader (>= 1.1.0)
- JetsonPDF.Writer (>= 1.1.0)
- Microsoft.Bcl.HashCode (>= 6.0.0)
- System.ValueTuple (>= 4.5.0)
-
.NETStandard 2.0
- JetsonPDF.Common (>= 1.1.0)
- JetsonPDF.Reader (>= 1.1.0)
- JetsonPDF.Writer (>= 1.1.0)
- Microsoft.Bcl.HashCode (>= 6.0.0)
-
net8.0
- JetsonPDF.Common (>= 1.1.0)
- JetsonPDF.Reader (>= 1.1.0)
- JetsonPDF.Writer (>= 1.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0 | 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 |