Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -175,18 +175,18 @@ internal async Task AddSignatureComponentsAsync() // #US321 TODO Use appropriate
_contentsPlaceholder = new(2 * signatureSize + 2);
_byteRangePlaceholder = new(ByteRangePlaceholderLength);

var catalog = Document.Catalog;
var acroForm = catalog.GetOrCreateAcroForm();

var signatureDictionary = GetSignatureDictionary(_contentsPlaceholder, _byteRangePlaceholder);
var signatureField = GetSignatureField(signatureDictionary);
var signatureField = GetSignatureField(signatureDictionary, ChooseFieldName(acroForm));

var page = Document.Pages[Options.PageIndex];
var annotations = page.Elements.GetArray(PdfPage.Keys.Annots);
if (annotations == null)
page.Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField));
else
annotations.Elements.Add(signatureField);

var catalog = Document.Catalog;
var acroForm = catalog.GetOrCreateAcroForm();

if (!acroForm.Elements.ContainsKey(PdfForm.Keys.SigFlags))
acroForm.Elements.Add(PdfForm.Keys.SigFlags, new PdfInteger(3, true));
Expand All @@ -200,7 +200,40 @@ internal async Task AddSignatureComponentsAsync() // #US321 TODO Use appropriate
acroForm.Fields.Elements.Add(signatureField);
}

PdfFormSignatureField GetSignatureField(PdfSignature signatureDic) // #US321 TODO Use appropriate classes.
/// <summary>
/// Chooses the partial field name of the signature field to be created, e.g. “Signature1”, “Signature2”, …
/// Interactive form fields must have unique fully qualified names. If a document is signed more than once,
/// reusing a name leads to two fields with the same name, which validators either merge or reject.
/// Therefore, the first unused name is taken.
/// </summary>
/// <param name="acroForm">The interactive form the new field is added to.</param>
static string ChooseFieldName(PdfForm acroForm)
{
// Only the names of the fields at the root of the form are relevant here, because the new field
// is added there. The name of a field below the root is relative to the name of its parent field
// and therefore cannot collide with a name at the root.
var usedNames = new HashSet<string>(StringComparer.Ordinal);
var fields = acroForm.Elements.GetArray(PdfForm.Keys.Fields);
if (fields != null)
{
int count = fields.Elements.Count;
for (int idx = 0; idx < count; idx++)
{
var name = fields.Elements.GetDictionary(idx)?.Elements.GetString(PdfFormField.Keys.T);
if (!String.IsNullOrEmpty(name))
usedNames.Add(name!);
}
}

for (int idx = 1; ; idx++)
{
var name = Invariant($"Signature{idx}");
if (!usedNames.Contains(name))
return name;
}
}

