mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-09-21 04:17:57 +08:00
#24 start adding classes for the acroform api
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
namespace UglyToad.PdfPig.Tests.Integration
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class AcroFormsBasicFieldsTests
|
||||||
|
{
|
||||||
|
private static string GetFilename()
|
||||||
|
{
|
||||||
|
return IntegrationHelpers.GetDocumentPath("AcroFormsBasicFields");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetFormNotNull()
|
||||||
|
{
|
||||||
|
using (var document = PdfDocument.Open(GetFilename(), new ParsingOptions { UseLenientParsing = false }))
|
||||||
|
{
|
||||||
|
var form = document.GetForm();
|
||||||
|
Assert.NotNull(form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetFormDisposedThrows()
|
||||||
|
{
|
||||||
|
var document = PdfDocument.Open(GetFilename());
|
||||||
|
|
||||||
|
document.Dispose();
|
||||||
|
|
||||||
|
Action action = () => document.GetForm();
|
||||||
|
|
||||||
|
Assert.Throws<ObjectDisposedException>(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
49
src/UglyToad.PdfPig/AcroForms/AcroForm.cs
Normal file
49
src/UglyToad.PdfPig/AcroForms/AcroForm.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
namespace UglyToad.PdfPig.AcroForms
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Tokens;
|
||||||
|
using Util.JetBrains.Annotations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A collection of interactive fields for gathering data from a user through dropdowns, textboxes, checkboxes, etc.
|
||||||
|
/// Each <see cref="PdfDocument"/> with form functionality contains a single <see cref="AcroForm"/> spread across one or more pages.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The name AcroForm distinguishes this from the other form type called form XObjects which act as templates for repeated sections of content.
|
||||||
|
/// </remarks>
|
||||||
|
internal class AcroForm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The raw PDF dictionary which is the root form object.
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public DictionaryToken Dictionary { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Document-level characteristics related to signature fields.
|
||||||
|
/// </summary>
|
||||||
|
public SignatureFlags SignatureFlags { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether all widget annotations need appearance dictionaries and streams.
|
||||||
|
/// </summary>
|
||||||
|
public bool NeedAppearances { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="AcroForm"/>.
|
||||||
|
/// </summary>
|
||||||
|
public AcroForm(DictionaryToken dictionary, SignatureFlags signatureFlags, bool needAppearances)
|
||||||
|
{
|
||||||
|
Dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
|
||||||
|
SignatureFlags = signatureFlags;
|
||||||
|
NeedAppearances = needAppearances;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Dictionary.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
90
src/UglyToad.PdfPig/AcroForms/AcroFormFactory.cs
Normal file
90
src/UglyToad.PdfPig/AcroForms/AcroFormFactory.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
namespace UglyToad.PdfPig.AcroForms
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Content;
|
||||||
|
using Exceptions;
|
||||||
|
using Filters;
|
||||||
|
using Parser.Parts;
|
||||||
|
using Tokenization.Scanner;
|
||||||
|
using Tokens;
|
||||||
|
using Util;
|
||||||
|
using Util.JetBrains.Annotations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the <see cref="AcroForm"/> from the document, if available.
|
||||||
|
/// </summary>
|
||||||
|
internal class AcroFormFactory
|
||||||
|
{
|
||||||
|
private readonly IPdfTokenScanner tokenScanner;
|
||||||
|
private readonly IFilterProvider filterProvider;
|
||||||
|
|
||||||
|
public AcroFormFactory(IPdfTokenScanner tokenScanner, IFilterProvider filterProvider)
|
||||||
|
{
|
||||||
|
this.tokenScanner = tokenScanner ?? throw new ArgumentNullException(nameof(tokenScanner));
|
||||||
|
this.filterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve the <see cref="AcroForm"/> from the document, if applicable.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The <see cref="AcroForm"/> if the document contains one.</returns>
|
||||||
|
[CanBeNull]
|
||||||
|
public AcroForm GetAcroForm(Catalog catalog)
|
||||||
|
{
|
||||||
|
if (!catalog.CatalogDictionary.TryGet(NameToken.AcroForm, out var acroRawToken) || !DirectObjectFinder.TryGet(acroRawToken, tokenScanner, out DictionaryToken acroDictionary))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var signatureFlags = (SignatureFlags)0;
|
||||||
|
if (acroDictionary.TryGetOptionalTokenDirect(NameToken.SigFlags, tokenScanner, out NumericToken signatureToken))
|
||||||
|
{
|
||||||
|
signatureFlags = (SignatureFlags)signatureToken.Int;
|
||||||
|
}
|
||||||
|
|
||||||
|
var needAppearances = false;
|
||||||
|
if (acroDictionary.TryGetOptionalTokenDirect(NameToken.NeedAppearances, tokenScanner, out BooleanToken appearancesToken))
|
||||||
|
{
|
||||||
|
needAppearances = appearancesToken.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
var calculationOrder = default(ArrayToken);
|
||||||
|
acroDictionary.TryGetOptionalTokenDirect(NameToken.Co, tokenScanner, out calculationOrder);
|
||||||
|
|
||||||
|
var formResources = default(DictionaryToken);
|
||||||
|
acroDictionary.TryGetOptionalTokenDirect(NameToken.Dr, tokenScanner, out formResources);
|
||||||
|
|
||||||
|
var da = default(string);
|
||||||
|
if (acroDictionary.TryGetOptionalTokenDirect(NameToken.Da, tokenScanner, out StringToken daToken))
|
||||||
|
{
|
||||||
|
da = daToken.Data;
|
||||||
|
}
|
||||||
|
else if (acroDictionary.TryGetOptionalTokenDirect(NameToken.Da, tokenScanner, out HexToken daHexToken))
|
||||||
|
{
|
||||||
|
da = daHexToken.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
var q = default(int?);
|
||||||
|
if (acroDictionary.TryGetOptionalTokenDirect(NameToken.Q, tokenScanner, out NumericToken qToken))
|
||||||
|
{
|
||||||
|
q = qToken.Int;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fieldsToken = acroDictionary.Data[NameToken.Fields.Data];
|
||||||
|
|
||||||
|
if (!DirectObjectFinder.TryGet(fieldsToken, tokenScanner, out ArrayToken fieldsArray))
|
||||||
|
{
|
||||||
|
throw new PdfDocumentFormatException($"Could not retrieve the fields array for an AcroForm: {acroDictionary}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var fieldToken in fieldsArray.Data)
|
||||||
|
{
|
||||||
|
var fieldDictionary = DirectObjectFinder.Get<DictionaryToken>(fieldToken, tokenScanner);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AcroForm(acroDictionary, signatureFlags, needAppearances);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/UglyToad.PdfPig/AcroForms/SignatureFlags.cs
Normal file
21
src/UglyToad.PdfPig/AcroForms/SignatureFlags.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace UglyToad.PdfPig.AcroForms
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies document level characteristics for any signature fields in the document's <see cref="AcroForm"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
internal enum SignatureFlags
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The document contains at least one signature field.
|
||||||
|
/// </summary>
|
||||||
|
SignaturesExist = 1 << 0,
|
||||||
|
/// <summary>
|
||||||
|
/// The document contains signatures which may be invalidated if the file is saved
|
||||||
|
/// in a way which alters its previous content rather than simply appending new content.
|
||||||
|
/// </summary>
|
||||||
|
AppendOnly = 1 << 1
|
||||||
|
}
|
||||||
|
}
|
@@ -30,7 +30,7 @@
|
|||||||
{
|
{
|
||||||
var type = dictionary.GetNameOrDefault(NameToken.Type);
|
var type = dictionary.GetNameOrDefault(NameToken.Type);
|
||||||
|
|
||||||
if (!type.Equals(NameToken.Font))
|
if (type != null && !type.Equals(NameToken.Font))
|
||||||
{
|
{
|
||||||
var message = "The font dictionary did not have type 'Font'. " + dictionary;
|
var message = "The font dictionary did not have type 'Font'. " + dictionary;
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using AcroForms;
|
||||||
using Content;
|
using Content;
|
||||||
using CrossReference;
|
using CrossReference;
|
||||||
using FileStructure;
|
using FileStructure;
|
||||||
@@ -127,9 +128,11 @@
|
|||||||
var catalog = catalogFactory.Create(pdfScanner, rootDictionary);
|
var catalog = catalogFactory.Create(pdfScanner, rootDictionary);
|
||||||
|
|
||||||
var caching = new ParsingCachingProviders(bruteForceSearcher, resourceContainer);
|
var caching = new ParsingCachingProviders(bruteForceSearcher, resourceContainer);
|
||||||
|
|
||||||
|
var acroFormFactory = new AcroFormFactory(pdfScanner, filterProvider);
|
||||||
|
|
||||||
return new PdfDocument(log, inputBytes, version, crossReferenceTable, isLenientParsing, caching, pageFactory, catalog, information,
|
return new PdfDocument(log, inputBytes, version, crossReferenceTable, isLenientParsing, caching, pageFactory, catalog, information,
|
||||||
pdfScanner);
|
pdfScanner, acroFormFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DictionaryToken ParseTrailer(CrossReferenceTable crossReferenceTable, bool isLenientParsing, IPdfTokenScanner pdfTokenScanner)
|
private static DictionaryToken ParseTrailer(CrossReferenceTable crossReferenceTable, bool isLenientParsing, IPdfTokenScanner pdfTokenScanner)
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using AcroForms;
|
||||||
using Content;
|
using Content;
|
||||||
using CrossReference;
|
using CrossReference;
|
||||||
using IO;
|
using IO;
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
public class PdfDocument : IDisposable
|
public class PdfDocument : IDisposable
|
||||||
{
|
{
|
||||||
private bool isDisposed;
|
private bool isDisposed;
|
||||||
|
private readonly Lazy<AcroForm> documentForm;
|
||||||
|
|
||||||
private readonly bool isLenientParsing;
|
private readonly bool isLenientParsing;
|
||||||
|
|
||||||
@@ -65,7 +67,8 @@
|
|||||||
ParsingCachingProviders cachingProviders,
|
ParsingCachingProviders cachingProviders,
|
||||||
IPageFactory pageFactory,
|
IPageFactory pageFactory,
|
||||||
Catalog catalog,
|
Catalog catalog,
|
||||||
DocumentInformation information, IPdfTokenScanner pdfScanner)
|
DocumentInformation information, IPdfTokenScanner pdfScanner,
|
||||||
|
AcroFormFactory acroFormFactory)
|
||||||
{
|
{
|
||||||
this.log = log;
|
this.log = log;
|
||||||
this.inputBytes = inputBytes;
|
this.inputBytes = inputBytes;
|
||||||
@@ -76,6 +79,7 @@
|
|||||||
Information = information ?? throw new ArgumentNullException(nameof(information));
|
Information = information ?? throw new ArgumentNullException(nameof(information));
|
||||||
pages = new Pages(log, catalog, pageFactory, isLenientParsing, pdfScanner);
|
pages = new Pages(log, catalog, pageFactory, isLenientParsing, pdfScanner);
|
||||||
Structure = new Structure(catalog, crossReferenceTable, pdfScanner);
|
Structure = new Structure(catalog, crossReferenceTable, pdfScanner);
|
||||||
|
documentForm = new Lazy<AcroForm>(() => acroFormFactory.GetAcroForm(catalog));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -122,6 +126,20 @@
|
|||||||
|
|
||||||
return pages.GetPage(pageNumber);
|
return pages.GetPage(pageNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the form if this document contains one.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An <see cref="AcroForm"/> from the document or <see langword="null"/> if not present.</returns>
|
||||||
|
internal AcroForm GetForm()
|
||||||
|
{
|
||||||
|
if (isDisposed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException("Cannot access the form after the document is disposed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return documentForm.Value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@@ -4,6 +4,8 @@
|
|||||||
using Exceptions;
|
using Exceptions;
|
||||||
using Geometry;
|
using Geometry;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Parser.Parts;
|
||||||
|
using Tokenization.Scanner;
|
||||||
using Tokens;
|
using Tokens;
|
||||||
|
|
||||||
internal static class DictionaryTokenExtensions
|
internal static class DictionaryTokenExtensions
|
||||||
@@ -68,6 +70,18 @@
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryGetOptionalTokenDirect<T>(this DictionaryToken token, NameToken name, IPdfTokenScanner scanner, out T result) where T : IToken
|
||||||
|
{
|
||||||
|
result = default(T);
|
||||||
|
if (token.TryGet(name, out var appearancesToken) && DirectObjectFinder.TryGet(appearancesToken, scanner, out T innerResult))
|
||||||
|
{
|
||||||
|
result = innerResult;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class ArrayTokenExtensions
|
internal static class ArrayTokenExtensions
|
||||||
|
Reference in New Issue
Block a user