Skip to content

Add existing PDF form authoring and widget actions - #2266

Open
PrzemyslawKlys wants to merge 3 commits into
feature/pdf-interactive-actionsfrom
feature/pdf-form-authoring
Open

Add existing PDF form authoring and widget actions#2266
PrzemyslawKlys wants to merge 3 commits into
feature/pdf-interactive-actionsfrom
feature/pdf-form-authoring

Conversation

@PrzemyslawKlys

@PrzemyslawKlys PrzemyslawKlys commented Aug 10, 2026

Copy link
Copy Markdown
Member

Summary

  • add existing-document AcroForm authoring for text and multiline fields, check boxes, list/combo choices, labeled radio groups, push buttons, and empty signature fields
  • generate reusable widget appearances and semantic field styles through the existing form-appearance owner, including visible radio labels and push-button captions
  • attach bounded widget JavaScript using primary /A or activation /AA /U, and expose full dictionary/array /Next chains as inert typed read models without executing them
  • preserve field-owned active content through proven form rewrites while rejecting unrelated rewrite blockers through the canonical mutation planner
  • prune orphaned indirect widget action graphs during full or selective flattening while retaining action objects still reachable from live document owners
  • keep legacy raw choice flags and empty-choice behavior compatible, snapshot mutable edit inputs/results, and enforce existing-plus-new widget JavaScript budgets

Example

PdfAcroFormEditResult edited = PdfDocument.Open("input.pdf").Forms.Edit(form => form
    .Create(new PdfFormFieldCreateOptions {
        Name = "customer.notes",
        Kind = PdfFormFieldCreationKind.Text,
        PageNumber = 1,
        X = 72,
        Y = 560,
        Width = 240,
        Height = 60,
        Style = new PdfFormFieldStyle { IsMultiline = true }
    })
    .Create(new PdfFormFieldCreateOptions {
        Name = "calculate",
        Kind = PdfFormFieldCreationKind.PushButton,
        PageNumber = 1,
        X = 72,
        Y = 520,
        Width = 100,
        Height = 24,
        Caption = "Calculate",
        JavaScript = "this.getField('total').value = 42;"
    }));

Validation

  • full OfficeIMO.Pdf.Tests: 3,480/3,480 on .NET 8 and 3,480/3,480 on .NET 10
  • OfficeIMO.Pdf netstandard2.0 build: no warnings or errors
  • representative authored form reopened with five canonical fields and seven page widgets, each with a normal appearance; Poppler rendering confirmed clean multiline, checkbox, combo, labeled-radio, and push-button output
  • cyclic chained-action traversal, aggregate budgets, flatten graph cleanup, and preflight/planner alignment are covered by focused regressions
  • independent parser/serialization/sanitizer review completed; all local and delayed actionable findings were fixed

Stack

This PR depends on #2265 for the shared safe JavaScript string encoding and document-action foundation. It is intentionally based on feature/pdf-interactive-actions.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7eed52d001

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread OfficeIMO.Pdf/Manipulation/PdfMutationPlanner.cs
Comment thread OfficeIMO.Pdf/Reading/Core/PdfReadDocument.Forms.cs
Comment thread OfficeIMO.Pdf/Reading/Model/PdfDocumentPreflight.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d51d1b35aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +213 to +216
if ((options.Kind == PdfFormFieldCreationKind.RadioButtonGroup ||
options.Kind == PdfFormFieldCreationKind.Choice && options.ChoiceOptions.Count > 0) &&
string.IsNullOrEmpty(options.Value)) {
return options.ChoiceOptions[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve empty initial choice values

When a caller supplies nonempty ChoiceOptions but leaves Value at its documented empty default, this branch substitutes the first option. The validation explicitly accepts empty choice values, and the previous creation path wrote options.Value directly, so list and combo fields can no longer be authored in an initially unselected state. Restrict this fallback to radio groups or otherwise distinguish an unspecified radio value.

Useful? React with 👍 / 👎.

if (!string.IsNullOrEmpty(options.Value) && !isEditableChoice && !options.ChoiceOptions.Contains(options.Value, StringComparer.Ordinal)) throw new ArgumentException("Choice value must match one of the provided options.", nameof(options));
if (options.DefaultValue is not null && !isEditableChoice && !options.ChoiceOptions.Contains(options.DefaultValue, StringComparer.Ordinal)) throw new ArgumentException("Choice default value must match one of the provided options.", nameof(options));
}
if (options.Style?.IsComb == true && !options.Style.MaxLength.HasValue) throw new ArgumentException("Comb text fields require a maximum length.", nameof(options));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject incompatible comb styles

When a text field sets IsComb and MaxLength together with IsMultiline, IsPassword, or IsFileSelect, this validation passes and GetCreateFieldFlags emits the incompatible flags. The existing writer rejects exactly these combinations in PdfAnnotationDictionaryBuilder.ValidateCombTextFieldStyle, while the appearance builder may render the field as multiline instead of comb, leaving an invalid or ambiguous AcroForm field. Apply the same exclusions in this authoring path.

Useful? React with 👍 / 👎.

TryReadInteger(dictionary, "F"),
ReadWidgetNormalAppearanceStates(dictionary));
ReadWidgetNormalAppearanceStates(dictionary),
ReadWidgetActions(dictionary, actionBudget));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Budget actions before rejecting widget geometry

