diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index fe9f2272..6777d2f0 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -1,113 +1,116 @@
-
-
-
- 6.0.0
- 6.0.0
- 6.0.0
- 6.0.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ 6.0.0
+ 6.0.0
+ 6.0.0
+ 6.0.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj
index 51dc232e..4b1c2957 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj
+++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj
@@ -256,6 +256,7 @@
+
@@ -319,6 +320,14 @@
+
+
+
+
+
+
+
+
@@ -424,4 +433,8 @@
+
+
+
+
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj
index e32b4515..32f1461e 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj
+++ b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj
@@ -255,6 +255,7 @@
+
@@ -318,6 +319,14 @@
+
+
+
+
+
+
+
+
@@ -430,4 +439,9 @@
-->
+
+
+
+
+
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs
index 79afe41b..17089acd 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs
@@ -267,7 +267,7 @@ public PdfAcroFieldCollection Fields
///
public sealed class PdfAcroFieldCollection : PdfArray
{
- PdfAcroFieldCollection(PdfArray array)
+ internal PdfAcroFieldCollection(PdfArray array)
: base(array)
{ }
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs
index d1f84691..429a6f9f 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs
@@ -1,7 +1,9 @@
-// PDFsharp - A .NET library for processing PDF
+// PDFsharp - A .NET library for processing PDF
// See the LICENSE file in the solution root for more information.
using PdfSharp.Pdf.IO;
+using PdfSharp.Drawing;
+using PdfSharp.Pdf.Annotations;
namespace PdfSharp.Pdf.AcroForms
{
@@ -21,6 +23,51 @@ internal PdfSignatureField(PdfDictionary dict)
: base(dict)
{ }
+ public IAnnotationAppearanceHandler CustomAppearanceHandler { get; internal set; }
+
+ ///
+ /// Creates the custom appearance form X object for the annotation that represents
+ /// this acro form text field.
+ ///
+ void RenderCustomAppearance()
+ {
+ PdfRectangle rect = Elements.GetRectangle(PdfAnnotation.Keys.Rect);
+
+ var visible = !(rect.X1 + rect.X2 + rect.Y1 + rect.Y2 == 0);
+
+ if (!visible)
+ return;
+
+ if (CustomAppearanceHandler == null)
+ throw new Exception("AppearanceHandler is null");
+
+ XForm form = new XForm(_document, rect.Size);
+ XGraphics gfx = XGraphics.FromForm(form);
+
+ CustomAppearanceHandler.DrawAppearance(gfx, rect.ToXRect());
+
+ form.DrawingFinished();
+
+ // Get existing or create new appearance dictionary
+ if (Elements[PdfAnnotation.Keys.AP] is not PdfDictionary ap)
+ {
+ ap = new PdfDictionary(_document);
+ Elements[PdfAnnotation.Keys.AP] = ap;
+ }
+
+ // Set XRef to normal state
+ ap.Elements["/N"] = form.PdfForm.Reference;
+
+ form.PdfRenderer.Close();
+ }
+
+ internal override void PrepareForSave()
+ {
+ base.PrepareForSave();
+ if (CustomAppearanceHandler != null)
+ RenderCustomAppearance();
+ }
+
///
/// Writes a key/value pair of this signature field dictionary.
///
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/IAnnotationAppearanceHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/IAnnotationAppearanceHandler.cs
new file mode 100644
index 00000000..e686b05f
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/IAnnotationAppearanceHandler.cs
@@ -0,0 +1,12 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+using PdfSharp.Drawing;
+
+namespace PdfSharp.Pdf.Annotations
+{
+ public interface IAnnotationAppearanceHandler
+ {
+ void DrawAppearance(XGraphics gfx, XRect rect);
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs
new file mode 100644
index 00000000..64a6b28a
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs
@@ -0,0 +1,89 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+using Org.BouncyCastle.Cms;
+using Org.BouncyCastle.Security;
+using Org.BouncyCastle.Utilities.Collections;
+using System.Security.Cryptography;
+#if WPF
+using System.IO;
+#endif
+using System.Security.Cryptography.X509Certificates;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ public class BouncySigner : ISigner
+ {
+ private X509Certificate2 Certificate { get; set; }
+ private X509Certificate2Collection CertificateChain { get; }
+
+ public string GetName()
+ {
+ return Certificate.GetNameInfo(X509NameType.SimpleName, false);
+ }
+
+ public BouncySigner(Tuple certificateData)
+ {
+ this.Certificate = certificateData.Item1;
+ this.CertificateChain = certificateData.Item2;
+ }
+
+ public byte[] GetSignedCms(Stream rangedStream, int pdfVersion)
+ {
+ rangedStream.Position = 0;
+
+ CmsSignedDataGenerator signedDataGenerator = new CmsSignedDataGenerator();
+
+ var cert = DotNetUtilities.FromX509Certificate(Certificate);
+ var key = DotNetUtilities.GetKeyPair(GetAsymmetricAlgorithm(Certificate));
+ var allCerts = CertificateChain.OfType().Select(item => DotNetUtilities.FromX509Certificate(item));
+
+ var store = CollectionUtilities.CreateStore(allCerts);
+
+ signedDataGenerator.AddSigner(key.Private, cert, GetProperDigestAlgorithm(pdfVersion));
+ signedDataGenerator.AddCertificates(store);
+
+ CmsProcessableInputStream msg = new CmsProcessableInputStream(rangedStream);
+
+ CmsSignedData signedData = signedDataGenerator.Generate(msg, false);
+
+ return signedData.GetEncoded();
+ }
+
+ ///
+ /// adbe.pkcs7.detached supported algorithms: SHA1 (PDF 1.3), SHA256 (PDF 1.6), SHA384/SHA512/RIPEMD160 (PDF 1.7)
+ ///
+ /// PDF version as int
+ ///
+ private string GetProperDigestAlgorithm(int pdfVersion)
+ {
+ switch (pdfVersion)
+ {
+ case int when pdfVersion >= 17:
+ return CmsSignedDataGenerator.DigestSha512;
+ case int when pdfVersion == 16:
+ return CmsSignedDataGenerator.DigestSha256;
+ case int when pdfVersion >= 13:
+ default:
+ return CmsSignedDataGenerator.DigestSha256; // SHA1 is obsolete, use at least SHA256
+ }
+ }
+
+ private AsymmetricAlgorithm? GetAsymmetricAlgorithm(X509Certificate2 cert)
+ {
+ const String RSA = "1.2.840.113549.1.1.1";
+ const String DSA = "1.2.840.10040.4.1";
+ const String ECC = "1.2.840.10045.2.1";
+
+ return cert.PublicKey.Oid.Value switch
+ {
+ RSA => cert.GetRSAPrivateKey(),
+#if NET6_0_OR_GREATER
+ DSA => cert.GetDSAPrivateKey(),
+#endif
+ ECC => cert.GetECDsaPrivateKey(),
+ _ => throw new NotImplementedException(),
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureAppearanceHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureAppearanceHandler.cs
new file mode 100644
index 00000000..652e9cfa
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureAppearanceHandler.cs
@@ -0,0 +1,35 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+using PdfSharp.Drawing;
+using PdfSharp.Drawing.Layout;
+using PdfSharp.Pdf.Annotations;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ internal class DefaultSignatureAppearanceHandler : IAnnotationAppearanceHandler
+ {
+ public string? Location { get; set; }
+ public string? Reason { get; set; }
+ public string? Signer { get; set; }
+
+
+ public void DrawAppearance(XGraphics gfx, XRect rect)
+ {
+ var backColor = XColor.Empty;
+ var defaultText = string.Format("Signed by: {0}\nLocation: {1}\nReason: {2}\nDate: {3}", Signer, Location, Reason, DateTime.Now);
+
+ XFont font = new XFont("Verdana", 7, XFontStyleEx.Regular);
+
+ XTextFormatter txtFormat = new XTextFormatter(gfx);
+
+ var currentPosition = new XPoint(0, 0);
+
+ txtFormat.DrawString(defaultText,
+ font,
+ new XSolidBrush(XColor.FromKnownColor(XKnownColor.Black)),
+ new XRect(currentPosition.X, currentPosition.Y, rect.Width - currentPosition.X, rect.Height),
+ XStringFormats.TopLeft);
+ }
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs
new file mode 100644
index 00000000..db8c8563
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs
@@ -0,0 +1,120 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+#if WPF
+using System.IO;
+#endif
+#if NET6_0_OR_GREATER
+using System.Net.Http;
+using System.Net.Http.Headers;
+#endif
+using System.Security.Cryptography;
+using System.Security.Cryptography.Pkcs;
+using System.Security.Cryptography.X509Certificates;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ public class DefaultSigner : ISigner
+ {
+ private static readonly Oid SignatureTimeStampOin = new Oid("1.2.840.113549.1.9.16.2.14");
+ private static readonly string TimestampQueryContentType = "application/timestamp-query";
+ private static readonly string TimestampReplyContentType = "application/timestamp-reply";
+
+ private X509Certificate2 _certificate { get; init; }
+ private Uri? _timeStampAuthorityUri { get; init; }
+
+ public DefaultSigner(X509Certificate2 Certificate)
+ {
+ _certificate = Certificate;
+ }
+
+#if NET6_0_OR_GREATER
+ ///
+ /// using a TimeStamp Authority to add timestamp to signature, only on net6+ for now due to available classes for Rfc3161
+ ///
+ ///
+ ///
+ public DefaultSigner(X509Certificate2 Certificate, Uri? timeStampAuthorityUri = null)
+ {
+ _certificate = Certificate;
+ _timeStampAuthorityUri = timeStampAuthorityUri;
+ }
+#endif
+
+ public byte[] GetSignedCms(Stream stream, int pdfVersion)
+ {
+ var range = new byte[stream.Length];
+ stream.Position = 0;
+ stream.Read(range, 0, range.Length);
+
+ // Sign the byte range
+ var contentInfo = new ContentInfo(range);
+ SignedCms signedCms = new SignedCms(contentInfo, true);
+ CmsSigner signer = new CmsSigner(_certificate)/* { IncludeOption = X509IncludeOption.WholeChain }*/;
+ signer.UnsignedAttributes.Add(new Pkcs9SigningTime());
+
+ signedCms.ComputeSignature(signer, true);
+
+#if NET6_0_OR_GREATER
+ if (_timeStampAuthorityUri is not null)
+ Task.Run(() => AddTimestampFromTSAAsync(signedCms)).Wait();
+#endif
+
+ var bytes = signedCms.Encode();
+
+ return bytes;
+ }
+
+ public string GetName()
+ {
+ return _certificate.GetNameInfo(X509NameType.SimpleName, false);
+ }
+
+#if NET6_0_OR_GREATER
+ private async Task AddTimestampFromTSAAsync(SignedCms signedCms)
+ {
+ // Generate our nonce to identify the pair request-response
+ byte[] nonce = new byte[8];
+#if NET6_0_OR_GREATER
+ nonce = RandomNumberGenerator.GetBytes(8);
+#else
+ using var cryptoProvider = new RNGCryptoServiceProvider();
+ cryptoProvider.GetBytes(nonce = new Byte[8]);
+#endif
+ // Get our signing information and create the RFC3161 request
+ SignerInfo newSignerInfo = signedCms.SignerInfos[0];
+ // Now we generate our request for us to send to our RFC3161 signing authority.
+ var request = Rfc3161TimestampRequest.CreateFromSignerInfo(
+ newSignerInfo,
+ HashAlgorithmName.SHA256,
+ requestSignerCertificates: true, // ask TSA to embed its signing certificate in the timestamp token
+ nonce: nonce);
+
+ var client = new HttpClient();
+ var content = new ReadOnlyMemoryContent(request.Encode());
+ content.Headers.ContentType = new MediaTypeHeaderValue(TimestampQueryContentType);
+ var httpResponse = await client.PostAsync(_timeStampAuthorityUri, content).ConfigureAwait(false);
+
+ // Process our response
+ if (!httpResponse.IsSuccessStatusCode)
+ {
+ throw new CryptographicException(
+ $"There was a error from the timestamp authority. It responded with {httpResponse.StatusCode} {(int)httpResponse.StatusCode}: {httpResponse.Content}");
+ }
+ if (httpResponse.Content.Headers.ContentType.MediaType != TimestampReplyContentType)
+ {
+ throw new CryptographicException("The reply from the time stamp server was in a invalid format.");
+ }
+ var data = await httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
+ var timestampToken = request.ProcessResponse(data, out _);
+
+ // The RFC3161 sign certificate is separate to the contents that was signed, we need to add it to the unsigned attributes.
+#if NET6_0_OR_GREATER
+ newSignerInfo.AddUnsignedAttribute(new AsnEncodedData(SignatureTimeStampOin, timestampToken.AsSignedCms().Encode()));
+#else
+ newSignerInfo.UnsignedAttributes.Add(new AsnEncodedData(SignatureTimeStampOin, timestampToken.AsSignedCms().Encode()));
+#endif
+ }
+#endif
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs
new file mode 100644
index 00000000..8f5722c2
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs
@@ -0,0 +1,16 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+#if WPF
+using System.IO;
+#endif
+
+namespace PdfSharp.Pdf.Signatures
+{
+ public interface ISigner
+ {
+ byte[] GetSignedCms(Stream stream, int pdfVersion);
+
+ string GetName();
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs
new file mode 100644
index 00000000..5bbb8461
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs
@@ -0,0 +1,46 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+using PdfSharp.Pdf.IO;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ internal class PdfArrayWithPadding : PdfArray
+ {
+ public int PaddingRight { get; private set; }
+
+ public PdfArrayWithPadding(PdfDocument document, int paddingRight, params PdfItem[] items)
+ : base(document, items)
+ {
+ PaddingRight = paddingRight;
+ }
+
+ internal override void WriteObject(PdfWriter writer)
+ {
+ PositionStart = writer.Position;
+
+ base.WriteObject(writer);
+
+ if (PaddingRight > 0)
+ {
+ var bytes = new byte[PaddingRight];
+ for (int i = 0; i < PaddingRight; i++)
+ bytes[i] = 32;// space
+
+ writer.Write(bytes);
+ }
+
+ PositionEnd = writer.Position;
+ }
+
+ ///
+ /// Position of the first byte of this string in PdfWriter's Stream
+ ///
+ public long PositionStart { get; internal set; }
+
+ ///
+ /// Position of the last byte of this string in PdfWriter's Stream
+ ///
+ public long PositionEnd { get; internal set; }
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs
new file mode 100644
index 00000000..bd14869a
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs
@@ -0,0 +1,235 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+using PdfSharp.Pdf.AcroForms;
+using PdfSharp.Pdf.Advanced;
+using PdfSharp.Pdf.Internal;
+using System.Text;
+#if WPF
+using System.IO;
+#endif
+
+namespace PdfSharp.Pdf.Signatures
+{
+ ///
+ /// PdfDocument signature handler.
+ /// Attaches a PKCS#7 signature digest to PdfDocument.
+ /// Digest algorithm will be either SHA256/SHA512 depending on PdfDocument.Version.
+ ///
+ public class PdfSignatureHandler
+ {
+ private PdfString signatureFieldContentsPdfString;
+ private PdfArray signatureFieldByteRangePdfArray;
+
+ ///
+ /// Cached signature length (in bytes) for each PDF version since digest length depends on digest algorithm that depends on PDF version.
+ ///
+ private static Dictionary knownSignatureLengthInBytesByPdfVersion = new();
+
+ ///
+ /// (arbitrary) big enough reserved space to replace ByteRange placeholder [0 0 0 0] with the actual computed value of the byte range to sign
+ ///
+ private const int byteRangePaddingLength = 36;
+
+ ///
+ /// Pdf Document signature will be attached to
+ ///
+ public PdfDocument Document { get; private set; }
+
+ ///
+ /// Signature options
+ ///
+ public PdfSignatureOptions Options { get; private set; }
+ private ISigner signer { get; set; }
+
+ ///
+ /// Attach this signature handler to the given Pdf document
+ ///
+ /// Pdf document to sign
+ public void AttachToDocument(PdfDocument documentToSign)
+ {
+ this.Document = documentToSign;
+ this.Document.BeforeSave += AddSignatureComponents;
+ this.Document.AfterSave += ComputeSignatureAndRange;
+
+ // estimate signature length by computing signature for a fake byte[]
+ if (!knownSignatureLengthInBytesByPdfVersion.ContainsKey(documentToSign.Version))
+ knownSignatureLengthInBytesByPdfVersion[documentToSign.Version] =
+ signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), documentToSign.Version).Length
+ + 10 /* arbitrary margin added because TSA timestamp response's length seems to vary from a call to another (I saw a variation of 1 byte) */;
+ }
+
+ public PdfSignatureHandler(ISigner signer, PdfSignatureOptions options)
+ {
+ if (signer is null)
+ throw new ArgumentNullException(nameof(signer));
+ if (options is null)
+ throw new ArgumentNullException(nameof(options));
+
+ if (options.PageIndex < 0)
+ throw new ArgumentOutOfRangeException($"Signature page index cannot be negative.");
+
+ this.signer = signer;
+ this.Options = options;
+ }
+
+ private void ComputeSignatureAndRange(object sender, PdfDocumentEventArgs e)
+ {
+ var writer = e.Writer;
+
+ var isVerbose = writer.Layout == IO.PdfWriterLayout.Verbose; // DEBUG mode makes the writer Verbose and will introduce 1 extra space between entries key and value
+ // if Verbose, a space is added between entry key and entry value
+ var verboseExtraSpaceSeparatorLength = isVerbose ? 1 : 0;
+
+ var (rangedStreamToSign, byteRangeArray) = GetRangeToSignAndByteRangeArray(writer.Stream, verboseExtraSpaceSeparatorLength);
+
+ // writing actual ByteRange in place of the placeholder
+
+ writer.Stream.Position = (signatureFieldByteRangePdfArray as PdfArrayWithPadding).PositionStart;
+ byteRangeArray.WriteObject(writer);
+
+ // computing signature from document's digest
+ var signature = signer.GetSignedCms(rangedStreamToSign, Document.Version);
+
+ if (signature.Length > knownSignatureLengthInBytesByPdfVersion[Document.Version])
+ throw new Exception("The actual digest length is bigger that the approximation made. Not enough room in the placeholder to fit the signature.");
+
+ // directly writes document's signature in the /Contents<> entry
+ writer.Stream.Position = signatureFieldContentsPdfString.PositionStart
+ + verboseExtraSpaceSeparatorLength /* tempContentsPdfString is orphan, so it will not write the space delimiter: need to begin write 1 byte further if Verbose */
+ + 1 /* skip the begin-delimiter '<' */;
+ writer.Write(PdfEncoders.RawEncoding.GetBytes(FormatHex(signature)));
+ }
+
+ private string FormatHex(byte[] bytes) // starting from .net5, could be replaced by Convert.ToHexString(Byte[]). keeping current method to be ease .net48/netstandard compatibility
+ {
+ var retval = new StringBuilder();
+
+ for (int idx = 0; idx < bytes.Length; idx++)
+ retval.AppendFormat("{0:X2}", bytes[idx]);
+
+ return retval.ToString();
+ }
+
+ ///
+ /// Get the bytes ranges to sign.
+ /// As recommended in PDF specs, whole document will be signed, except for the hexadecimal signature token value in the /Contents entry.
+ /// Example: '/Contents <aaaaa111111>' => '<aaaaa111111>' will be excluded from the bytes to sign.
+ ///
+ ///
+ ///
+ ///
+ private (RangedStream rangedStream, PdfArray byteRangeArray) GetRangeToSignAndByteRangeArray(Stream stream, int verboseExtraSpaceSeparatorLength)
+ {
+ long firstRangeOffset = 0,
+ firstRangeLength = signatureFieldContentsPdfString.PositionStart + verboseExtraSpaceSeparatorLength,
+ secondRangeOffset = signatureFieldContentsPdfString.PositionEnd,
+ secondRangeLength = (int)stream.Length - signatureFieldContentsPdfString.PositionEnd;
+
+ var byteRangeArray = new PdfArray();
+ byteRangeArray.Elements.Add(new PdfLongInteger(firstRangeOffset));
+ byteRangeArray.Elements.Add(new PdfLongInteger(firstRangeLength));
+ byteRangeArray.Elements.Add(new PdfLongInteger(secondRangeOffset));
+ byteRangeArray.Elements.Add(new PdfLongInteger(secondRangeLength));
+
+ var rangedStream = new RangedStream(stream, new List()
+ {
+ new RangedStream.Range(firstRangeOffset, firstRangeLength),
+ new RangedStream.Range(secondRangeOffset, secondRangeLength)
+ });
+
+ return (rangedStream, byteRangeArray);
+ }
+
+ private void AddSignatureComponents(object sender, EventArgs e)
+ {
+ if (Options.PageIndex >= Document.PageCount)
+ throw new ArgumentOutOfRangeException($"Signature page doesn't exist, specified page was {Options.PageIndex + 1} but document has only {Document.PageCount} page(s).");
+
+ var fakeSignature = Enumerable.Repeat((byte)0x00/*padded with zeros, as recommended (trailing zeros have no incidence on signature decoding)*/, knownSignatureLengthInBytesByPdfVersion[Document.Version]).ToArray();
+ var fakeSignatureAsRawString = PdfEncoders.RawEncoding.GetString(fakeSignature, 0, fakeSignature.Length);
+ signatureFieldContentsPdfString = new PdfString(fakeSignatureAsRawString, PdfStringFlags.HexLiteral); // has to be a hex string
+ signatureFieldByteRangePdfArray = new PdfArrayWithPadding(Document, byteRangePaddingLength, new PdfLongInteger(0), new PdfLongInteger(0), new PdfLongInteger(0), new PdfLongInteger(0));
+ //Document.Internals.AddObject(signatureFieldByteRange);
+
+ var signatureDictionary = GetSignatureDictionary(signatureFieldContentsPdfString, signatureFieldByteRangePdfArray);
+ var signatureField = GetSignatureField(signatureDictionary);
+
+ var annotations = Document.Pages[Options.PageIndex].Elements.GetArray(PdfPage.Keys.Annots);
+ if (annotations == null)
+ Document.Pages[Options.PageIndex].Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField));
+ else
+ annotations.Elements.Add(signatureField);
+
+
+ // acroform
+
+ var catalog = Document.Catalog;
+
+ if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null)
+ catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document));
+
+ if (!catalog.AcroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags))
+ catalog.AcroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3));
+ else
+ {
+ var sigFlagVersion = catalog.AcroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags);
+ if (sigFlagVersion < 3)
+ catalog.AcroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3);
+ }
+
+ if (catalog.AcroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) == null)
+ catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, new PdfAcroField.PdfAcroFieldCollection(new PdfArray()));
+ catalog.AcroForm.Fields.Elements.Add(signatureField);
+ }
+
+ private PdfSignatureField GetSignatureField(PdfDictionary signatureDic)
+ {
+ var signatureField = new PdfSignatureField(Document);
+
+ signatureField.Elements.Add(PdfSignatureField.Keys.V, signatureDic);
+
+ // annotation keys
+ signatureField.Elements.Add(PdfSignatureField.Keys.FT, new PdfName("/Sig"));
+ signatureField.Elements.Add(PdfSignatureField.Keys.T, new PdfString("Signature1")); // TODO? if already exists, will it cause error? implement a name choser if yes
+ signatureField.Elements.Add(PdfSignatureField.Keys.Ff, new PdfInteger(132));
+ signatureField.Elements.Add(PdfSignatureField.Keys.DR, new PdfDictionary());
+ signatureField.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Annot"));
+ signatureField.Elements.Add("/Subtype", new PdfName("/Widget"));
+ signatureField.Elements.Add("/P", Document.Pages[Options.PageIndex]);
+
+ signatureField.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle));
+
+ signatureField.CustomAppearanceHandler = Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler()
+ {
+ Location = Options.Location,
+ Reason = Options.Reason,
+ Signer = signer.GetName()
+ };
+ signatureField.PrepareForSave(); // TODO: for some reason, PdfSignatureField.PrepareForSave() is not triggered automatically so let's call it manually from here, but it would be better to be called automatically
+
+ Document.Internals.AddObject(signatureField);
+
+ return signatureField;
+ }
+
+ private PdfDictionary GetSignatureDictionary(PdfString contents, PdfArray byteRange)
+ {
+ PdfDictionary signatureDic = new PdfDictionary(Document);
+
+ signatureDic.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Sig"));
+ signatureDic.Elements.Add(PdfSignatureField.Keys.Filter, new PdfName("/Adobe.PPKLite"));
+ signatureDic.Elements.Add(PdfSignatureField.Keys.SubFilter, new PdfName("/adbe.pkcs7.detached"));
+ signatureDic.Elements.Add(PdfSignatureField.Keys.M, new PdfDate(DateTime.Now));
+
+ signatureDic.Elements.Add(PdfSignatureField.Keys.Contents, contents);
+ signatureDic.Elements.Add(PdfSignatureField.Keys.ByteRange, byteRange);
+ signatureDic.Elements.Add(PdfSignatureField.Keys.Reason, new PdfString(Options.Reason));
+ signatureDic.Elements.Add(PdfSignatureField.Keys.Location, new PdfString(Options.Location));
+
+ Document.Internals.AddObject(signatureDic);
+
+ return signatureDic;
+ }
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs
new file mode 100644
index 00000000..1867ff54
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs
@@ -0,0 +1,21 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+using PdfSharp.Drawing;
+using PdfSharp.Pdf.Annotations;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ public class PdfSignatureOptions
+ {
+ public IAnnotationAppearanceHandler AppearanceHandler { get; set; }
+ public string ContactInfo { get; set; }
+ public string Location { get; set; }
+ public string Reason { get; set; }
+ public XRect Rectangle { get; set; }
+ ///
+ /// page index, zero-based
+ ///
+ public int PageIndex { get; set; }
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs
new file mode 100644
index 00000000..84dbcbdb
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs
@@ -0,0 +1,168 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+#if WPF
+using System.IO;
+#endif
+
+namespace PdfSharp.Pdf.Signatures
+{
+ internal class RangedStream : Stream
+ {
+ private Range[] ranges;
+
+ public class Range
+ {
+
+ public Range(long offset, long length)
+ {
+ this.Offset = offset;
+ this.Length = length;
+ }
+ public long Offset { get; set; }
+ public long Length { get; set; }
+ }
+
+ private Stream stream { get; set; }
+
+
+ public RangedStream(Stream originalStrem, List ranges)
+ {
+ this.stream = originalStrem;
+
+ long previousPosition = 0;
+
+ this.ranges = ranges.OrderBy(item => item.Offset).ToArray();
+ foreach (var range in ranges)
+ {
+ if (range.Offset < previousPosition)
+ throw new Exception("Ranges are not continuous");
+ previousPosition = range.Offset + range.Length;
+ }
+ }
+
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override bool CanWrite
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override long Length
+ {
+ get
+ {
+ return ranges.Sum(item => item.Length);
+ }
+ }
+
+
+ private IEnumerable GetPreviousRanges(long position)
+ {
+ return ranges.Where(item => item.Offset < position && item.Offset + item.Length < position);
+ }
+
+ private Range GetCurrentRange(long position)
+ {
+ return ranges.FirstOrDefault(item => item.Offset <= position && item.Offset + item.Length > position);
+ }
+
+
+
+ public override long Position
+ {
+ get
+ {
+ return GetPreviousRanges(stream.Position).Sum(item => item.Length) + stream.Position - GetCurrentRange(stream.Position).Offset;
+ }
+
+ set
+ {
+ Range? currentRange = null;
+ List previousRanges = new List();
+ long maxPosition = 0;
+ foreach (var range in ranges)
+ {
+ currentRange = range;
+ maxPosition += range.Length;
+ if (maxPosition > value)
+ break;
+ previousRanges.Add(range);
+ }
+
+ long positionInCurrentRange = value - previousRanges.Sum(item => item.Length);
+ stream.Position = currentRange.Offset + positionInCurrentRange;
+ }
+ }
+
+
+
+ public override void Flush()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+
+ var length = stream.Length;
+ int retVal = 0;
+ for (int i = 0; i < count; i++)
+ {
+
+ if (stream.Position == length)
+ {
+ break;
+ }
+
+ PerformSkipIfNeeded();
+ retVal += stream.Read(buffer, offset++, 1);
+
+ }
+
+ return retVal;
+ }
+
+
+ private void PerformSkipIfNeeded()
+ {
+ var currentRange = GetCurrentRange(stream.Position);
+
+ if (currentRange == null)
+ stream.Position = GetNextRange().Offset;
+ }
+
+ private Range GetNextRange()
+ {
+ return ranges.OrderBy(item => item.Offset).First(item => item.Offset > stream.Position);
+ }
+
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs
index 9b3ad75d..5bf645bf 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs
@@ -1,4 +1,4 @@
-// PDFsharp - A .NET library for processing PDF
+// PDFsharp - A .NET library for processing PDF
// See the LICENSE file in the solution root for more information.
using System.Runtime.InteropServices;
@@ -19,12 +19,24 @@
namespace PdfSharp.Pdf
{
+ internal class PdfDocumentEventArgs : EventArgs
+ {
+ public PdfDocumentEventArgs(PdfWriter writer)
+ {
+ Writer = writer;
+ }
+
+ public PdfWriter Writer { get; set; }
+ }
+
///
/// Represents a PDF document.
///
[DebuggerDisplay("(Name={" + nameof(Name) + "})")] // A name makes debugging easier
public sealed class PdfDocument : PdfObject, IDisposable
{
+ internal event EventHandler BeforeSave = (s, e) => { };
+ internal event EventHandler AfterSave = (s, e) => { };
#if DEBUG_
static PdfDocument()
{
@@ -220,7 +232,7 @@ public void Save(string path)
if (!CanModify)
throw new InvalidOperationException(PSSR.CannotModify);
- using Stream stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ using Stream stream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite/*Read access needed for SignatureHandler*/, FileShare.None);
Save(stream);
}
@@ -312,6 +324,8 @@ void DoSave(PdfWriter writer)
{
PdfSharpLogHost.Logger.PdfDocumentSaved(Name);
+ BeforeSave(this, EventArgs.Empty);
+
if (_pages == null || _pages.Count == 0)
{
if (OutStream != null)
@@ -376,6 +390,8 @@ void DoSave(PdfWriter writer)
{
if (writer != null!)
{
+ AfterSave(this, new PdfDocumentEventArgs(writer));
+
writer.Stream.Flush();
// DO NOT CLOSE WRITER HERE
}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs
index 9d83a5ce..75c924a2 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs
@@ -420,7 +420,21 @@ static bool IsRawEncoding(string s)
///
internal override void WriteObject(PdfWriter writer)
{
+ PositionStart = writer.Position;
+
writer.Write(this);
+
+ PositionEnd = writer.Position;
}
+
+ ///
+ /// Position of the first byte of this string in PdfWriter's Stream
+ ///
+ public long PositionStart { get; internal set; }
+
+ ///
+ /// Position of the last byte of this string in PdfWriter's Stream
+ ///
+ public long PositionEnd { get; internal set; }
}
}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj
index cefdd52c..217bb259 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj
@@ -58,4 +58,9 @@
+
+
+
+
+