mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-09-21 04:17:57 +08:00
handle checked state of radio buttons and checkboxes
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user