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; }