mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-06-28 15:30:17 +08:00
wrap checkboxes and radiobuttons in their own form field types with access to the child collections
This commit is contained in:
parent
677d2b5e8f
commit
910e22a4e9
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
24
src/UglyToad.PdfPig/AcroForms/Fields/AcroCheckboxesField.cs
Normal file
24
src/UglyToad.PdfPig/AcroForms/Fields/AcroCheckboxesField.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
30
src/UglyToad.PdfPig/AcroForms/Fields/AcroRadioButtonField.cs
Normal file
30
src/UglyToad.PdfPig/AcroForms/Fields/AcroRadioButtonField.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user