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.Linq;
using AcroForms.Fields;
using Xunit;
public class AcroFormsBasicFieldsTests
@@ -53,5 +54,38 @@
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>
internal class AcroForm
{
private readonly IReadOnlyDictionary<IndirectReference, AcroFieldBase> fieldsWithReferences;
/// <summary>
/// The raw PDF dictionary which is the root form object.
/// </summary>
@@ -33,20 +35,21 @@
public bool NeedAppearances { get; }
/// <summary>
/// All root fields in this form with their corresponding references.
/// All root fields in this form.
/// </summary>
public IReadOnlyDictionary<IndirectReference, AcroFieldBase> Fields { get; }
public IReadOnlyList<AcroFieldBase> Fields { get; }
/// <summary>
/// Create a new <see cref="AcroForm"/>.
/// </summary>
public AcroForm(DictionaryToken dictionary, SignatureFlags signatureFlags, bool needAppearances,
IReadOnlyDictionary<IndirectReference, AcroFieldBase> fields)
IReadOnlyDictionary<IndirectReference, AcroFieldBase> fieldsWithReferences)
{
Dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
SignatureFlags = signatureFlags;
NeedAppearances = needAppearances;
Fields = fields ?? throw new ArgumentNullException(nameof(fields));
this.fieldsWithReferences = fieldsWithReferences ?? throw new ArgumentNullException(nameof(fieldsWithReferences));
Fields = fieldsWithReferences.Values.ToList();
}
/// <summary>
@@ -61,14 +64,14 @@
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))
{
yield return field.Value;
yield return field;
}
}
}

View File

@@ -112,7 +112,9 @@
private AcroFieldBase GetAcroField(DictionaryToken fieldDictionary, Catalog catalog,
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.Ff, tokenScanner, out NumericToken fieldFlagsToken);
@@ -194,9 +196,13 @@
}
else
{
var (isChecked, valueToken) = GetCheckedState(fieldDictionary, inheritsValue);
var field = new AcroRadioButtonField(fieldDictionary, fieldType, buttonFlags, information,
pageNumber,
bounds);
bounds,
valueToken,
isChecked);
result = field;
}
@@ -210,16 +216,6 @@
}
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)
{
result = new AcroCheckboxesField(fieldDictionary, fieldType, buttonFlags, information,
@@ -227,6 +223,7 @@
}
else
{
var (isChecked, valueToken) = GetCheckedState(fieldDictionary, inheritsValue);
var field = new AcroCheckboxField(fieldDictionary, fieldType, buttonFlags, information,
valueToken,
isChecked,
@@ -441,13 +438,38 @@
bounds);
}
private static DictionaryToken CreateInheritedDictionary(DictionaryToken fieldDictionary, IReadOnlyList<DictionaryToken> parents)
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 dictionary, bool inheritsValue) CreateInheritedDictionary(DictionaryToken fieldDictionary,
IReadOnlyList<DictionaryToken> parents)
{
if (parents.Count == 0)
{
return fieldDictionary;
return (fieldDictionary, false);
}
var inheritsValue = false;
var inheritedDictionary = new Dictionary<NameToken, IToken>();
foreach (var parent in parents)
{
@@ -457,16 +479,26 @@
if (InheritableFields.Contains(key))
{
inheritedDictionary[key] = kvp.Value;
if (NameToken.V.Equals(key))
{
inheritsValue = true;
}
}
}
}
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)

View File

@@ -14,6 +14,16 @@
/// </summary>
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 />
/// <summary>
/// Create a new <see cref="AcroRadioButtonField"/>.
@@ -21,10 +31,14 @@
public AcroRadioButtonField(DictionaryToken dictionary, string fieldType, AcroButtonFieldFlags fieldFlags,
AcroFieldCommonInformation information,
int? pageNumber,
PdfRectangle? bounds) :
PdfRectangle? bounds,
NameToken currentValue,
bool isSelected) :
base(dictionary, fieldType, (uint)fieldFlags, AcroFieldType.RadioButton, information, pageNumber, bounds)
{
Flags = fieldFlags;
CurrentValue = currentValue;
IsSelected = isSelected;
}
}
}