handle checked state of radio buttons and checkboxes

This commit is contained in:
Eliot Jones
2019-11-27 15:34:28 +00:00
parent 910e22a4e9
commit ed53773c7b
4 changed files with 108 additions and 25 deletions

View File

@@ -2,6 +2,7 @@
{ {
using System; using System;
using System.Linq; using System.Linq;
using AcroForms.Fields;
using Xunit; using Xunit;
public class AcroFormsBasicFieldsTests public class AcroFormsBasicFieldsTests
@@ -53,5 +54,38 @@
Assert.Equal(18, fields.Count); Assert.Equal(18, fields.Count);
} }
} }
[Fact]
public void GetsRadioButtonState()
{
using (var document = PdfDocument.Open(GetFilename(), ParsingOptions.LenientParsingOff))
{
var form = document.GetForm();
var radioButtons = form.Fields.OfType<AcroRadioButtonsField>().ToList();
Assert.Equal(2, radioButtons.Count);
// ReSharper disable once PossibleInvalidOperationException
var ordered = radioButtons.OrderBy(x => x.Children.Min(y => y.Bounds.Value.Left)).ToList();
var left = ordered[0];
Assert.Equal(2, left.Children.Count);
foreach (var acroFieldBase in left.Children)
{
var button = Assert.IsType<AcroRadioButtonField>(acroFieldBase);
Assert.False(button.IsSelected);
}
var right = ordered[1];
Assert.Equal(2, right.Children.Count);
var buttonOn = Assert.IsType<AcroRadioButtonField>(right.Children[0]);
Assert.True(buttonOn.IsSelected);
var buttonOff = Assert.IsType<AcroRadioButtonField>(right.Children[1]);
Assert.False(buttonOff.IsSelected);
}
}
} }
} }

View File

@@ -16,6 +16,8 @@
/// </remarks> /// </remarks>
internal class AcroForm internal class AcroForm
{ {
private readonly IReadOnlyDictionary<IndirectReference, AcroFieldBase> fieldsWithReferences;
/// <summary> /// <summary>
/// The raw PDF dictionary which is the root form object. /// The raw PDF dictionary which is the root form object.
/// </summary> /// </summary>
@@ -33,20 +35,21 @@
public bool NeedAppearances { get; } public bool NeedAppearances { get; }
/// <summary> /// <summary>
/// All root fields in this form with their corresponding references. /// All root fields in this form.
/// </summary> /// </summary>
public IReadOnlyDictionary<IndirectReference, AcroFieldBase> Fields { get; } public IReadOnlyList<AcroFieldBase> Fields { get; }
/// <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> fieldsWithReferences)
{ {
Dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); Dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
SignatureFlags = signatureFlags; SignatureFlags = signatureFlags;
NeedAppearances = needAppearances; NeedAppearances = needAppearances;
Fields = fields ?? throw new ArgumentNullException(nameof(fields)); this.fieldsWithReferences = fieldsWithReferences ?? throw new ArgumentNullException(nameof(fieldsWithReferences));
Fields = fieldsWithReferences.Values.ToList();
} }
/// <summary> /// <summary>
@@ -61,14 +64,14 @@
foreach (var field in Fields) foreach (var field in Fields)
{ {
if (field.Value.PageNumber == pageNumber) if (field.PageNumber == pageNumber)
{ {
yield return field.Value; yield return field;
} }
else if (field.Value is AcroNonTerminalField parent else if (field is AcroNonTerminalField parent
&& parent.Children.Any(x => x.PageNumber == pageNumber)) && parent.Children.Any(x => x.PageNumber == pageNumber))
{ {
yield return field.Value; yield return field;
} }
} }
} }

View File

