wrap checkboxes and radiobuttons in their own form field types with access to the child collections

This commit is contained in:
Eliot Jones 2019-11-26 16:33:24 +00:00
parent 677d2b5e8f
commit 910e22a4e9
9 changed files with 180 additions and 45 deletions

View File

@ -43,16 +43,15 @@
} }
} }
//[Fact] [Fact]
//public void GetFormFieldsByPage() public void GetFormFieldsByPage()
//{ {
// using (var document = PdfDocument.Open(GetFilename(), ParsingOptions.LenientParsingOff)) using (var document = PdfDocument.Open(GetFilename(), ParsingOptions.LenientParsingOff))
// { {
// var form = document.GetForm(); var form = document.GetForm();
// var fields = form.GetFieldsForPage(1).ToList(); var fields = form.GetFieldsForPage(1).ToList();
// var page = document.GetPage(1).ExperimentalAccess.GetAnnotations().ToList(); Assert.Equal(18, fields.Count);
// Assert.Equal(16, fields.Count); }
// } }
//}
} }
} }

View File

@ -41,6 +41,7 @@
{ {
"UglyToad.PdfPig.AcroForms.Fields.AcroButtonFieldFlags", "UglyToad.PdfPig.AcroForms.Fields.AcroButtonFieldFlags",
"UglyToad.PdfPig.AcroForms.Fields.AcroCheckboxField", "UglyToad.PdfPig.AcroForms.Fields.AcroCheckboxField",
"UglyToad.PdfPig.AcroForms.Fields.AcroCheckboxesField",
"UglyToad.PdfPig.AcroForms.Fields.AcroChoiceFieldFlags", "UglyToad.PdfPig.AcroForms.Fields.AcroChoiceFieldFlags",
"UglyToad.PdfPig.AcroForms.Fields.AcroChoiceOption", "UglyToad.PdfPig.AcroForms.Fields.AcroChoiceOption",
"UglyToad.PdfPig.AcroForms.Fields.AcroComboBoxField", "UglyToad.PdfPig.AcroForms.Fields.AcroComboBoxField",
@ -50,6 +51,7 @@
"UglyToad.PdfPig.AcroForms.Fields.AcroListBoxField", "UglyToad.PdfPig.AcroForms.Fields.AcroListBoxField",
"UglyToad.PdfPig.AcroForms.Fields.AcroNonTerminalField", "UglyToad.PdfPig.AcroForms.Fields.AcroNonTerminalField",
"UglyToad.PdfPig.AcroForms.Fields.AcroPushButtonField", "UglyToad.PdfPig.AcroForms.Fields.AcroPushButtonField",
"UglyToad.PdfPig.AcroForms.Fields.AcroRadioButtonField",
"UglyToad.PdfPig.AcroForms.Fields.AcroRadioButtonsField", "UglyToad.PdfPig.AcroForms.Fields.AcroRadioButtonsField",
"UglyToad.PdfPig.AcroForms.Fields.AcroSignatureField", "UglyToad.PdfPig.AcroForms.Fields.AcroSignatureField",
"UglyToad.PdfPig.AcroForms.Fields.AcroTextField", "UglyToad.PdfPig.AcroForms.Fields.AcroTextField",

View File

@ -2,6 +2,7 @@
{ {
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Fields; using Fields;
using Tokens; using Tokens;
using Util.JetBrains.Annotations; using Util.JetBrains.Annotations;
@ -39,7 +40,7 @@
/// <summary> /// <summary>
/// Create a new <see cref="AcroForm"/>. /// Create a new <see cref="AcroForm"/>.
/// </summary> /// </summary>
public AcroForm(DictionaryToken dictionary, SignatureFlags signatureFlags, bool needAppearances, public AcroForm(DictionaryToken dictionary, SignatureFlags signatureFlags, bool needAppearances,
IReadOnlyDictionary<IndirectReference, AcroFieldBase> fields) IReadOnlyDictionary<IndirectReference, AcroFieldBase> fields)
{ {
Dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); Dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
@ -64,6 +65,11 @@
{ {
yield return field.Value; yield return field.Value;
} }
else if (field.Value is AcroNonTerminalField parent
&& parent.Children.Any(x => x.PageNumber == pageNumber))
{
yield return field.Value;
}
} }
} }

View File

@ -19,6 +19,15 @@
/// </summary> /// </summary>
internal class AcroFormFactory internal class AcroFormFactory
{ {
private static readonly HashSet<NameToken> InheritableFields = new HashSet<NameToken>
{
NameToken.Ft,
NameToken.Ff,
NameToken.V,
NameToken.Dv,
NameToken.Aa
};
private readonly IPdfTokenScanner tokenScanner; private readonly IPdfTokenScanner tokenScanner;
private readonly IFilterProvider filterProvider; private readonly IFilterProvider filterProvider;
@ -92,7 +101,7 @@
var fieldDictionary = DirectObjectFinder.Get<DictionaryToken>(fieldToken, tokenScanner); var fieldDictionary = DirectObjectFinder.Get<DictionaryToken>(fieldToken, tokenScanner);
var field = GetAcroField(fieldDictionary, catalog); var field = GetAcroField(fieldDictionary, catalog, new List<DictionaryToken>(0));
fields[fieldReferenceToken.Data] = field; fields[fieldReferenceToken.Data] = field;
} }
@ -100,12 +109,15 @@
return new AcroForm(acroDictionary, signatureFlags, needAppearances, fields); return new AcroForm(acroDictionary, signatureFlags, needAppearances, fields);
} }
private AcroFieldBase GetAcroField(DictionaryToken fieldDictionary, Catalog catalog) private AcroFieldBase GetAcroField(DictionaryToken fieldDictionary, Catalog catalog,
IReadOnlyList<DictionaryToken> parentDictionaries)
{ {
fieldDictionary.TryGet(NameToken.Ft, out NameToken fieldType); fieldDictionary = CreateInheritedDictionary(fieldDictionary, parentDictionaries);
fieldDictionary.TryGet(NameToken.Ff, out NumericToken fieldFlagsToken);
var kids = new List<DictionaryToken>(); 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)) if (fieldDictionary.TryGetOptionalTokenDirect(NameToken.Kids, tokenScanner, out ArrayToken kidsToken))
{ {
@ -120,7 +132,8 @@
if (kidObject.Data is DictionaryToken kidDictionaryToken) if (kidObject.Data is DictionaryToken kidDictionaryToken)
{ {
kids.Add(kidDictionaryToken); var hasParent = kidDictionaryToken.TryGet(NameToken.Parent, out IndirectReferenceToken _);
kids.Add((hasParent, kidDictionaryToken));
} }
else else
{ {
@ -147,19 +160,26 @@
bounds = rectArray.ToRectangle(); bounds = rectArray.ToRectangle();
} }
var newParentDictionaries = new List<DictionaryToken>(parentDictionaries) {fieldDictionary};
var children = new List<AcroFieldBase>(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); var fieldFlags = (uint) (fieldFlagsToken?.Long ?? 0);
AcroFieldBase result; AcroFieldBase result;
if (fieldType == null) if (fieldType == null)
{ {
var children = new List<AcroFieldBase>(); result = new AcroNonTerminalField(fieldDictionary, "Non-Terminal Field", fieldFlags, information, AcroFieldType.Unknown, children);
foreach (var kid in kids)
{
var kidField = GetAcroField(kid, catalog);
children.Add(kidField);
}
result = new AcroNonTerminalField(fieldDictionary, "Non-Terminal Field", fieldFlags, information, children);
} }
else if (fieldType == NameToken.Btn) else if (fieldType == NameToken.Btn)
{ {
@ -167,10 +187,19 @@
if (buttonFlags.HasFlag(AcroButtonFieldFlags.Radio)) if (buttonFlags.HasFlag(AcroButtonFieldFlags.Radio))
{ {
var field = new AcroRadioButtonsField(fieldDictionary, fieldType, buttonFlags, information, if (children.Count > 0)
pageNumber, {
bounds); result = new AcroRadioButtonsField(fieldDictionary, fieldType, buttonFlags, information,
result = field; children);
}
else
{
var field = new AcroRadioButtonField(fieldDictionary, fieldType, buttonFlags, information,
pageNumber,
bounds);
result = field;
}
} }
else if (buttonFlags.HasFlag(AcroButtonFieldFlags.PushButton)) else if (buttonFlags.HasFlag(AcroButtonFieldFlags.PushButton))
{ {
@ -191,13 +220,21 @@
isChecked = !string.Equals(valueToken.Data, NameToken.Off, StringComparison.OrdinalIgnoreCase); isChecked = !string.Equals(valueToken.Data, NameToken.Off, StringComparison.OrdinalIgnoreCase);
} }
var field = new AcroCheckboxField(fieldDictionary, fieldType, buttonFlags, information, if (children.Count > 0)
valueToken, {
isChecked, result = new AcroCheckboxesField(fieldDictionary, fieldType, buttonFlags, information,
pageNumber, children);
bounds); }
else
{
var field = new AcroCheckboxField(fieldDictionary, fieldType, buttonFlags, information,
valueToken,
isChecked,
pageNumber,
bounds);
result = field; result = field;
}
} }
} }
else if (fieldType == NameToken.Tx) else if (fieldType == NameToken.Tx)
@ -403,6 +440,34 @@
pageNumber, pageNumber,
bounds); bounds);
} }
private static DictionaryToken CreateInheritedDictionary(DictionaryToken fieldDictionary, IReadOnlyList<DictionaryToken> parents)
{
if (parents.Count == 0)
{
return fieldDictionary;
}
var inheritedDictionary = new Dictionary<NameToken, IToken>();
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<string> selectedOptionNames, IReadOnlyList<int> selectedOptionIndices, int index, string name) private static bool IsChoiceSelected(IReadOnlyList<string> selectedOptionNames, IReadOnlyList<int> selectedOptionIndices, int index, string name)
{ {

View File

@ -0,0 +1,24 @@
namespace UglyToad.PdfPig.AcroForms.Fields
{
using System.Collections.Generic;
using Tokens;
/// <inheritdoc />
/// <summary>
/// A set of related checkboxes.
/// </summary>
public class AcroCheckboxesField : AcroNonTerminalField
{
/// <inheritdoc />
/// <summary>
/// Create a new <see cref="AcroCheckboxesField"/>.
/// </summary>
internal AcroCheckboxesField(DictionaryToken dictionary, string fieldType, AcroButtonFieldFlags fieldFlags,
AcroFieldCommonInformation information,
IReadOnlyList<AcroFieldBase> children) :
base(dictionary, fieldType, (uint)fieldFlags, information,
AcroFieldType.Checkboxes, children)
{
}
}
}

View File

@ -10,12 +10,20 @@
/// </summary> /// </summary>
PushButton, PushButton,
/// <summary> /// <summary>
/// A set of checkboxes.
/// </summary>
Checkboxes,
/// <summary>
/// A checkbox which toggles between on and off states. /// A checkbox which toggles between on and off states.
/// </summary> /// </summary>
Checkbox, Checkbox,
/// <summary> /// <summary>
/// A set of radio buttons. /// A set of radio buttons.
/// </summary> /// </summary>
RadioButtons,
/// <summary>
/// A single radio button, as part of a set or on its own.
/// </summary>
RadioButton, RadioButton,
/// <summary> /// <summary>
/// A textbox allowing user input through the keyboard. /// A textbox allowing user input through the keyboard.
@ -34,8 +42,8 @@
/// </summary> /// </summary>
Signature, Signature,
/// <summary> /// <summary>
/// A field which acts as a container for other fields. /// The field type wasn't specified.
/// </summary> /// </summary>
NonTerminal Unknown
} }
} }