PdfFormSignatureField GetSignatureField(PdfSignature signatureDic, string fieldName) // #US321 TODO Use appropriate classes.
{
var signatureField = new PdfFormSignatureField(Document);

Expand All @@ -209,7 +242,7 @@ PdfFormSignatureField GetSignatureField(PdfSignature signatureDic) // #US321 TOD
// #AcroForms
// Annotation keys.
signatureField.Elements.Add(PdfFormField.Keys.FT, new PdfName(PdfFormFieldType.Signature));
signatureField.Elements.Add(PdfFormField.Keys.T, new PdfString("Signature1")); // TODO If already exists, will it cause error? implement a name chooser if yes.
signatureField.Elements.Add(PdfFormField.Keys.T, new PdfString(fieldName));
signatureField.Elements.Add(PdfFormField.Keys.Ff, new PdfInteger(132));
// signatureField.Elements.Add(PdfFormField.Keys.DR, new PdfDictionary()); TODO COMPILE
signatureField.Elements.Add(PdfAnnotation.Keys.Type, new PdfName("/Annot"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// PDFsharp - A .NET library for processing PDF
// See the LICENSE file in the solution root for more information.

#if WPF
using System.IO;
#endif
using FluentAssertions;
using PdfSharp.Diagnostics;
using PdfSharp.Drawing;
using PdfSharp.Pdf;
using PdfSharp.Pdf.Forms;
using PdfSharp.Pdf.IO;
using PdfSharp.Pdf.Signatures;
#if CORE
using PdfSharp.Fonts;
using PdfSharp.Quality;
#endif
using Xunit;

namespace PdfSharp.Tests.Pdf.Signatures
{
[Collection("PDFsharp")]
public class SignatureFieldNameTests : IDisposable
{
public SignatureFieldNameTests()
{
PdfSharpCore.ResetAll();
#if CORE
GlobalFontSettings.FontResolver = new UnitTestFontResolver();
#endif
}

public void Dispose()
{
PdfSharpCore.ResetAll();
}

[Fact]
public void Signature_field_of_a_new_document_is_named_Signature1()
{
using var document = new PdfDocument();
document.AddPage();

var signedBytes = Sign(document);

FieldNamesOf(signedBytes).Should().Equal("Signature1");
}

[Fact]
public void Signature_field_of_an_already_signed_document_gets_an_unused_name()
{
using var document = new PdfDocument();
document.AddPage();
var signedBytes = Sign(document);

// Sign the signed document again. The fully qualified names of interactive form fields must be
// unique, so the second field must not be named "Signature1" again.
using var signedDocument = PdfReader.Open(new MemoryStream(signedBytes), PdfDocumentOpenMode.Modify);
var twiceSignedBytes = Sign(signedDocument);

FieldNamesOf(twiceSignedBytes).Should().Equal("Signature1", "Signature2");
}

[Fact]
public void Signature_field_does_not_take_the_name_of_an_existing_field()
{
using var document = new PdfDocument();
document.AddPage();

// A text field named "Signature1" already occupies the name the signature field would get.
var acroForm = document.Catalog.GetOrCreateAcroForm();
var textField = new PdfDictionary(document);
textField.Elements.SetName(PdfFormField.Keys.FT, PdfFormFieldType.Text);
textField.Elements.SetString(PdfFormField.Keys.T, "Signature1");
document.Internals.AddObject(textField);
acroForm.Fields.Elements.Add(textField);

var signedBytes = Sign(document);

FieldNamesOf(signedBytes).Should().Equal("Signature1", "Signature2");
}

static byte[] Sign(PdfDocument document)
{
var options = new DigitalSignatureOptions
{
ContactInfo = "John Doe",
Location = "Seattle",
Reason = "License Agreement",
Rectangle = new XRect(36, 36, 200, 50),
AppearanceHandler = new EmptyAppearanceHandler()
};
_ = DigitalSignatureHandler.ForDocument(document, new TestSigner(), options);

using var stream = new MemoryStream();
document.Save(stream, false);
return stream.ToArray();
}

/// <summary>
/// Gets the partial names of the fields at the root of the interactive form.
/// </summary>
internal static List<string> FieldNamesOf(byte[] pdfBytes)
{
using var document = PdfReader.Open(new MemoryStream(pdfBytes), PdfDocumentOpenMode.Import);
var fields = document.Catalog.GetAcroForm()?.Elements.GetArray(PdfForm.Keys.Fields);
fields.Should().NotBeNull();

var names = new List<string>();
for (int idx = 0; idx < fields!.Elements.Count; idx++)
names.Add(fields.Elements.GetDictionary(idx)!.Elements.GetString(PdfFormField.Keys.T));
return names;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// PDFsharp - A .NET library for processing PDF
// See the LICENSE file in the solution root for more information.

#if WPF
using System.IO;
#endif
using PdfSharp.Drawing;
using PdfSharp.Pdf.Annotations;
using PdfSharp.Pdf.Signatures;

namespace PdfSharp.Tests.Pdf.Signatures
{
/// <summary>
/// A signer that creates a deterministic dummy signature, so that tests need no certificate.
/// </summary>
class TestSigner : IDigitalSigner
{
public string CertificateName => "PDFsharp unit test";

public Task<int> GetSignatureSizeAsync() => Task.FromResult(SignatureSize);

public Task<byte[]> GetSignatureAsync(Stream stream)
{
// Read the stream to ensure the ranges to be signed are readable.
var buffer = new byte[4096];
while (stream.Read(buffer, 0, buffer.Length) > 0)
{ }

var signature = new byte[SignatureSize];
for (int idx = 0; idx < signature.Length; idx++)
signature[idx] = (byte)idx;
return Task.FromResult(signature);
}

const int SignatureSize = 512;
}

/// <summary>
/// An appearance handler that draws nothing, so that tests need no font.
/// </summary>
class EmptyAppearanceHandler : IAnnotationAppearanceHandler
{
public void DrawAppearance(XGraphics gfx, XRect rect)
{ }
}
}