diff --git a/src/UglyToad.PdfPig.Tests/Integration/AcroFormsBasicFieldsTests.cs b/src/UglyToad.PdfPig.Tests/Integration/AcroFormsBasicFieldsTests.cs
index c68a4d16..2c679d7f 100644
--- a/src/UglyToad.PdfPig.Tests/Integration/AcroFormsBasicFieldsTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Integration/AcroFormsBasicFieldsTests.cs
@@ -43,16 +43,15 @@
}
}
- //[Fact]
- //public void GetFormFieldsByPage()
- //{
- // using (var document = PdfDocument.Open(GetFilename(), ParsingOptions.LenientParsingOff))
- // {
- // var form = document.GetForm();
- // var fields = form.GetFieldsForPage(1).ToList();
- // var page = document.GetPage(1).ExperimentalAccess.GetAnnotations().ToList();
- // Assert.Equal(16, fields.Count);
- // }
- //}
+ [Fact]
+ public void GetFormFieldsByPage()
+ {
+ using (var document = PdfDocument.Open(GetFilename(), ParsingOptions.LenientParsingOff))
+ {
+ var form = document.GetForm();
+ var fields = form.GetFieldsForPage(1).ToList();
+ Assert.Equal(18, fields.Count);
+ }
+ }
}
}
diff --git a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
index e1f3fa6a..c274f032 100644
--- a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
+++ b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
@@ -41,6 +41,7 @@
{
"UglyToad.PdfPig.AcroForms.Fields.AcroButtonFieldFlags",
"UglyToad.PdfPig.AcroForms.Fields.AcroCheckboxField",
+ "UglyToad.PdfPig.AcroForms.Fields.AcroCheckboxesField",
"UglyToad.PdfPig.AcroForms.Fields.AcroChoiceFieldFlags",
"UglyToad.PdfPig.AcroForms.Fields.AcroChoiceOption",
"UglyToad.PdfPig.AcroForms.Fields.AcroComboBoxField",
@@ -50,6 +51,7 @@
"UglyToad.PdfPig.AcroForms.Fields.AcroListBoxField",
"UglyToad.PdfPig.AcroForms.Fields.AcroNonTerminalField",
"UglyToad.PdfPig.AcroForms.Fields.AcroPushButtonField",
+ "UglyToad.PdfPig.AcroForms.Fields.AcroRadioButtonField",
"UglyToad.PdfPig.AcroForms.Fields.AcroRadioButtonsField",
"UglyToad.PdfPig.AcroForms.Fields.AcroSignatureField",
"UglyToad.PdfPig.AcroForms.Fields.AcroTextField",
diff --git a/src/UglyToad.PdfPig/AcroForms/AcroForm.cs b/src/UglyToad.PdfPig/AcroForms/AcroForm.cs
index 04558d83..5d4102e5 100644
--- a/src/UglyToad.PdfPig/AcroForms/AcroForm.cs
+++ b/src/UglyToad.PdfPig/AcroForms/AcroForm.cs
@@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
+ using System.Linq;
using Fields;
using Tokens;
using Util.JetBrains.Annotations;
@@ -39,7 +40,7 @@
///
/// Create a new .
///
- public AcroForm(DictionaryToken dictionary, SignatureFlags signatureFlags, bool needAppearances,
+ public AcroForm(DictionaryToken dictionary, SignatureFlags signatureFlags, bool needAppearances,
IReadOnlyDictionary fields)
{
Dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
@@ -64,6 +65,11 @@
{
yield return field.Value;
}
+ else if (field.Value is AcroNonTerminalField parent
+ && parent.Children.Any(x => x.PageNumber == pageNumber))
+ {
+ yield return field.Value;
+ }
}
}
diff --git a/src/UglyToad.PdfPig/AcroForms/AcroFormFactory.cs b/src/UglyToad.PdfPig/AcroForms/AcroFormFactory.cs
index 36d395d7..3713bc51 100644
--- a/src/UglyToad.PdfPig/AcroForms/AcroFormFactory.cs
+++ b/src/UglyToad.PdfPig/AcroForms/AcroFormFactory.cs
@@ -19,6 +19,15 @@
///
internal class AcroFormFactory
{
+ private static readonly HashSet InheritableFields = new HashSet
+ {
+ NameToken.Ft,
+ NameToken.Ff,
+ NameToken.V,
+ NameToken.Dv,
+ NameToken.Aa
+ };
+
private readonly IPdfTokenScanner tokenScanner;
private readonly IFilterProvider filterProvider;
@@ -92,7 +101,7 @@
var fieldDictionary = DirectObjectFinder.Get(fieldToken, tokenScanner);
- var field = GetAcroField(fieldDictionary, catalog);
+ var field = GetAcroField(fieldDictionary, catalog, new List(0));
fields[fieldReferenceToken.Data] = field;
}
@@ -100,12 +109,15 @@
return new AcroForm(acroDictionary, signatureFlags, needAppearances, fields);
}
- private AcroFieldBase GetAcroField(DictionaryToken fieldDictionary, Catalog catalog)
+ private AcroFieldBase GetAcroField(DictionaryToken fieldDictionary, Catalog catalog,
+ IReadOnlyList parentDictionaries)
{
- fieldDictionary.TryGet(NameToken.Ft, out NameToken fieldType);
- fieldDictionary.TryGet(NameToken.Ff, out NumericToken fieldFlagsToken);
+ fieldDictionary = CreateInheritedDictionary(fieldDictionary, parentDictionaries);
- var kids = new List();
+ fieldDictionary.TryGet(NameToken.Ft, tokenScanner, out NameToken fieldType);
+ fieldDictionary.TryGet(NameToken.Ff, tokenScanner, out NumericToken fieldFlagsToken);
+
+ var kids = new List<(bool hasParent, DictionaryToken dictionary)>();
if (fieldDictionary.TryGetOptionalTokenDirect(NameToken.Kids, tokenScanner, out ArrayToken kidsToken))
{
@@ -120,7 +132,8 @@
if (kidObject.Data is DictionaryToken kidDictionaryToken)
{
- kids.Add(kidDictionaryToken);
+ var hasParent = kidDictionaryToken.TryGet(NameToken.Parent, out IndirectReferenceToken _);
+ kids.Add((hasParent, kidDictionaryToken));
}
else
{
@@ -147,19 +160,26 @@
bounds = rectArray.ToRectangle();
}
+ var newParentDictionaries = new List(parentDictionaries) {fieldDictionary};
+
+ var children = new List(kids.Count);
+ foreach (var kid in kids)
+ {
+ if (!kid.hasParent)
+ {
+ // Is a widget annotation dictionary.
+ continue;
+ }
+
+ children.Add(GetAcroField(kid.dictionary, catalog, newParentDictionaries));
+ }
+
var fieldFlags = (uint) (fieldFlagsToken?.Long ?? 0);
AcroFieldBase result;
if (fieldType == null)
{
- var children = new List();
- foreach (var kid in kids)
- {
- var kidField = GetAcroField(kid, catalog);
- children.Add(kidField);
- }
-
- result = new AcroNonTerminalField(fieldDictionary, "Non-Terminal Field", fieldFlags, information, children);
+ result = new AcroNonTerminalField(fieldDictionary, "Non-Terminal Field", fieldFlags, information, AcroFieldType.Unknown, children);
}
else if (fieldType == NameToken.Btn)
{
@@ -167,10 +187,19 @@
if (buttonFlags.HasFlag(AcroButtonFieldFlags.Radio))
{
- var field = new AcroRadioButtonsField(fieldDictionary, fieldType, buttonFlags, information,
- pageNumber,
- bounds);
- result = field;
+ if (children.Count > 0)
+ {
+ result = new AcroRadioButtonsField(fieldDictionary, fieldType, buttonFlags, information,
+ children);
+ }
+ else
+ {
+ var field = new AcroRadioButtonField(fieldDictionary, fieldType, buttonFlags, information,
+ pageNumber,
+ bounds);
+
+ result = field;
+ }
}
else if (buttonFlags.HasFlag(AcroButtonFieldFlags.PushButton))
{
@@ -191,13 +220,21 @@
isChecked = !string.Equals(valueToken.Data, NameToken.Off, StringComparison.OrdinalIgnoreCase);
}
- var field = new AcroCheckboxField(fieldDictionary, fieldType, buttonFlags, information,
- valueToken,
- isChecked,
- pageNumber,
- bounds);
+ if (children.Count > 0)
+ {
+ result = new AcroCheckboxesField(fieldDictionary, fieldType, buttonFlags, information,
+ children);
+ }
+ else
+ {
+ var field = new AcroCheckboxField(fieldDictionary, fieldType, buttonFlags, information,
+ valueToken,
+ isChecked,
+ pageNumber,
+ bounds);
- result = field;
+ result = field;
+ }
}
}
else if (fieldType == NameToken.Tx)
@@ -403,6 +440,34 @@
pageNumber,
bounds);
}
+
+ private static DictionaryToken CreateInheritedDictionary(DictionaryToken fieldDictionary, IReadOnlyList parents)
+ {
+ if (parents.Count == 0)
+ {
+ return fieldDictionary;
+ }
+
+ var inheritedDictionary = new Dictionary();
+ foreach (var parent in parents)
+ {
+ foreach (var kvp in parent.Data)
+ {
+ var key = NameToken.Create(kvp.Key);
+ if (InheritableFields.Contains(key))
+ {
+ inheritedDictionary[key] = kvp.Value;
+ }
+ }
+ }
+
+ foreach (var kvp in fieldDictionary.Data)
+ {
+ inheritedDictionary[NameToken.Create(kvp.Key)] = kvp.Value;
+ }
+
+ return new DictionaryToken(inheritedDictionary);
+ }
private static bool IsChoiceSelected(IReadOnlyList selectedOptionNames, IReadOnlyList selectedOptionIndices, int index, string name)
{
diff --git a/src/UglyToad.PdfPig/AcroForms/Fields/AcroCheckboxesField.cs b/src/UglyToad.PdfPig/AcroForms/Fields/AcroCheckboxesField.cs
new file mode 100644
index 00000000..80a22a33
--- /dev/null
+++ b/src/UglyToad.PdfPig/AcroForms/Fields/AcroCheckboxesField.cs
@@ -0,0 +1,24 @@
+namespace UglyToad.PdfPig.AcroForms.Fields
+{
+ using System.Collections.Generic;
+ using Tokens;
+
+ ///
+ ///
+ /// A set of related checkboxes.
+ ///
+ public class AcroCheckboxesField : AcroNonTerminalField
+ {
+ ///
+ ///
+ /// Create a new .
+ ///
+ internal AcroCheckboxesField(DictionaryToken dictionary, string fieldType, AcroButtonFieldFlags fieldFlags,
+ AcroFieldCommonInformation information,
+ IReadOnlyList children) :
+ base(dictionary, fieldType, (uint)fieldFlags, information,
+ AcroFieldType.Checkboxes, children)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig/AcroForms/Fields/AcroFieldType.cs b/src/UglyToad.PdfPig/AcroForms/Fields/AcroFieldType.cs
index 4ddd5dd4..eaaa88b8 100644
--- a/src/UglyToad.PdfPig/AcroForms/Fields/AcroFieldType.cs
+++ b/src/UglyToad.PdfPig/AcroForms/Fields/AcroFieldType.cs
@@ -10,12 +10,20 @@
///
PushButton,
///
+ /// A set of checkboxes.
+ ///
+ Checkboxes,
+ ///
/// A checkbox which toggles between on and off states.
///
Checkbox,
///
/// A set of radio buttons.
///
+ RadioButtons,
+ ///
+ /// A single radio button, as part of a set or on its own.
+ ///
RadioButton,
///
/// A textbox allowing user input through the keyboard.
@@ -34,8 +42,8 @@
///
Signature,
///
- /// A field which acts as a container for other fields.
+ /// The field type wasn't specified.
///
- NonTerminal
+ Unknown
}
}
diff --git a/src/UglyToad.PdfPig/AcroForms/Fields/AcroNonTerminalField.cs b/src/UglyToad.PdfPig/AcroForms/Fields/AcroNonTerminalField.cs
index 5cef5b4a..7e6b806c 100644
--- a/src/UglyToad.PdfPig/AcroForms/Fields/AcroNonTerminalField.cs
+++ b/src/UglyToad.PdfPig/AcroForms/Fields/AcroNonTerminalField.cs
@@ -19,9 +19,11 @@
///
/// Create a new .
///
- internal AcroNonTerminalField(DictionaryToken dictionary, string fieldType, uint fieldFlags, AcroFieldCommonInformation information,
+ internal AcroNonTerminalField(DictionaryToken dictionary, string fieldType, uint fieldFlags,
+ AcroFieldCommonInformation information,
+ AcroFieldType acroFieldType,
IReadOnlyList children) :
- base(dictionary, fieldType, fieldFlags, AcroFieldType.NonTerminal, information,
+ base(dictionary, fieldType, fieldFlags, acroFieldType, information,
null, null)
{
Children = children ?? throw new ArgumentNullException(nameof(children));
diff --git a/src/UglyToad.PdfPig/AcroForms/Fields/AcroRadioButtonField.cs b/src/UglyToad.PdfPig/AcroForms/Fields/AcroRadioButtonField.cs
new file mode 100644
index 00000000..ff99ea61
--- /dev/null
+++ b/src/UglyToad.PdfPig/AcroForms/Fields/AcroRadioButtonField.cs
@@ -0,0 +1,30 @@
+namespace UglyToad.PdfPig.AcroForms.Fields
+{
+ using Geometry;
+ using Tokens;
+
+ ///
+ ///
+ /// A single radio button.
+ ///
+ public class AcroRadioButtonField : AcroFieldBase
+ {
+ ///
+ /// The which define the behaviour of this button type.
+ ///
+ public AcroButtonFieldFlags Flags { get; }
+
+ ///
+ ///
+ /// Create a new .
+ ///
+ public AcroRadioButtonField(DictionaryToken dictionary, string fieldType, AcroButtonFieldFlags fieldFlags,
+ AcroFieldCommonInformation information,
+ int? pageNumber,
+ PdfRectangle? bounds) :
+ base(dictionary, fieldType, (uint)fieldFlags, AcroFieldType.RadioButton, information, pageNumber, bounds)
+ {
+ Flags = fieldFlags;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig/AcroForms/Fields/AcroRadioButtonsField.cs b/src/UglyToad.PdfPig/AcroForms/Fields/AcroRadioButtonsField.cs
index d39c1645..3701e437 100644
--- a/src/UglyToad.PdfPig/AcroForms/Fields/AcroRadioButtonsField.cs
+++ b/src/UglyToad.PdfPig/AcroForms/Fields/AcroRadioButtonsField.cs
@@ -1,13 +1,13 @@
namespace UglyToad.PdfPig.AcroForms.Fields
{
- using Geometry;
+ using System.Collections.Generic;
using Tokens;
///
///
/// A set of radio buttons.
///
- public class AcroRadioButtonsField : AcroFieldBase
+ public class AcroRadioButtonsField : AcroNonTerminalField
{
///
/// The which define the behaviour of this button type.
@@ -20,9 +20,8 @@
///
public AcroRadioButtonsField(DictionaryToken dictionary, string fieldType, AcroButtonFieldFlags fieldFlags,
AcroFieldCommonInformation information,
- int? pageNumber,
- PdfRectangle? bounds) :
- base(dictionary, fieldType, (uint)fieldFlags, AcroFieldType.RadioButton, information, pageNumber, bounds)
+ IReadOnlyList children) :
+ base(dictionary, fieldType, (uint)fieldFlags, information, AcroFieldType.RadioButtons, children)
{
Flags = fieldFlags;
}