View File

@ -19,9 +19,11 @@
/// <summary> /// <summary>
/// Create a new <see cref="AcroNonTerminalField"/>. /// Create a new <see cref="AcroNonTerminalField"/>.
/// </summary> /// </summary>
internal AcroNonTerminalField(DictionaryToken dictionary, string fieldType, uint fieldFlags, AcroFieldCommonInformation information, internal AcroNonTerminalField(DictionaryToken dictionary, string fieldType, uint fieldFlags,
AcroFieldCommonInformation information,
AcroFieldType acroFieldType,
IReadOnlyList<AcroFieldBase> children) : IReadOnlyList<AcroFieldBase> children) :
base(dictionary, fieldType, fieldFlags, AcroFieldType.NonTerminal, information, base(dictionary, fieldType, fieldFlags, acroFieldType, information,
null, null) null, null)
{ {
Children = children ?? throw new ArgumentNullException(nameof(children)); Children = children ?? throw new ArgumentNullException(nameof(children));

View File

@ -0,0 +1,30 @@
namespace UglyToad.PdfPig.AcroForms.Fields
{
using Geometry;
using Tokens;
/// <inheritdoc />
/// <summary>
/// A single radio button.
/// </summary>
public class AcroRadioButtonField : AcroFieldBase
{
/// <summary>
/// The <see cref="AcroButtonFieldFlags"/> which define the behaviour of this button type.
/// </summary>
public AcroButtonFieldFlags Flags { get; }
/// <inheritdoc />
/// <summary>
/// Create a new <see cref="AcroRadioButtonField"/>.
/// </summary>
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;
}
}
}

View File

@ -1,13 +1,13 @@
namespace UglyToad.PdfPig.AcroForms.Fields namespace UglyToad.PdfPig.AcroForms.Fields
{ {
using Geometry; using System.Collections.Generic;
using Tokens; using Tokens;
/// <inheritdoc /> /// <inheritdoc />
/// <summary> /// <summary>
/// A set of radio buttons. /// A set of radio buttons.
/// </summary> /// </summary>
public class AcroRadioButtonsField : AcroFieldBase public class AcroRadioButtonsField : AcroNonTerminalField
{ {
/// <summary> /// <summary>
/// The <see cref="AcroButtonFieldFlags"/> which define the behaviour of this button type. /// The <see cref="AcroButtonFieldFlags"/> which define the behaviour of this button type.
@ -20,9 +20,8 @@
/// </summary> /// </summary>
public AcroRadioButtonsField(DictionaryToken dictionary, string fieldType, AcroButtonFieldFlags fieldFlags, public AcroRadioButtonsField(DictionaryToken dictionary, string fieldType, AcroButtonFieldFlags fieldFlags,
AcroFieldCommonInformation information, AcroFieldCommonInformation information,
int? pageNumber, IReadOnlyList<AcroFieldBase> children) :
PdfRectangle? bounds) : base(dictionary, fieldType, (uint)fieldFlags, information, AcroFieldType.RadioButtons, children)
base(dictionary, fieldType, (uint)fieldFlags, AcroFieldType.RadioButton, information, pageNumber, bounds)
{ {
Flags = fieldFlags; Flags = fieldFlags;
} }