@@ -112,7 +112,9 @@
private AcroFieldBase GetAcroField(DictionaryToken fieldDictionary, Catalog catalog, private AcroFieldBase GetAcroField(DictionaryToken fieldDictionary, Catalog catalog,
IReadOnlyList<DictionaryToken> parentDictionaries) IReadOnlyList<DictionaryToken> parentDictionaries)
{ {
fieldDictionary = CreateInheritedDictionary(fieldDictionary, parentDictionaries); var (combinedFieldDictionary, inheritsValue) = CreateInheritedDictionary(fieldDictionary, parentDictionaries);
fieldDictionary = combinedFieldDictionary;
fieldDictionary.TryGet(NameToken.Ft, tokenScanner, out NameToken fieldType); fieldDictionary.TryGet(NameToken.Ft, tokenScanner, out NameToken fieldType);
fieldDictionary.TryGet(NameToken.Ff, tokenScanner, out NumericToken fieldFlagsToken); fieldDictionary.TryGet(NameToken.Ff, tokenScanner, out NumericToken fieldFlagsToken);
@@ -194,9 +196,13 @@
} }
else else
{ {
var (isChecked, valueToken) = GetCheckedState(fieldDictionary, inheritsValue);
var field = new AcroRadioButtonField(fieldDictionary, fieldType, buttonFlags, information, var field = new AcroRadioButtonField(fieldDictionary, fieldType, buttonFlags, information,
pageNumber, pageNumber,
bounds); bounds,
valueToken,
isChecked);
result = field; result = field;
} }
@@ -210,16 +216,6 @@
} }
else else
{ {
var isChecked = false;
if (!fieldDictionary.TryGetOptionalTokenDirect(NameToken.V, tokenScanner, out NameToken valueToken))
{
valueToken = NameToken.Off;
}
else
{
isChecked = !string.Equals(valueToken.Data, NameToken.Off, StringComparison.OrdinalIgnoreCase);
}
if (children.Count > 0) if (children.Count > 0)
{ {
result = new AcroCheckboxesField(fieldDictionary, fieldType, buttonFlags, information, result = new AcroCheckboxesField(fieldDictionary, fieldType, buttonFlags, information,
@@ -227,6 +223,7 @@
} }
else else
{ {
var (isChecked, valueToken) = GetCheckedState(fieldDictionary, inheritsValue);
var field = new AcroCheckboxField(fieldDictionary, fieldType, buttonFlags, information, var field = new AcroCheckboxField(fieldDictionary, fieldType, buttonFlags, information,
valueToken, valueToken,
isChecked, isChecked,
@@ -440,14 +437,39 @@
pageNumber, pageNumber,
bounds); bounds);
} }
private (bool isChecked, NameToken stateName) GetCheckedState(DictionaryToken fieldDictionary, bool inheritsValue)
{
var isChecked = false;
if (!fieldDictionary.TryGetOptionalTokenDirect(NameToken.V, tokenScanner, out NameToken valueToken))
{
valueToken = NameToken.Off;
}
else if (inheritsValue && fieldDictionary.TryGet(NameToken.As, tokenScanner, out NameToken appearanceStateName))
{
// The parent field's V entry holds a name object corresponding to the
// appearance state of whichever child field is currently in the on state.
isChecked = appearanceStateName.Equals(valueToken);
valueToken = appearanceStateName;
}
else
{
isChecked = !string.Equals(valueToken.Data, NameToken.Off, StringComparison.OrdinalIgnoreCase);
}
return (isChecked, valueToken);
}
private static DictionaryToken CreateInheritedDictionary(DictionaryToken fieldDictionary, IReadOnlyList<DictionaryToken> parents) private static (DictionaryToken dictionary, bool inheritsValue) CreateInheritedDictionary(DictionaryToken fieldDictionary,
IReadOnlyList<DictionaryToken> parents)
{ {
if (parents.Count == 0) if (parents.Count == 0)
{ {
return fieldDictionary; return (fieldDictionary, false);
} }
var inheritsValue = false;
var inheritedDictionary = new Dictionary<NameToken, IToken>(); var inheritedDictionary = new Dictionary<NameToken, IToken>();
foreach (var parent in parents) foreach (var parent in parents)
{ {
@@ -457,16 +479,26 @@
if (InheritableFields.Contains(key)) if (InheritableFields.Contains(key))
{ {
inheritedDictionary[key] = kvp.Value; inheritedDictionary[key] = kvp.Value;
if (NameToken.V.Equals(key))
{
inheritsValue = true;
}
} }
} }
} }
foreach (var kvp in fieldDictionary.Data) foreach (var kvp in fieldDictionary.Data)
{ {
inheritedDictionary[NameToken.Create(kvp.Key)] = kvp.Value; var key = NameToken.Create(kvp.Key);
inheritedDictionary[key] = kvp.Value;
if (NameToken.V.Equals(key))
{
inheritsValue = false;
}
} }
return new DictionaryToken(inheritedDictionary); return (new DictionaryToken(inheritedDictionary), inheritsValue);
} }
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

@@ -14,6 +14,16 @@
/// </summary> /// </summary>
public AcroButtonFieldFlags Flags { get; } public AcroButtonFieldFlags Flags { get; }
/// <summary>
/// The current value of this radio button.
/// </summary>
public NameToken CurrentValue { get; }
/// <summary>
/// Whether the radio button is currently on/active.
/// </summary>
public bool IsSelected { get; }
/// <inheritdoc /> /// <inheritdoc />
/// <summary> /// <summary>
/// Create a new <see cref="AcroRadioButtonField"/>. /// Create a new <see cref="AcroRadioButtonField"/>.
@@ -21,10 +31,14 @@
public AcroRadioButtonField(DictionaryToken dictionary, string fieldType, AcroButtonFieldFlags fieldFlags, public AcroRadioButtonField(DictionaryToken dictionary, string fieldType, AcroButtonFieldFlags fieldFlags,
AcroFieldCommonInformation information, AcroFieldCommonInformation information,
int? pageNumber, int? pageNumber,
PdfRectangle? bounds) : PdfRectangle? bounds,
NameToken currentValue,
bool isSelected) :
base(dictionary, fieldType, (uint)fieldFlags, AcroFieldType.RadioButton, information, pageNumber, bounds) base(dictionary, fieldType, (uint)fieldFlags, AcroFieldType.RadioButton, information, pageNumber, bounds)
{ {
Flags = fieldFlags; Flags = fieldFlags;
CurrentValue = currentValue;
IsSelected = isSelected;
} }
} }
} }