diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureHandler.cs
index a6d00f3c..c36a1119 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureHandler.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureHandler.cs
@@ -175,8 +175,11 @@ 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);
@@ -184,9 +187,6 @@ internal async Task AddSignatureComponentsAsync() // #US321 TODO Use appropriate
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));
@@ -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.
+ ///
+ /// 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.
+ ///
+ /// The interactive form the new field is added to.
+ 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(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);
@@ -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"));
diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/SignatureFieldNameTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/SignatureFieldNameTests.cs
new file mode 100644
index 00000000..2778bb96
--- /dev/null
+++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/SignatureFieldNameTests.cs
@@ -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();
+ }
+
+ ///
+ /// Gets the partial names of the fields at the root of the interactive form.
+ ///
+ internal static List 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();
+ for (int idx = 0; idx < fields!.Elements.Count; idx++)
+ names.Add(fields.Elements.GetDictionary(idx)!.Elements.GetString(PdfFormField.Keys.T));
+ return names;
+ }
+ }
+}
diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/TestSigner.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/TestSigner.cs
new file mode 100644
index 00000000..a6a85008
--- /dev/null
+++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/TestSigner.cs
@@ -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
+{
+ ///
+ /// A signer that creates a deterministic dummy signature, so that tests need no certificate.
+ ///
+ class TestSigner : IDigitalSigner
+ {
+ public string CertificateName => "PDFsharp unit test";
+
+ public Task GetSignatureSizeAsync() => Task.FromResult(SignatureSize);
+
+ public Task 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;
+ }
+
+ ///
+ /// An appearance handler that draws nothing, so that tests need no font.
+ ///
+ class EmptyAppearanceHandler : IAnnotationAppearanceHandler
+ {
+ public void DrawAppearance(XGraphics gfx, XRect rect)
+ { }
+ }
+}