For a malformed or geometry-less widget whose /Rect is absent or unreadable, the earlier guard returns before this action reader is invoked. Any /A, /AA, or chained JavaScript on that widget is therefore omitted from the typed action inventory and from FormWidgetJavaScriptCount/FormWidgetJavaScriptBytes, allowing existing scripts to bypass the configured widget-action budgets when a subsequent form edit adds more JavaScript. Scan and budget actions independently of whether the widget geometry can be exposed.

Useful? React with 👍 / 👎.

Comment on lines +55 to +58
if (options.Kind == PdfFormFieldCreationKind.Text && options.Style?.IsMultiline == true && !field.IsMultiline) throw new InvalidOperationException("AcroForm multiline text-field readback validation failed for " + options.Name + ".");
if (options.Kind == PdfFormFieldCreationKind.Choice && field.IsCombo != IsChoiceComboBox(options)) throw new InvalidOperationException("AcroForm choice presentation readback validation failed for " + options.Name + ".");
if (options.Kind == PdfFormFieldCreationKind.RadioButtonGroup && (!field.IsRadioButton || field.WidgetCount != options.ChoiceOptions.Count)) throw new InvalidOperationException("AcroForm radio-button readback validation failed for " + options.Name + ".");
if (options.Kind == PdfFormFieldCreationKind.PushButton && !field.IsPushButton) throw new InvalidOperationException("AcroForm push-button readback validation failed for " + options.Name + ".");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for later flag edits during create readback

When one transaction creates a choice field and then calls SetFlags to change its combo bit, the saved field legitimately reflects the later command, but this check still compares it with the original create options and throws before the successful result can be returned. The same conflict occurs when later flags clear an authored multiline, radio, or push-button bit. Evaluate create assertions against the final command state, or skip flag-derived create checks when a later SetFlags targets the field.

Useful? React with 👍 / 👎.


return new PdfSanitizationResult(sanitized, plan, before, remaining, quarantined);
var preservationOptions = new PdfRewritePreservationOptions {
OriginalReadOptions = readOptions,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse custom read limits for sanitized readback

When sanitization is invoked through a PdfDocument opened with raised semantic limits, only the original inspection receives those options; AssertPreserved inspects the rewritten artifact with default limits. For example, a source accepted with MaxFormFields above the default can be rewritten successfully and pass the preceding sanitizer scan, then fail here solely because preservation readback falls back to the default field limit. Supply corresponding RewrittenReadOptions, as the AcroForm editor does for its rewritten artifact.

Useful? React with 👍 / 👎.

Comment on lines +55 to +57
PreserveCatalogActions = false,
PreservePageActions = false,
PreserveOpenAction = false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prove preservation of allowlisted actions

When AllowedActionTypes retains an action such as JavaScript, that action is outside the sanitization removal policy, but these hardcoded settings disable preservation comparisons for catalog, page, and open actions unconditionally. Consequently PreservationReport.IsPreserved can remain true even if the rewrite accidentally drops an explicitly allowlisted action, contrary to the new property's claim to prove preservation outside the policy. Preserve or selectively compare action surfaces that the active policy allows.

Useful? React with 👍 / 👎.

Comment on lines +114 to +116
PdfFormFieldStyle style = options.Style?.Clone() ?? new PdfFormFieldStyle();
style.TextAlignment = PdfFormFieldTextAlignment.Center;
PdfStream appearance = PdfFormFiller.CreateAuthoredTextWidgetAppearance(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip text-only flags from button appearance styles

When a style intended for reuse has a text-only option such as IsPassword set, this clone is passed unchanged into the text-field appearance builder for a push-button caption; GetTextFieldAppearanceDisplayValue then masks the caption with asterisks even though GetCreateFieldFlags correctly does not mark the push button as a password field. The labeled-radio path similarly clones the full style and can mask its option labels. Clear text-only behaviors such as password, multiline, and comb before rendering button captions and radio labels.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant