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]
//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);
}
}
}
}

View File

@ -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",

View File

@ -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 @@
/// <summary>
/// Create a new <see cref="AcroForm"/>.
/// </summary>
public AcroForm(DictionaryToken dictionary, SignatureFlags signatureFlags, bool needAppearances,
public AcroForm(DictionaryToken dictionary, SignatureFlags signatureFlags, bool needAppearances,
IReadOnlyDictionary<IndirectReference, AcroFieldBase> 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;
}
}
}

View File

@ -19,6 +19,15 @@
/// </summary>
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 IFilterProvider filterProvider;
@ -92,7 +101,7 @@
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;
}
@ -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<DictionaryToken> parentDictionaries)
{
fieldDictionary.TryGet(NameToken.Ft, out NameToken fieldType);
fieldDictionary.TryGet(NameToken.Ff, out NumericToken fieldFlagsToken);
fieldDictionary = CreateInheritedDictionary(fieldDictionary, parentDictionaries);
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))
{
@ -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<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);
AcroFieldBase result;
if (fieldType == null)
{
var children = new List<AcroFieldBase>();
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<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)
{

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

View File

@ -19,9 +19,11 @@
/// <summary>
/// Create a new <see cref="AcroNonTerminalField"/>.
/// </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) :
base(dictionary, fieldType, fieldFlags, AcroFieldType.NonTerminal, information,
base(dictionary, fieldType, fieldFlags, acroFieldType, information,
null, null)
{
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
{
using Geometry;
using System.Collections.Generic;
using Tokens;
/// <inheritdoc />
/// <summary>
/// A set of radio buttons.
/// </summary>
public class AcroRadioButtonsField : AcroFieldBase
public class AcroRadioButtonsField : AcroNonTerminalField
{
/// <summary>
/// The <see cref="AcroButtonFieldFlags"/> which define the behaviour of this button type.
@ -20,9 +20,8 @@
/// </summary>
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<AcroFieldBase> children) :
base(dictionary, fieldType, (uint)fieldFlags, information, AcroFieldType.RadioButtons, children)
{
Flags = fieldFlags;
}