diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 6777d2f0..6816e98f 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -11,6 +11,7 @@
+
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 4b1c2957..ed5edecd 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj
+++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj
@@ -324,10 +324,11 @@
-
-
-
+
+
+
+
@@ -434,6 +435,7 @@
+
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 32f1461e..0e04bc1b 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj
+++ b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj
@@ -319,15 +319,16 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -440,6 +441,7 @@
-->
+
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 17089acd..5f8417e8 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs
@@ -34,6 +34,10 @@ public string Name
string name = Elements.GetString(Keys.T);
return name;
}
+ set
+ {
+ Elements.SetString(Keys.T, value);
+ }
}
///
@@ -267,6 +271,10 @@ public PdfAcroFieldCollection Fields
///
public sealed class PdfAcroFieldCollection : PdfArray
{
+ PdfAcroFieldCollection(PdfDocument document)
+ : base(document)
+ { }
+
internal PdfAcroFieldCollection(PdfArray array)
: base(array)
{ }
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroForm.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroForm.cs
index d1763a9d..d6578f48 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroForm.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroForm.cs
@@ -39,6 +39,34 @@ public PdfAcroField.PdfAcroFieldCollection Fields
}
PdfAcroField.PdfAcroFieldCollection? _fields;
+ ///
+ /// Gets the flattened field-hierarchy of this AcroForm
+ ///
+ public IEnumerable GetAllFields()
+ {
+ var fields = new List();
+ if (Fields != null)
+ {
+ for (var i = 0; i < Fields.Elements.Count; i++)
+ {
+ var field = Fields[i];
+ TraverseFields(field, ref fields);
+ }
+ }
+ return fields;
+ }
+
+ private static void TraverseFields(PdfAcroField parentField, ref List fieldList)
+ {
+ fieldList.Add(parentField);
+ for (var i = 0; i < parentField.Fields.Elements.Count; i++)
+ {
+ var field = parentField.Fields[i];
+ if (!string.IsNullOrEmpty(field.Name))
+ TraverseFields(field, ref fieldList);
+ }
+ }
+
///
/// Predefined keys of this dictionary.
/// The description comes from PDF 1.4 Reference.
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 429a6f9f..b1bbc562 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs
@@ -4,6 +4,7 @@
using PdfSharp.Pdf.IO;
using PdfSharp.Drawing;
using PdfSharp.Pdf.Annotations;
+using PdfSharp.Pdf.Signatures;
namespace PdfSharp.Pdf.AcroForms
{
@@ -17,13 +18,40 @@ public sealed class PdfSignatureField : PdfAcroField
///
internal PdfSignatureField(PdfDocument document)
: base(document)
- { }
+ {
+ Elements[PdfAcroField.Keys.FT] = new PdfName("/Sig");
+ }
internal PdfSignatureField(PdfDictionary dict)
: base(dict)
{ }
- public IAnnotationAppearanceHandler CustomAppearanceHandler { get; internal set; }
+ ///
+ /// Gets or sets the value for this field
+ ///
+ public new PdfSignatureValue? Value
+ {
+ get
+ {
+ if (sigValue is null)
+ {
+ var dict = Elements.GetValue(PdfAcroField.Keys.V) as PdfDictionary;
+ if (dict is not null)
+ sigValue = new PdfSignatureValue(dict);
+ }
+ return sigValue;
+ }
+ set
+ {
+ if (value is not null)
+ Elements.SetReference(PdfAcroField.Keys.V, value);
+ else
+ Elements.Remove(PdfAcroField.Keys.V);
+ }
+ }
+ PdfSignatureValue? sigValue;
+
+ public IAnnotationAppearanceHandler? CustomAppearanceHandler { get; internal set; }
///
/// Creates the custom appearance form X object for the annotation that represents
@@ -87,7 +115,9 @@ internal override void WriteDictionaryElement(PdfWriter writer, PdfName key)
///
/// Predefined keys of this dictionary.
- /// The description comes from PDF 1.4 Reference.
+ /// The description comes from PDF 1.4 Reference.
+ /// TODO: These are wrong !
+ /// The keys are for a , not for a
///
public new class Keys : PdfAcroField.Keys
{
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs
index 77534360..13ca3310 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCatalog.cs
@@ -68,7 +68,7 @@ public PdfPages Pages
if (_pages == null)
{
_pages = (PdfPages?)Elements.GetValue(Keys.Pages, VCF.CreateIndirect) ?? NRT.ThrowOnNull();
- if (Owner.IsImported)
+ if (Owner.IsImported && Owner._openMode != PdfDocumentOpenMode.Append)
_pages.FlattenPageTree();
}
return _pages;
@@ -150,16 +150,33 @@ public PdfNameDictionary Names
PdfNameDictionary? _names;
///
- /// Gets the AcroForm dictionary of this document.
+ /// Gets or sets the AcroForm dictionary of this document.
///
- public PdfAcroForm AcroForm
+ public PdfAcroForm? AcroForm
{
get
{
if (_acroForm == null)
- _acroForm = (PdfAcroForm?)Elements.GetValue(Keys.AcroForm)??NRT.ThrowOnNull();
+ _acroForm = (PdfAcroForm?)Elements.GetValue(Keys.AcroForm);
return _acroForm;
}
+ set
+ {
+ if (value != null)
+ {
+ if (!value.IsIndirect)
+ _document.IrefTable.Add(value);
+ Elements.SetReference(Keys.AcroForm, value);
+ _acroForm = value;
+ }
+ else
+ {
+ if (AcroForm != null && AcroForm.Reference != null)
+ _document.IrefTable.Remove(AcroForm.Reference);
+ Elements.Remove(Keys.AcroForm);
+ _acroForm = null;
+ }
+ }
}
PdfAcroForm? _acroForm;
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContent.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContent.cs
index 9cbf77c8..d985f646 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContent.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContent.cs
@@ -38,7 +38,7 @@ public PdfContent(PdfDictionary dict) // HACK PdfContent
: base(dict)
{
// A PdfContent dictionary is always unfiltered.
- Decode();
+ Owner.IrefTable.IgnoreModify(Decode); // decode modifies the object, ignore that
}
///
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContents.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContents.cs
index d11544c9..39a5230f 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContents.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfContents.cs
@@ -49,7 +49,9 @@ public PdfContent AppendContent()
{
Debug.Assert(Owner != null);
- SetModified();
+ if (Owner._openMode != PdfDocumentOpenMode.Append)
+ SetModified();
+
PdfContent content = new PdfContent(Owner);
Owner.IrefTable.Add(content);
Debug.Assert(content.Reference != null);
@@ -64,7 +66,9 @@ public PdfContent PrependContent()
{
Debug.Assert(Owner != null);
- SetModified();
+ if (Owner._openMode != PdfDocumentOpenMode.Append)
+ SetModified();
+
PdfContent content = new PdfContent(Owner);
Owner.IrefTable.Add(content);
Debug.Assert(content.Reference != null);
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs
index 6042dc55..83b476dc 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs
@@ -1,10 +1,7 @@
// PDFsharp - A .NET library for processing PDF
// See the LICENSE file in the solution root for more information.
-using System;
using System.Collections;
-using Microsoft.Extensions.Logging;
-using PdfSharp.Logging;
using PdfSharp.Pdf.IO;
namespace PdfSharp.Pdf.Advanced
@@ -27,37 +24,76 @@ public PdfCrossReferenceTable(PdfDocument document)
public Dictionary ObjectTable = [];
///
+ /// Used to collect modified objects for incremental updates
+ ///
+ public Dictionary ModifiedObjects = [];
+
/// Gets or sets a value indicating whether this table is under construction.
/// It is true while reading a PDF file.
///
internal bool IsUnderConstruction { get; set; }
+ ///
+ /// Gets a value that indicates whether this table is fully loaded (true) on in the process of being loaded (false)
+ ///
+ internal bool FullyLoaded { get; private set; }
+
+ internal void MarkFullyLoaded()
+ {
+ FullyLoaded = true;
+ }
+
+ internal void MarkAsModified(PdfReference? pdfReference)
+ {
+ if (pdfReference == null || !FullyLoaded)
+ return;
+
+ if (pdfReference.ObjectID.IsEmpty)
+ throw new ArgumentException("ObjectID must not be empty", nameof(pdfReference.ObjectID));
+
+ ModifiedObjects[pdfReference.ObjectID] = pdfReference;
+ }
+
+ ///
+ /// Used to temporarily ignore modifications to objects
+ /// (i.e. when doing type-transformations that do not change the structure of the document)
+ ///
+ ///
+ internal void IgnoreModify(Action action)
+ {
+ var prev = FullyLoaded;
+ FullyLoaded = false;
+ try
+ {
+ action();
+ }
+ finally
+ {
+ FullyLoaded = prev;
+ }
+ }
+
///
/// Adds a cross-reference entry to the table. Used when parsing the trailer.
///
public void Add(PdfReference iref)
{
+#if DEBUG_
+ if (iref.ObjectID.ObjectNumber == 948)
+ GetType();
+#endif
if (iref.ObjectID.IsEmpty)
- iref.ObjectID = new(GetNewObjectNumber());
+ iref.ObjectID = new PdfObjectID(GetNewObjectNumber());
- // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd because it would not build with .NET Framework.
+ // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd because it would not build with .NET framework
if (ObjectTable.ContainsKey(iref.ObjectID))
- {
-#if true_
- // Really happens with existing (bad) PDF files.
- // See file 'Detaljer.ARGO.KOD.rev.B.pdf' from https://github.com/ststeiger/PdfSharpCore/issues/362
throw new InvalidOperationException("Object already in table.");
-#else
- // We remove the existing one and use the latter reference.
- // HACK: This is just a quick fix that may not be the best solution in all cases.
- // On GitHub user packdat provides a PR that orders objects. This code is not yet integrated,
- // because releasing 6.1.0 had a higher priority. We will fix this in 6.2.0.
- // However, this quick fix is better than throwing an exception in all cases.
- PdfSharpLogHost.PdfReadingLogger.LogError("Object '{ObjectID}' already exists in xref table. The latter one is used.", iref.ObjectID);
- ObjectTable.Remove(iref.ObjectID);
-#endif
- }
+
ObjectTable.Add(iref.ObjectID, iref);
+
+ // new objects must be treated like modified objects
+ if (FullyLoaded && _document.IsAppending)
+ ModifiedObjects[iref.ObjectID] = iref;
}
///
@@ -77,6 +113,10 @@ public void Add(PdfObject value)
throw new InvalidOperationException("Object already in table.");
ObjectTable.Add(value.ObjectID, value.ReferenceNotNull);
+
+ // new objects must be treated like modified objects
+ if (FullyLoaded && _document.IsAppending)
+ ModifiedObjects[value.ObjectID] = value.ReferenceNotNull;
}
///
@@ -217,7 +257,7 @@ internal int Compact()
#if DEBUG
// Have any two objects the same ID?
- Dictionary ids = [];
+ Dictionary ids = new Dictionary();
foreach (PdfObjectID objID in ObjectTable.Keys)
{
ids.Add(objID.ObjectNumber, 0);
@@ -241,24 +281,24 @@ internal int Compact()
foreach (PdfReference value in ObjectTable.Values)
{
if (!refs.ContainsKey(value))
- _ = typeof(int);
+ value.GetType();
}
foreach (PdfReference iref in ObjectTable.Values)
{
- if (iref.Value == null!)
- _ = typeof(int);
+ if (iref.Value == null)
+ GetType();
Debug.Assert(iref.Value != null);
}
foreach (PdfReference iref in irefs)
{
if (!ObjectTable.ContainsKey(iref.ObjectID))
- _ = typeof(int);
+ GetType();
Debug.Assert(ObjectTable.ContainsKey(iref.ObjectID));
- if (iref.Value == null!)
- _ = typeof(int);
+ if (iref.Value == null)
+ GetType();
Debug.Assert(iref.Value != null);
}
#endif
@@ -296,7 +336,7 @@ internal void Renumber()
PdfReference iref = irefs[idx];
#if DEBUG_
if (iref.ObjectNumber == 1108)
- _ = typeof(int);
+ GetType();
#endif
iref.ObjectID = new PdfObjectID(idx + 1);
// Rehash with new number.
@@ -312,10 +352,17 @@ internal void Renumber()
///
internal SizeType GetPositionOfObjectBehind(PdfObject obj, SizeType position)
{
+ ////var position = obj.Reference?.Position ?? -1;
+ ////if (position == -1)
+ ////{
+ //// Debug.Assert(false, "Should not happen. Please send us the PDF file if you come here.");
+ //// return -1;
+ ////}
#if DEBUG
if (obj.Reference == null)
_ = typeof(int);
#endif
+
var closestPosition = SizeType.MaxValue;
PdfReference? closest = null;
foreach (var iref in ObjectTable.Values)
@@ -371,6 +418,7 @@ public void CheckConsistence()
Debug.Assert(!Equals(irefs[i].ObjectID, irefs[j].Value.ObjectID));
Debug.Assert(irefs[i].ObjectNumber != irefs[j].Value.ObjectNumber);
Debug.Assert(ReferenceEquals(irefs[i].Document, irefs[j].Document));
+ //GetType();
}
#endif
#endif
@@ -397,11 +445,20 @@ public void CheckConsistence()
// TransitiveClosure(objects, _xrefTable.document.trailer);
// }
+ ///
+ /// Calculates the transitive closure of the specified PdfObject, i.e. all indirect objects
+ /// recursively reachable from the specified object.
+ ///
+ public PdfReference[] TransitiveClosure(PdfObject pdfObject)
+ {
+ return TransitiveClosure(pdfObject, Int16.MaxValue);
+ }
+
///
/// Calculates the transitive closure of the specified PdfObject with the specified depth, i.e. all indirect objects
/// recursively reachable from the specified object in up to maximally depth steps.
///
- public PdfReference[] TransitiveClosure(PdfObject pdfObject, int depth = Int16.MaxValue)
+ public PdfReference[] TransitiveClosure(PdfObject pdfObject, int depth)
{
CheckConsistence();
Dictionary objects = new();
@@ -440,7 +497,7 @@ public PdfReference[] TransitiveClosure(PdfObject pdfObject, int depth = Int16.M
Debug.Assert(!Equals(irefs[i].ObjectID, irefs[j].Value.ObjectID));
Debug.Assert(irefs[i].ObjectNumber != irefs[j].Value.ObjectNumber);
Debug.Assert(ReferenceEquals(irefs[i].Document, irefs[j].Document));
- _ = typeof(int);
+ GetType();
}
#endif
return irefs;
@@ -463,15 +520,16 @@ void TransitiveClosureImplementation(Dictionary objects, PdfOb
#if DEBUG_
//enterCount++;
if (enterCount == 5400)
- _ = typeof(int);
+ GetType();
//if (!Object.ReferenceEquals(pdfObject.Owner, _document))
- // _ = typeof(int);
+ // GetType();
//////Debug.Assert(Object.ReferenceEquals(pdfObject27.Document, _document));
// if (item is PdfObject && ((PdfObject)item).ObjectID.ObjectNumber == 5)
// Deb/ug.WriteLine("items: " + ((PdfObject)item).ObjectID.ToString());
//if (pdfObject.ObjectNumber == 5)
- // _ = typeof(int);
+ // GetType();
#endif
+
IEnumerable? enumerable = null; //(IEnumerator)pdfObject;
PdfDictionary? dict;
PdfArray? array;
@@ -506,13 +564,13 @@ void TransitiveClosureImplementation(Dictionary objects, PdfOb
if (!ReferenceEquals(iref.Document, _document))
{
- //Debug.WriteLine($"Bad iref: {iref.ObjectID.ToString()}");
- PdfSharpLogHost.PdfReadingLogger.LogError($"Bad iref: {iref.ObjectID.ToString()}");
+ //GetType();
+ Debug.WriteLine($"Bad iref: {iref.ObjectID.ToString()}");
}
Debug.Assert(ReferenceEquals(iref.Document, _document) || iref.Document == null, "External object detected!");
#if DEBUG_
if (iref.ObjectID.ObjectNumber == 23)
- _ = typeof(int);
+ GetType();
#endif
if (!objects.ContainsKey(iref))
{
@@ -542,14 +600,11 @@ void TransitiveClosureImplementation(Dictionary objects, PdfOb
}
else
{
- //var pdfObject28 = item as PdfObject;
- ////if (pdfObject28 != null)
- //// Debug.Assert(Object.ReferenceEquals(pdfObject28.Document, _document));
- //if (pdfObject28 != null && (pdfObject28 is PdfDictionary || pdfObject28 is PdfArray))
+ var pdfObject28 = item as PdfObject;
//if (pdfObject28 != null)
// Debug.Assert(Object.ReferenceEquals(pdfObject28.Document, _document));
- if (item is PdfObject pdfObj and (PdfDictionary or PdfArray))
- TransitiveClosureImplementation(objects, pdfObj /*, ref depth*/);
+ if (pdfObject28 != null && (pdfObject28 is PdfDictionary || pdfObject28 is PdfArray))
+ TransitiveClosureImplementation(objects, pdfObject28 /*, ref depth*/);
}
}
}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfTrailer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfTrailer.cs
index 0cf6157a..add62a81 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfTrailer.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfTrailer.cs
@@ -14,6 +14,12 @@ namespace PdfSharp.Pdf.Advanced
// Reference: 3.4.4 File Trailer / Page 96
class PdfTrailer : PdfDictionary
{
+ ///
+ /// Gets or sets the position of this trailer in the input-stream
+ /// Only meaningful for loaded documents; will be zero for new documents
+ ///
+ internal SizeType Position { get; set; }
+
///
/// Initializes a new instance of PdfTrailer.
///
@@ -211,8 +217,9 @@ internal void Finish()
Elements.Remove(Keys.Prev);
- Debug.Assert(_document.IrefTable.IsUnderConstruction == false);
+ Debug.Assert(_document.IrefTable.IsUnderConstruction == false); // Why ??
_document.IrefTable.IsUnderConstruction = false;
+ _document.IrefTable.MarkFullyLoaded();
}
///
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotation.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotation.cs
index c90bd1d7..8fbaf397 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotation.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Annotations/PdfAnnotation.cs
@@ -83,7 +83,7 @@ public PdfAnnotations Parent
///
public PdfRectangle Rectangle
{
- get => Elements.GetRectangle(Keys.Rect, true);
+ get => Elements.GetRectangle(Keys.Rect);
set
{
Elements.SetRectangle(Keys.Rect, value);
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs
index 0b88e539..5462b976 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs
@@ -1239,11 +1239,13 @@ internal PdfTrailer ReadTrailer()
// Read position behind 'startxref'.
_lexer.Position = ReadSize();
+ var xrefStart = _lexer.Position;
+
// Read all trailers.
PdfTrailer? newerTrailer = null;
while (true)
{
- var trailer = ReadXRefTableAndTrailer(_document.IrefTable);
+ var trailer = ReadXRefTableAndTrailer(_document.IrefTable, xrefStart);
// Return the first found trailer, which is the one 'startxref' points to.
// This is the current trailer, even for incrementally updated files.
@@ -1261,6 +1263,7 @@ internal PdfTrailer ReadTrailer()
// Continue loading previous trailer and cache this one as the newerTrailer to add its previous trailer.
_lexer.Position = prev;
+ xrefStart = prev;
newerTrailer = trailer;
}
return _document.Trailer;
@@ -1269,7 +1272,7 @@ internal PdfTrailer ReadTrailer()
///
/// Reads cross-reference table(s) and trailer(s).
///
- PdfTrailer? ReadXRefTableAndTrailer(PdfCrossReferenceTable xrefTable)
+ PdfTrailer? ReadXRefTableAndTrailer(PdfCrossReferenceTable xrefTable, SizeType xrefStart)
{
Debug.Assert(xrefTable != null);
@@ -1336,7 +1339,10 @@ internal PdfTrailer ReadTrailer()
else if (symbol == Symbol.Trailer)
{
ReadSymbol(Symbol.BeginDictionary);
- var trailer = new PdfTrailer(_document);
+ var trailer = new PdfTrailer(_document)
+ {
+ Position = xrefStart
+ };
ReadDictionary(trailer, false);
return trailer;
}
@@ -1352,7 +1358,7 @@ internal PdfTrailer ReadTrailer()
// TODO: We have not yet tested PDF files larger than 2 GiB because we have none and cannot produce one.
// The parsed integer is the object ID of the cross-reference stream object.
- return ReadXRefStream(xrefTable);
+ return ReadXRefStream(xrefTable, xrefStart);
}
return null;
}
@@ -1406,14 +1412,11 @@ bool CheckXRefTableEntry(SizeType position, int id, int generation, out int idCh
///
/// Reads cross-reference stream(s).
///
- PdfTrailer ReadXRefStream(PdfCrossReferenceTable xrefTable)
+ PdfTrailer ReadXRefStream(PdfCrossReferenceTable xrefTable, SizeType xrefStart)
{
// Read cross-reference stream.
//Debug.Assert(_lexer.Symbol == Symbol.Integer);
- // NEEDED???
- var xrefStart = _lexer.Position - _lexer.Token.Length;
-
int number = _lexer.TokenToInteger;
int generation = ReadInteger();
// According to specs, generation number "shall not" be "other than zero".
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs
index 95f4826e..566c6535 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs
@@ -345,7 +345,8 @@ PdfDocument OpenFromStream(Stream stream, string? password, PdfDocumentOpenMode
throw new PdfReaderException(PSSR.InvalidPassword);
}
}
- else if (validity == PasswordValidity.UserPassword && openMode == PdfDocumentOpenMode.Modify)
+ else if (validity == PasswordValidity.UserPassword
+ && (openMode == PdfDocumentOpenMode.Modify || openMode == PdfDocumentOpenMode.Append))
{
if (passwordProvider != null)
{
@@ -448,16 +449,19 @@ void FinishReferences()
"All references saved in IrefTable should have been created when their referred PdfObject has been accessible.");
// Get and update object’s references.
- FinishItemReferences(iref.Value, _document, finishedObjects);
+ FinishItemReferences(iref.Value, iref, _document, finishedObjects);
}
+ // why setting it here AND in Trailer.Finish ??
_document.IrefTable.IsUnderConstruction = false;
// Fix references of trailer values and then objects and irefs are consistent.
_document.Trailer.Finish();
+
+ Debug.Assert(_document.IrefTable.ModifiedObjects.Count == 0, "There should be no modified objects");
}
- void FinishItemReferences(PdfItem? pdfItem, PdfDocument document, HashSet finishedObjects)
+ void FinishItemReferences(PdfItem? pdfItem, PdfReference itemReference, PdfDocument document, HashSet finishedObjects)
{
// Only PdfObjects may contain further PdfReferences.
if (pdfItem is not PdfObject pdfObject)
@@ -481,10 +485,12 @@ void FinishItemReferences(PdfItem? pdfItem, PdfDocument document, HashSet finishedObjects)
+ void FinishChildReferences(PdfDictionary dictionary, PdfReference containingReference, HashSet finishedObjects)
{
+ if (dictionary.ObjectNumber == 15)
+ GetType();
+ if (dictionary.Reference is null && dictionary.ContainingReference is null)
+ dictionary.ContainingReference = containingReference;
+
// Dictionary elements are modified inside loop. Avoid "Collection was modified; enumeration operation may not execute" error occuring in net 4.7.2.
// There is no way to access KeyValuePairs via index natively to use a for loop with.
// Instead, enumerate Keys and get value via Elements[key], which shall be O(1).
@@ -514,12 +525,15 @@ void FinishChildReferences(PdfDictionary dictionary, HashSet finished
}
// Get and update item’s references.
- FinishItemReferences(item, _document, finishedObjects);
+ FinishItemReferences(item, containingReference, _document, finishedObjects);
}
}
- void FinishChildReferences(PdfArray array, HashSet finishedObjects)
+ void FinishChildReferences(PdfArray array, PdfReference containingReference, HashSet finishedObjects)
{
+ if (array.Reference is null && array.ContainingReference is null)
+ array.ContainingReference = containingReference;
+
var elements = array.Elements;
for (var i = 0; i < elements.Count; i++)
{
@@ -534,7 +548,7 @@ void FinishChildReferences(PdfArray array, HashSet finishedObjects)
}
// Get and update item’s references.
- FinishItemReferences(item, _document, finishedObjects);
+ FinishItemReferences(item, containingReference, _document, finishedObjects);
}
}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs
index c67b7416..9c2f5b23 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs
@@ -532,7 +532,8 @@ public void WriteEof(PdfDocument document, SizeType startxref)
WriteRaw(startxref.ToString(CultureInfo.InvariantCulture));
WriteRaw("\n%%EOF\n");
SizeType fileSize = (SizeType)_stream.Position;
- if (_layout == PdfWriterLayout.Verbose)
+ // position check required for incremental updates to avoid overwriting the start of the file
+ if (_layout == PdfWriterLayout.Verbose && _commentPosition > 0)
{
TimeSpan duration = DateTime.Now - document._creation;
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/enums/PdfDocumentOpenMode.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/enums/PdfDocumentOpenMode.cs
index 03f4cd94..823435dd 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/enums/PdfDocumentOpenMode.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/enums/PdfDocumentOpenMode.cs
@@ -35,5 +35,12 @@ public enum PdfDocumentOpenMode
///
[Obsolete("InformationOnly is not implemented, use Import instead.")]
InformationOnly,
+
+ ///
+ /// Comparable to but instead of overwriting the original document,
+ /// changed objects are appended to the original document when saving.
+ /// In PDF-terminology this is called an Incremental Update
+ ///
+ Append
}
}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs
index 64a6b28a..763bb865 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/BouncySigner.cs
@@ -85,5 +85,15 @@ private string GetProperDigestAlgorithm(int pdfVersion)
_ => throw new NotImplementedException(),
};
}
+
+ public Byte[] GetSignedCms(Stream documentStream, PdfDocument document)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Byte[] GetSignedCms(Byte[] range, PdfDocument document)
+ {
+ throw new NotImplementedException();
+ }
}
}
\ No newline at end of file
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureRenderer.cs
new file mode 100644
index 00000000..bae23019
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSignatureRenderer.cs
@@ -0,0 +1,48 @@
+using PdfSharp.Drawing;
+using PdfSharp.Drawing.Layout;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ internal class DefaultSignatureRenderer : ISignatureRenderer
+ {
+ public void Render(XGraphics gfx, XRect rect, PdfSignatureOptions options)
+ {
+ // if an image was provided, render only that
+ if (options.Image != null)
+ {
+ gfx.DrawImage(options.Image, 0, 0, rect.Width, rect.Height);
+ return;
+ }
+
+ var sb = new StringBuilder();
+ if (options.SignerName != null)
+ {
+ sb.AppendFormat("Signed by {0}\n", options.SignerName);
+ }
+ if (options.Location != null)
+ {
+ sb.AppendFormat("Location: {0}\n", options.Location);
+ }
+ if (options.Reason != null)
+ {
+ sb.AppendFormat("Reason: {0}\n", options.Reason);
+ }
+ sb.AppendFormat(CultureInfo.CurrentCulture, "Date: {0}", DateTime.Now);
+
+ XFont font = new XFont("Verdana", 7, XFontStyleEx.Regular);
+
+ XTextFormatter txtFormat = new XTextFormatter(gfx);
+
+ txtFormat.DrawString(sb.ToString(),
+ font,
+ new XSolidBrush(XColor.FromKnownColor(XKnownColor.Black)),
+ new XRect(0, 0, rect.Width, 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
index db8c8563..969b2341 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs
@@ -5,8 +5,13 @@
using System.IO;
#endif
#if NET6_0_OR_GREATER
-using System.Net.Http;
+using System.Formats.Asn1;
using System.Net.Http.Headers;
+#if WPF
+ using System.Net.Http;
+#endif
+#else
+using Org.BouncyCastle.Asn1;
#endif
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
@@ -20,57 +25,68 @@ public class DefaultSigner : ISigner
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; }
+ private readonly PdfSignatureOptions options;
- public DefaultSigner(X509Certificate2 Certificate)
+ public DefaultSigner(PdfSignatureOptions signatureOptions)
{
- _certificate = Certificate;
+ if (signatureOptions?.Certificate is null)
+ throw new ArgumentException("Missing certificate in signature options");
+
+ options = signatureOptions;
}
-#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)
+ public byte[] GetSignedCms(Stream documentStream, PdfDocument document)
{
- _certificate = Certificate;
- _timeStampAuthorityUri = timeStampAuthorityUri;
+ var range = new byte[documentStream.Length];
+ documentStream.Position = 0;
+ documentStream.Read(range, 0, range.Length);
+
+ return GetSignedCms(range, document);
}
-#endif
- public byte[] GetSignedCms(Stream stream, int pdfVersion)
+ public byte[] GetSignedCms(byte[] range, PdfDocument document)
{
- var range = new byte[stream.Length];
- stream.Position = 0;
- stream.Read(range, 0, range.Length);
+ var cert = options.Certificate!;
// 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());
+ var signedCms = new SignedCms(contentInfo, true);
+ var signer = new CmsSigner(cert)
+ {
+ DigestAlgorithm = new Oid("2.16.840.1.101.3.4.2.1"),
+ IncludeOption = X509IncludeOption.WholeChain,
+ };
+
+ foreach (var attr in signer.SignedAttributes)
+ {
+ signer.SignedAttributes.Remove(attr);
+ }
+
+ signer.SignedAttributes.Add(new Pkcs9SigningTime());
+
+ var signingCert = SigningCertV2(cert.RawData);
+ if (signingCert != null)
+ {
+ signer.SignedAttributes.Add(signingCert);
+ }
signedCms.ComputeSignature(signer, true);
-#if NET6_0_OR_GREATER
- if (_timeStampAuthorityUri is not null)
+ if (options.TimestampAuthorityUri is not null)
+ {
Task.Run(() => AddTimestampFromTSAAsync(signedCms)).Wait();
-#endif
+ }
var bytes = signedCms.Encode();
return bytes;
}
- public string GetName()
+ public string? GetName()
{
- return _certificate.GetNameInfo(X509NameType.SimpleName, false);
+ return options.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
@@ -81,6 +97,7 @@ private async Task AddTimestampFromTSAAsync(SignedCms signedCms)
using var cryptoProvider = new RNGCryptoServiceProvider();
cryptoProvider.GetBytes(nonce = new Byte[8]);
#endif
+#if NET6_0_OR_GREATER
// 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.
@@ -93,7 +110,7 @@ private async Task AddTimestampFromTSAAsync(SignedCms signedCms)
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);
+ var httpResponse = await client.PostAsync(options.TimestampAuthorityUri, content).ConfigureAwait(false);
// Process our response
if (!httpResponse.IsSuccessStatusCode)
@@ -101,7 +118,7 @@ private async Task AddTimestampFromTSAAsync(SignedCms signedCms)
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)
+ if (httpResponse.Content.Headers.ContentType?.MediaType != TimestampReplyContentType)
{
throw new CryptographicException("The reply from the time stamp server was in a invalid format.");
}
@@ -109,12 +126,76 @@ private async Task AddTimestampFromTSAAsync(SignedCms signedCms)
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
}
+
+ private AsnEncodedData? SigningCertV2(byte[] certRawData)
+ {
+ byte[] certHash;
+ using (var sha256 = SHA256.Create())
+ {
+ certHash = sha256.ComputeHash(certRawData);
+ }
+
+#if NET6_0_OR_GREATER
+ var writer = new AsnWriter(AsnEncodingRules.DER);
+ writer.PushSequence(); // SigningCertificateV2 SEQUENCE
+ writer.PushSequence(); // certs SEQUENCE
+
+ writer.PushSequence(); // ESSCertIDv2
+
+ // Hash algorithm identifier (SHA-256)
+ writer.PushSequence();
+ writer.WriteObjectIdentifier("2.16.840.1.101.3.4.2.1"); // SHA-256
+ writer.PopSequence();
+
+ // certHash (OCTET STRING)
+ writer.WriteOctetString(certHash);
+
+ writer.PopSequence(); // End of ESSCertIDv2
+
+ writer.PopSequence(); // End of certs
+ writer.PopSequence(); // End of SigningCertificateV2
+
+ var essAttr = new AsnEncodedData(
+ new Oid("1.2.840.113549.1.9.16.2.47"), // SigningCertificateV2
+ writer.Encode());
+
+ return essAttr;
+#else
+ // SHA-256 OID
+ DerObjectIdentifier sha256Oid = new DerObjectIdentifier("2.16.840.1.101.3.4.2.1");
+
+ // Build AlgorithmIdentifier sequence (hash algorithm)
+ Asn1EncodableVector hashAlgVector = new Asn1EncodableVector();
+ hashAlgVector.Add(sha256Oid);
+ hashAlgVector.Add(DerNull.Instance);
+ DerSequence hashAlgSeq = new DerSequence(hashAlgVector);
+
+ // Build ESSCertIDv2 sequence
+ Asn1EncodableVector essCertVector = new Asn1EncodableVector();
+ essCertVector.Add(hashAlgSeq);
+ essCertVector.Add(new DerOctetString(certHash));
+ DerSequence essCertSeq = new DerSequence(essCertVector);
+
+ // certs SEQUENCE (of ESSCertIDv2)
+ Asn1EncodableVector certsVector = new Asn1EncodableVector();
+ certsVector.Add(essCertSeq);
+ DerSequence certsSeq = new DerSequence(certsVector);
+
+ // SigningCertificateV2 SEQUENCE
+ Asn1EncodableVector signingCertV2Vector = new Asn1EncodableVector();
+ signingCertV2Vector.Add(certsSeq);
+ DerSequence signingCertV2 = new DerSequence(signingCertV2Vector);
+
+ // Wrap in AsnEncodedData (to match return type)
+ var essAttr = new AsnEncodedData(
+ new Oid("1.2.840.113549.1.9.16.2.47"),
+ signingCertV2.GetDerEncoded());
+
+ return essAttr;
#endif
+ }
}
}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISignatureRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISignatureRenderer.cs
new file mode 100644
index 00000000..03cbeaea
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISignatureRenderer.cs
@@ -0,0 +1,9 @@
+using PdfSharp.Drawing;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ public interface ISignatureRenderer
+ {
+ void Render(XGraphics gfx, XRect rect, PdfSignatureOptions options);
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs
index 8f5722c2..3529df05 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs
@@ -9,8 +9,9 @@ namespace PdfSharp.Pdf.Signatures
{
public interface ISigner
{
- byte[] GetSignedCms(Stream stream, int pdfVersion);
+ byte[] GetSignedCms(Stream documentStream, PdfDocument document);
+ byte[] GetSignedCms(byte[] range, PdfDocument document);
- string GetName();
+ 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
deleted file mode 100644
index 5bbb8461..00000000
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfArrayWithPadding.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-// 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
deleted file mode 100644
index bd14869a..00000000
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs
+++ /dev/null
@@ -1,235 +0,0 @@
-// 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
index 1867ff54..ab6a2e3b 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs
@@ -1,21 +1,81 @@
-// 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;
+using PdfSharp.Drawing;
+using System.Security.Cryptography.X509Certificates;
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
+ /// Certificate to sign the document with
+ ///
+ public X509Certificate2? Certificate { get; set; }
+
+ ///
+ /// Uri of a timestamp authority used to get a timestamp from a trusted authority
+ ///
+ public Uri? TimestampAuthorityUri { get; set; }
+
+ ///
+ /// The name of the signer.
+ /// If not set, defaults to the Subject of the provided Certificate
+ ///
+ public string? SignerName { get; set; }
+
+ ///
+ /// Contact info for the signer
+ ///
+ public string? ContactInfo { get; set; }
+
+ ///
+ /// The location where the signing took place
+ ///
+ public string? Location { get; set; }
+
+ ///
+ /// The reason for signing
+ ///
+ public string? Reason { get; set; }
+
+ ///
+ /// Intended to create a DocMPD signature entry.
+ /// Not implemented yet
+ ///
+ public bool Certify { get; set; }
+
+ ///
+ /// Rectangle of the Signature-Field's Annotation.
+ /// Specify an empty rectangle to create an invisible signature.
+ /// Note:
The rectangle's reference-point is the bottom left corner.
+ /// The recangle's location on the page is measured from the bottom-left corner.
+ ///
+ public XRect Rectangle { get; set; } = XRect.Empty;
+
+ ///
+ /// Page index for the signature's annotation, zero-based. Only needed for visible signatures.
+ ///
+ public int PageIndex { get; set; } = 0;
+
+ ///
+ /// The name of the Signature-Field.
+ /// If a field with that name already exist in the document, it will be used, otherwise it will be created.
+ /// Currently, only root-fields are supported (that is, the existing field is not allowed to be a child of another field)
+ ///
+ public string FieldName { get; set; } = "Signature1";
+
+ ///
+ /// An image to render as the Field's Annotation
+ ///
+ public XImage? Image { get; set; }
+
+ ///
+ /// A custom appearance renderer for the signature
+ ///
+ public ISignatureRenderer? Renderer { get; set; }
+
+ ///
+ /// The signer.
+ /// If not set, defaults to the DefaultSigner
///
- public int PageIndex { get; set; }
+ public ISigner? Signer { get; set; }
}
}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureValue.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureValue.cs
new file mode 100644
index 00000000..cc77b4a9
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureValue.cs
@@ -0,0 +1,400 @@
+using PdfSharp.Internal;
+using PdfSharp.Pdf.AcroForms;
+using PdfSharp.Pdf.Internal;
+using PdfSharp.Pdf.IO;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ ///
+ /// Defines the value for a
+ ///
+ public class PdfSignatureValue : PdfDictionary
+ {
+ ///
+ /// Used to report the positions of the values of and
+ /// when writing this field to a stream
+ ///
+ /// A reference to the value itself
+ /// The start-position of the value
+ /// The end-position of the value
+ internal delegate void SignatureWriteCallback(PdfSignatureValue signatureValue, SizeType start, SizeType end);
+
+ internal SignatureWriteCallback? SignatureContentsWritten;
+
+ internal SignatureWriteCallback? SignatureRangeWritten;
+
+ internal PdfSignatureValue(PdfDocument document)
+ : base(document)
+ {
+ Elements.SetName(Keys.Type, "/Sig");
+ }
+
+ internal PdfSignatureValue(PdfDictionary dict)
+ : base(dict)
+ { }
+
+ ///
+ /// (Required; inheritable) The name of the signature handler to be used for
+ /// authenticating the field’s contents, such as Adobe.PPKLite, Entrust.PPKEF,
+ /// CICI.SignIt, or VeriSign.PPKVS.
+ ///
+ public string Filter
+ {
+ get
+ {
+ var val = Elements.GetName(Keys.Filter);
+ return val;
+ }
+ set
+ {
+ Elements.SetName(Keys.Filter, value);
+ }
+ }
+
+ ///
+ /// (Optional) A name that describes the encoding of the signature value and key
+ /// information in the signature dictionary.
+ /// A PDF processor may use any handler that supports this format to validate the signature.
+ ///
+ public string SubFilter
+ {
+ get
+ {
+ var val = Elements.GetName(Keys.SubFilter);
+ return val;
+ }
+ set
+ {
+ Elements.SetName(Keys.SubFilter, value);
+ }
+ }
+
+ ///
+ /// (Optional) The name of the person or authority signing the document.
+ ///
+ public string Name
+ {
+ get
+ {
+ var val = Elements.GetString(Keys.Name);
+ return val;
+ }
+ set
+ {
+ Elements.SetString(Keys.Name, value);
+ }
+ }
+
+ ///
+ /// (Optional) The CPU host name or physical location of the signing.
+ ///
+ public string Location
+ {
+ get
+ {
+ var val = Elements.GetString(Keys.Location);
+ return val;
+ }
+ set
+ {
+ Elements.SetString(Keys.Location, value);
+ }
+ }
+
+ ///
+ /// (Optional) The reason for the signing, such as (I agree…).
+ ///
+ public string Reason
+ {
+ get
+ {
+ var val = Elements.GetString(Keys.Reason);
+ return val;
+ }
+ set
+ {
+ Elements.SetString(Keys.Reason, value);
+ }
+ }
+
+ ///
+ /// (Optional) Information provided by the signer to enable a recipient to contact the signer to verify the signature.
+ /// If SubFilter is ETSI.RFC3161, this entry should not be used and should be ignored by a PDF processor.
+ ///
+ public string ContactInfo
+ {
+ get
+ {
+ var val = Elements.GetString(Keys.ContactInfo);
+ return val;
+ }
+ set
+ {
+ Elements.SetString(Keys.ContactInfo, value);
+ }
+ }
+
+ ///
+ /// (Optional) The time of signing.
+ /// Depending on the signature handler, this may be a normal unverified computer time
+ /// or a time generated in a verifiable way from a secure time server.
+ ///
+ public DateTime SigningDate
+ {
+ get
+ {
+ var dt = Elements.GetDateTime(Keys.M, DateTime.UtcNow);
+ return dt;
+ }
+ set
+ {
+ Elements.SetDateTime(Keys.M, value);
+ }
+ }
+
+ ///
+ /// (Required) An array of pairs of integers (starting byte offset, length in bytes)
+ /// describing the exact byte range for the digest calculation.
+ /// Multiple discontinuous byte ranges may be used to describe a digest that does not include the
+ /// signature token itself.
+ ///
+ public PdfArray? ByteRange
+ {
+ get
+ {
+ return Elements.GetArray(Keys.ByteRange);
+ }
+ set
+ {
+ if (value is not null)
+ Elements.SetObject(Keys.ByteRange, value);
+ else
+ Elements.Remove(Keys.ByteRange);
+ }
+ }
+
+ ///
+ /// (Required) The encrypted signature token.
+ ///
+ public byte[] Contents
+ {
+ get
+ {
+ var str = Elements.GetString(Keys.Contents);
+ return PdfEncoders.RawEncoding.GetBytes(str);
+ }
+ set
+ {
+ var str = PdfEncoders.RawEncoding.GetString(value, 0, value.Length);
+ var hexStr = new PdfString(str, PdfStringFlags.HexLiteral);
+ Elements[Keys.Contents] = hexStr;
+ }
+ }
+
+ ///
+ /// Writes a key/value pair of this signature field dictionary.
+ ///
+ internal override void WriteDictionaryElement(PdfWriter writer, PdfName key)
+ {
+ // Don’t encrypt Contents key’s value (PDF Reference 2.0: 7.6.2, Page 71).
+ if (key.Value == Keys.Contents)
+ {
+ var item = Elements[key];
+ key.WriteObject(writer);
+ var start = writer.Position;
+ item?.WriteObject(writer);
+ var end = writer.Position;
+ writer.NewLine();
+ SignatureContentsWritten?.Invoke(this, start, end);
+
+ // TODO: handle encryption
+ //var effectiveSecurityHandler = writer.EffectiveSecurityHandler;
+ //writer.EffectiveSecurityHandler = null;
+ //base.WriteDictionaryElement(writer, key);
+ //writer.EffectiveSecurityHandler = effectiveSecurityHandler;
+ }
+ else if (key.Value == Keys.ByteRange)
+ {
+ var item = Elements[key];
+ key.WriteObject(writer);
+ var start = writer.Position;
+ item?.WriteObject(writer);
+ var end = writer.Position;
+ writer.NewLine();
+ SignatureRangeWritten?.Invoke(this, start, end);
+ }
+ else
+ base.WriteDictionaryElement(writer, key);
+ }
+
+ ///
+ /// Predefined keys of this dictionary.
+ /// PDF Reference 2.0, Chapter 12.8.1, Table 255
+ /// Consult the spec for more detailed information.
+ ///
+ public class Keys : KeysBase
+ {
+ ///
+ /// (Optional if Sig; Required if DocTimeStamp)
+ /// The type of PDF object that this dictionary describes; if present, shall be Sig for a signature dictionary or
+ /// DocTimeStamp for a timestamp signature dictionary.
+ /// The default value is: Sig.
+ ///
+ [KeyInfo(KeyType.Name | KeyType.Optional)]
+ public const string Type = "/Type";
+
+ ///
+ /// (Required; inheritable) The name of the signature handler to be used for
+ /// authenticating the field’s contents, such as Adobe.PPKLite, Entrust.PPKEF,
+ /// CICI.SignIt, or VeriSign.PPKVS.
+ ///
+ [KeyInfo(KeyType.Name | KeyType.Required)]
+ public const string Filter = "/Filter";
+
+ ///
+ /// (Optional) A name that describes the encoding of the signature value and key
+ /// information in the signature dictionary.
+ /// A PDF processor may use any handler that supports this format to validate the signature.
+ ///
+ [KeyInfo(KeyType.Name | KeyType.Optional)]
+ public const string SubFilter = "/SubFilter";
+
+ ///
+ /// (Required) An array of pairs of integers (starting byte offset, length in bytes)
+ /// describing the exact byte range for the digest calculation.
+ /// Multiple discontinuous byte ranges may be used to describe a digest that does not include the
+ /// signature token itself.
+ ///
+ [KeyInfo(KeyType.Array | KeyType.Required)]
+ public const string ByteRange = "/ByteRange";
+
+ ///
+ /// (Required) The encrypted signature token.
+ ///
+ [KeyInfo(KeyType.String | KeyType.Required)]
+ public const string Contents = "/Contents";
+
+ // Cert (deprecated ?)
+
+ ///
+ /// (Optional; PDF 1.5) An array of signature reference dictionaries
+ /// (see "Table 256 — Entries in a signature reference dictionary").
+ /// If SubFilter is ETSI.RFC3161, this entry shall not be used.
+ ///
+ [KeyInfo(KeyType.Array | KeyType.Optional)]
+ public const string Reference = "/Reference";
+
+ ///
+ /// (Optional) An array of three integers that shall specify changes to the
+ /// document that have been made between the previous signature and this
+ /// signature: in this order, the number of pages altered, the number of fields altered,
+ /// and the number of fields filled in.
+ /// The ordering of signatures shall be determined by the value of ByteRange.
+ /// Since each signature results in an incremental save, later signatures have a
+ /// greater length value.
+ /// If SubFilter is ETSI.RFC3161, this entry shall not be used.
+ ///
+ [KeyInfo(KeyType.Array | KeyType.Optional)]
+ public const string Changes = "/Changes";
+
+ ///
+ /// (Optional) The name of the person or authority signing the document.
+ ///
+ [KeyInfo(KeyType.TextString | KeyType.Optional)]
+ public const string Name = "/Name";
+
+ ///
+ /// (Optional) The time of signing. Depending on the signature handler, this
+ /// may be a normal unverified computer time or a time generated in a verifiable
+ /// way from a secure time server.
+ ///
+ [KeyInfo(KeyType.Date | KeyType.Optional)]
+ public const string M = "/M";
+
+ ///
+ /// (Optional) The CPU host name or physical location of the signing.
+ ///
+ [KeyInfo(KeyType.TextString | KeyType.Optional)]
+ public const string Location = "/Location";
+
+ ///
+ /// (Optional) The reason for the signing, such as (I agree…).
+ ///
+ [KeyInfo(KeyType.TextString | KeyType.Optional)]
+ public const string Reason = "/Reason";
+
+ ///
+ /// (Optional) Information provided by the signer to enable a recipient to contact the signer to verify the signature.
+ /// If SubFilter is ETSI.RFC3161, this entry should not be used and should be ignored by a PDF processor.
+ ///
+ [KeyInfo(KeyType.TextString | KeyType.Optional)]
+ public const string ContactInfo = "/ContactInfo";
+
+ ///
+ /// (Optional; deprecated in PDF 2.0) The version of the signature handler that
+ /// was used to create the signature.
+ /// (PDF 1.5) This entry shall not be used, and the information shall be stored in the Prop_Build dictionary.
+ ///
+ [KeyInfo(KeyType.Integer | KeyType.Optional)]
+ public const string R = "/R";
+
+ ///
+ /// (Optional; PDF 1.5) The version of the signature dictionary format.
+ /// It corresponds to the usage of the signature dictionary in the context of the value of SubFilter.
+ /// The value is 1 if the Reference dictionary shall be considered critical to the validation of the signature.
+ /// If SubFilter is ETSI.RFC3161, this V value shall be 0 (possibly by default).
+ /// Default value: 0.
+ ///
+ [KeyInfo(KeyType.Integer | KeyType.Optional)]
+ public const string V = "/V";
+
+ ///
+ /// (Optional; PDF 1.5) A dictionary that may be used by a signature handler to
+ /// record information that captures the state of the computer environment used
+ /// for signing, such as the name of the handler used to create the signature,
+ /// software build date, version, and operating system.
+ /// The use of this dictionary is defined by Adobe PDF Signature Build Dictionary
+ /// Specification, which provides implementation guidelines.
+ ///
+ [KeyInfo(KeyType.Dictionary | KeyType.Optional)]
+ public const string Prop_Build = "/Prop_Build";
+
+ ///
+ /// (Optional; PDF 1.5) The number of seconds since the signer was last
+ /// authenticated, used in claims of signature repudiation.
+ /// It should be omitted if the value is unknown.
+ /// If SubFilter is ETSI.RFC3161, this entry shall not be used.
+ ///
+ [KeyInfo(KeyType.Integer | KeyType.Optional)]
+ public const string Prop_AuthTime = "/Prop_AuthTime";
+
+ ///
+ /// (Optional; PDF 1.5) The method that shall be used to authenticate the signer,
+ /// used in claims of signature repudiation.
+ /// Valid values shall be PIN, Password, and Fingerprint.
+ ///If SubFilter is ETSI.RFC3161, this entry shall not be used.
+ ///
+ [KeyInfo(KeyType.Name | KeyType.Optional)]
+ public const string Prop_AuthType = "/Prop_AuthType";
+
+ ///
+ /// Gets the KeysMeta for these keys.
+ ///
+ internal static DictionaryMeta Meta => _meta ??= CreateMeta(typeof(Keys));
+
+ static DictionaryMeta? _meta;
+ }
+
+ ///
+ /// Gets the KeysMeta of this dictionary type.
+ ///
+ internal override DictionaryMeta Meta => Keys.Meta;
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSigner.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSigner.cs
new file mode 100644
index 00000000..a1ba6241
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSigner.cs
@@ -0,0 +1,257 @@
+using PdfSharp.Drawing;
+using PdfSharp.Pdf.AcroForms;
+using PdfSharp.Pdf.Annotations;
+using PdfSharp.Pdf.Internal;
+using PdfSharp.Pdf.IO;
+using System.Security.Cryptography.X509Certificates;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ ///
+ /// Utility class for signing PDF-documents
+ ///
+ public class PdfSigner
+ {
+ private readonly Stream inputStream;
+
+ private readonly PdfDocument document;
+
+ private readonly ISigner signer;
+
+ private readonly PdfSignatureOptions options;
+
+ ///
+ /// Create new new instance for the specified document and with the specified options
+ ///
+ /// Stream specifying the document to sign. Must be readable and seekable
+ /// The options that spefify, how the signing is performed
+ ///
+ ///
+ public PdfSigner(Stream documentStream, PdfSignatureOptions signatureOptions)
+ {
+ if (documentStream is null)
+ throw new ArgumentNullException(nameof(documentStream));
+ if (!documentStream.CanRead || !documentStream.CanSeek)
+ throw new ArgumentException("Invalid stream. Must be readable and seekable", nameof(documentStream));
+ options = signatureOptions ?? throw new ArgumentNullException(nameof(signatureOptions));
+
+ if (options.Certificate is null)
+ throw new ArgumentException("A certificate is required to sign");
+ if (options.PageIndex < 0)
+ throw new ArgumentException("Page index cannot be less than zero");
+
+ inputStream = documentStream;
+ // apply signature as an incremental update
+ document = PdfReader.Open(documentStream, PdfDocumentOpenMode.Append);
+ signer = signatureOptions.Signer ?? new DefaultSigner(signatureOptions);
+ }
+
+ ///
+ /// Signs the document
+ ///
+ /// A stream containing the signed document. Stream-position is 0
+ public Stream Sign()
+ {
+ var signatureValue = CreateSignatureValue();
+ var signatureField = GetOrCreateSignatureField(signatureValue);
+ RenderSignatureAppearance(signatureField);
+
+ var finalDocumentLength = 0L;
+ var contentStart = 0L;
+ var contentEnd = 0L;
+ var rangeStart = 0L;
+ var rangeEnd = 0L;
+ var extraSpace = 0;
+ signatureValue.SignatureContentsWritten = (sigValue, start, end) =>
+ {
+ contentStart = start;
+ contentEnd = end;
+ };
+ signatureValue.SignatureRangeWritten = (sigValue, start, end) =>
+ {
+ rangeStart = start;
+ rangeEnd = end;
+ };
+ document.AfterSave = (writer) =>
+ {
+ extraSpace = writer.Layout == PdfWriterLayout.Verbose ? 1 : 0;
+ };
+ var ms = new MemoryStream();
+ // copy original document to output-stream
+ inputStream.Seek(0, SeekOrigin.Begin);
+ inputStream.CopyTo(ms);
+ // append incremental update
+ document.Save(ms);
+
+ finalDocumentLength = ms.Length;
+
+ contentStart += extraSpace;
+ rangeStart += extraSpace;
+
+ // write new ByteRange array
+ var rangeArrayValue = string.Format(CultureInfo.InvariantCulture, "[0 {0} {1} {2}]",
+ contentStart, contentEnd, finalDocumentLength - contentEnd);
+ Debug.Assert(rangeArrayValue.Length <= rangeEnd - rangeStart);
+ rangeArrayValue = rangeArrayValue.PadRight((int)(rangeEnd - rangeStart), ' ');
+ ms.Seek(rangeStart, SeekOrigin.Begin);
+ var writeBytes = PdfEncoders.RawEncoding.GetBytes(rangeArrayValue);
+ ms.Write(writeBytes, 0, writeBytes.Length);
+
+ // concat the ranges before and after the content-string
+ var lengthToSign = contentStart + finalDocumentLength - contentEnd;
+ var toSign = new byte[lengthToSign];
+ ms.Seek(0, SeekOrigin.Begin);
+ ms.Read(toSign, 0, (int)contentStart);
+ ms.Seek(contentEnd, SeekOrigin.Begin);
+ ms.Read(toSign, (int)contentStart, (int)(finalDocumentLength - contentEnd));
+
+ // do the signing
+ var signatureData = signer.GetSignedCms(toSign, document);
+
+ // move past the '<'
+ ms.Seek(contentStart + 1, SeekOrigin.Begin);
+ // convert signature to string
+ var signHexString = PdfEncoders.ToHexStringLiteral(signatureData, false, false, null);
+ writeBytes = new byte[signHexString.Length - 2];
+ // exclude '<' and '>' from hex-string and overwrite fake value
+ PdfEncoders.RawEncoding.GetBytes(signHexString, 1, signHexString.Length - 2, writeBytes, 0);
+ ms.Write(writeBytes, 0, writeBytes.Length);
+
+ ms.Position = 0;
+
+ document.Dispose();
+
+ return ms;
+ }
+
+ private int GetContentLength()
+ {
+ return signer.GetSignedCms(new MemoryStream(new byte[] { 0 }), document).Length + 10;
+ }
+
+ private PdfSignatureField GetOrCreateSignatureField(PdfSignatureValue value)
+ {
+ var acroForm = document.GetOrCreateAcroForm();
+ var fieldList = GetExistingFields();
+ // if a field with the specified name exist, use that
+ // Note: only root-level fields are currently supported
+ var fieldWithName = fieldList.FirstOrDefault(f => f.Name == options.FieldName);
+ if (fieldWithName != null && !(fieldWithName is PdfSignatureField))
+ throw new ArgumentException(
+ $"Field '{options.FieldName}' exist in document, but it is not a Signature-Field ({fieldWithName.GetType().Name})");
+
+ var isNewField = false;
+ var signatureField = fieldList.FirstOrDefault(f =>
+ f is PdfSignatureField && f.Name == options.FieldName) as PdfSignatureField;
+ if (signatureField == null)
+ {
+ // field does not exist, create new one
+ signatureField = new PdfSignatureField(document)
+ {
+ Name = options.FieldName
+ };
+ document.IrefTable.Add(signatureField);
+ acroForm.Fields.Elements.Add(signatureField);
+ isNewField = true;
+ }
+ // Flags: SignaturesExit + AppendOnly
+ acroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3);
+
+ signatureField.Value = value;
+ signatureField.Elements.SetInteger(PdfAcroField.Keys.Ff, (int)PdfAcroFieldFlags.NoExport);
+ signatureField.Elements.SetName(PdfAnnotation.Keys.Type, "/Annot");
+ signatureField.Elements.SetName(PdfAnnotation.Keys.Subtype, "/Widget");
+ if (isNewField)
+ {
+ signatureField.Elements.SetReference("/P", document.Pages[options.PageIndex]);
+ signatureField.Elements.Add(PdfAnnotation.Keys.Rect, new PdfRectangle(options.Rectangle));
+ }
+ 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 if (!annotations.Elements.Contains(signatureField))
+ annotations.Elements.Add(signatureField);
+
+ return signatureField;
+ }
+
+ private PdfSignatureValue CreateSignatureValue()
+ {
+ var signatureDict = new PdfSignatureValue(document);
+ document.IrefTable.Add(signatureDict);
+
+ var contentLength = GetContentLength();
+ var content = Enumerable.Repeat(0, contentLength).ToArray();
+ signatureDict.Contents = content;
+ signatureDict.Filter = "/Adobe.PPKLite";
+ signatureDict.SubFilter = "/adbe.pkcs7.detached";
+ signatureDict.SigningDate = DateTime.Now;
+
+ var documentLength = inputStream.Length;
+ // fill with large enough fake values. we will overwrite these later
+ var byteRange = new PdfArray(document, new PdfLongInteger(0), new PdfLongInteger(documentLength),
+ new PdfLongInteger(documentLength), new PdfLongInteger(documentLength));
+ signatureDict.ByteRange = byteRange;
+ if (options.Reason is not null)
+ signatureDict.Reason = options.Reason;
+ if (options.Location is not null)
+ signatureDict.Location = options.Location;
+ if (options.ContactInfo is not null)
+ signatureDict.ContactInfo = options.ContactInfo;
+
+ return signatureDict;
+ }
+
+ private void RenderSignatureAppearance(PdfSignatureField signatureField)
+ {
+ if (string.IsNullOrEmpty(options.SignerName))
+ options.SignerName = signer.GetName() ?? "unknown";
+
+ XRect annotRect;
+ var rect = signatureField.Elements.GetRectangle(PdfAnnotation.Keys.Rect);
+ if (rect.IsEmpty)
+ {
+ // XRect.IsEmpty returns false even when width and height are zero ??
+ if (options.Rectangle.Width <= 0 || options.Rectangle.Height <= 0)
+ return;
+
+ annotRect = options.Rectangle;
+ signatureField.Elements.SetRectangle(PdfAnnotation.Keys.Rect, new PdfRectangle(annotRect));
+ }
+ else
+ annotRect = rect.ToXRect();
+
+ var form = new XForm(document, annotRect.Size);
+ var gfx = XGraphics.FromForm(form);
+ var renderer = options.Renderer ?? new DefaultSignatureRenderer();
+ renderer.Render(gfx, annotRect, options);
+ form.DrawingFinished();
+ form.PdfRenderer?.Close();
+
+ if (signatureField.Elements[PdfAnnotation.Keys.AP] is not PdfDictionary ap)
+ {
+ ap = new PdfDictionary(document);
+ signatureField.Elements.Add(PdfAnnotation.Keys.AP, ap);
+ }
+ ap.Elements.SetReference("/N", form.PdfForm);
+ }
+
+ ///
+ /// Gets the list of existing root-fields of this document
+ ///
+ ///
+ private IEnumerable GetExistingFields()
+ {
+ var fields = new List();
+ if (document.AcroForm?.Fields != null)
+ {
+ for (var i = 0; i < document.AcroForm.Fields.Count; i++)
+ {
+ var field = document.AcroForm.Fields[i];
+ fields.Add(field);
+ }
+ }
+ return fields;
+ }
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs
deleted file mode 100644
index 84dbcbdb..00000000
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/RangedStream.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-// 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/PdfArray.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfArray.cs
index 3c75d163..9a3ecfbe 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfArray.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfArray.cs
@@ -14,6 +14,40 @@ namespace PdfSharp.Pdf
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")]
public class PdfArray : PdfObject, IEnumerable
{
+ ///
+ /// Gets a value that determines whether the object was modified after loading.
+ ///
+ internal bool IsModified { get; private set; }
+
+ ///
+ /// Sets the modified-status of this object
+ ///
+ ///
+ internal void SetModified(bool modified)
+ {
+ if (!Owner.IsAppending || !Owner.IrefTable.FullyLoaded)
+ return;
+
+ IsModified = modified;
+ if (modified)
+ {
+ Owner.IrefTable.MarkAsModified(Reference ?? ContainingReference);
+ }
+ else
+ {
+ var iref = Reference ?? ContainingReference;
+ if (iref != null)
+ Owner.IrefTable.ModifiedObjects.Remove(iref.ObjectID);
+ }
+ }
+
+ ///
+ /// Gets or sets the to the object that is the nearest indirect parent of this object
+ /// (that is, the object that encapsulates the current object)
+ /// This is only meaningful for direct objects embedded in other objects
+ ///
+ internal PdfReference? ContainingReference { get; set; }
+
///
/// Initializes a new instance of the class.
///
@@ -380,6 +414,7 @@ public PdfItem this[int index]
if (value == null!)
throw new ArgumentNullException(nameof(value));
_elements[index] = value;
+ _ownerArray?.SetModified(true);
}
}
@@ -389,6 +424,7 @@ public PdfItem this[int index]
public void RemoveAt(int index)
{
_elements.RemoveAt(index);
+ _ownerArray?.SetModified(true);
}
///
@@ -396,7 +432,10 @@ public void RemoveAt(int index)
///
public bool Remove(PdfItem item)
{
- return _elements.Remove(item);
+ var removed = _elements.Remove(item);
+ if (removed)
+ _ownerArray?.SetModified(true);
+ return removed;
}
///
@@ -405,6 +444,7 @@ public bool Remove(PdfItem item)
public void Insert(int index, PdfItem value)
{
_elements.Insert(index, value);
+ _ownerArray?.SetModified(true);
}
///
@@ -420,6 +460,8 @@ public bool Contains(PdfItem value)
///
public void Clear()
{
+ if (_elements.Count > 0)
+ _ownerArray?.SetModified(true);
_elements.Clear();
}
@@ -444,6 +486,7 @@ public void Add(PdfItem value)
_elements.Add(obj.Reference!);
else
_elements.Add(value);
+ _ownerArray?.SetModified(true);
}
///
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDictionary.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDictionary.cs
index 7075b141..3fa2d803 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDictionary.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDictionary.cs
@@ -44,6 +44,40 @@ public class PdfDictionary : PdfObject, IEnumerable
+ /// Gets a value that determines whether the object was modified after loading.
+ ///
+ internal bool IsModified { get; private set; }
+
+ ///
+ /// Sets the modified-status of this object
+ ///
+ ///
+ internal void SetModified(bool modified)
+ {
+ if (!Owner.IsAppending || !Owner.IrefTable.FullyLoaded)
+ return;
+
+ IsModified = modified;
+ if (modified)
+ {
+ Owner.IrefTable.MarkAsModified(Reference ?? ContainingReference);
+ }
+ else
+ {
+ var iref = Reference ?? ContainingReference;
+ if (iref != null)
+ Owner.IrefTable.ModifiedObjects.Remove(iref.ObjectID);
+ }
+ }
+
+ ///
+ /// Gets or sets the to the object that is the nearest indirect parent of this object
+ /// (that is, the object that encapsulates the current object)
+ /// This is only meaningful for direct objects embedded in other objects
+ ///
+ internal PdfReference? ContainingReference { get; set; }
+
///
/// Initializes a new instance of the class.
///
@@ -542,7 +576,7 @@ public void SetName(string key, string value)
/// If the value does not exist, the function returns an empty rectangle.
/// If the value is not convertible, the function throws an InvalidCastException.
///
- public PdfRectangle GetRectangle(string key, bool create)
+ public PdfRectangle GetRectangle(string key, bool create = false)
{
var value = new PdfRectangle();
var obj = this[key];
@@ -559,21 +593,14 @@ public PdfRectangle GetRectangle(string key, bool create)
{
value = new PdfRectangle(array.Elements.GetReal(0), array.Elements.GetReal(1),
array.Elements.GetReal(2), array.Elements.GetReal(3));
- this[key] = value;
+ // ignore modification as we're just changing the type
+ Owner.Owner.IrefTable.IgnoreModify(() => this[key] = value);
}
else
value = (PdfRectangle)obj;
return value;
}
- ///
- /// Converts the specified value to PdfRectangle.
- /// If the value does not exist, the function returns an empty rectangle.
- /// If the value is not convertible, the function throws an InvalidCastException.
- ///
- public PdfRectangle GetRectangle(string key)
- => GetRectangle(key, false);
-
///
/// Sets the entry to a direct rectangle value, represented by an array with four values.
///
@@ -864,6 +891,8 @@ PdfArray CreateArray(Type type, PdfArray? oldArray)
Debug.Assert(ctorInfo != null, "No appropriate constructor found for type: " + type.Name);
//array = ctorInfo.Invoke(new object[] { oldArray }) as PdfArray;
array = ctorInfo.Invoke(new object[] { oldArray }) as PdfArray;
+ if (array != null && oldArray.ContainingReference != null)
+ array.ContainingReference = oldArray.ContainingReference;
}
return array ?? NRT.ThrowOnNull();
#else
@@ -925,6 +954,8 @@ PdfDictionary CreateDictionary(Type type, PdfDictionary? oldDictionary)
null, new[] { typeof(PdfDictionary) }, null);
Debug.Assert(ctorInfo != null, "No appropriate constructor found for type: " + type.Name);
dict = ctorInfo.Invoke(new object[] { oldDictionary }) as PdfDictionary;
+ if (dict != null && oldDictionary.ContainingReference != null)
+ dict.ContainingReference = oldDictionary.ContainingReference;
}
return dict ?? NRT.ThrowOnNull();
#else
@@ -1145,6 +1176,7 @@ public PdfItem? this[string key]
if (value is PdfObject { IsIndirect: true } obj)
value = obj.Reference;
_elements[key] = value;
+ _ownerDictionary.SetModified(true);
}
}
@@ -1171,6 +1203,7 @@ public PdfItem? this[PdfName key]
if (value is PdfObject { IsIndirect: true } obj)
value = obj.Reference;
_elements[key.Value] = value;
+ _ownerDictionary.SetModified(true);
}
}
@@ -1179,7 +1212,10 @@ public PdfItem? this[PdfName key]
///
public bool Remove(string key)
{
- return _elements.Remove(key);
+ var removed = _elements.Remove(key);
+ if (removed)
+ _ownerDictionary.SetModified(true);
+ return removed;
}
///
@@ -1220,6 +1256,8 @@ public bool Contains(KeyValuePair item)
///
public void Clear()
{
+ if (_elements.Count > 0)
+ _ownerDictionary.SetModified(true);
_elements.Clear();
}
@@ -1239,6 +1277,7 @@ public void Add(string key, PdfItem? value)
value = obj.Reference;
_elements.Add(key, value);
+ _ownerDictionary.SetModified(true);
}
///
@@ -1432,6 +1471,7 @@ internal void ChangeOwner(PdfDictionary dict)
// Set owners stream to this.
_ownerDictionary.Stream = this;
+ //_ownerDictionary.SetModified(true); // needed ?
}
///
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs
index 5bf645bf..ffdadeec 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs
@@ -19,24 +19,19 @@
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) => { };
+ ///
+ /// Used to report that saving the document has been finished.
+ ///
+ ///
+ internal delegate void AfterSaveCallback(PdfWriter writer);
+
+ internal AfterSaveCallback? AfterSave;
#if DEBUG_
static PdfDocument()
{
@@ -192,7 +187,7 @@ static string NewName()
static int _nameCount;
//internal bool CanModify => true;
- internal bool CanModify => _openMode == PdfDocumentOpenMode.Modify;
+ internal bool CanModify => _openMode == PdfDocumentOpenMode.Modify || _openMode == PdfDocumentOpenMode.Append;
///
/// Closes this instance.
@@ -324,9 +319,7 @@ void DoSave(PdfWriter writer)
{
PdfSharpLogHost.Logger.PdfDocumentSaved(Name);
- BeforeSave(this, EventArgs.Empty);
-
- if (_pages == null || _pages.Count == 0)
+ if (Pages == null || Pages.Count == 0)
{
if (OutStream != null)
{
@@ -341,7 +334,10 @@ void DoSave(PdfWriter writer)
// HACK: Remove XRefTrailer
if (Trailer is PdfCrossReferenceStream crossReferenceStream)
{
- Trailer = new PdfTrailer(crossReferenceStream);
+ Trailer = new PdfTrailer(crossReferenceStream)
+ {
+ Position = crossReferenceStream.Position
+ };
}
var effectiveSecurityHandler = _securitySettings?.EffectiveSecurityHandler;
@@ -356,6 +352,16 @@ void DoSave(PdfWriter writer)
else
Trailer.Elements.Remove(PdfTrailer.Keys.Encrypt);
+ if (_openMode == PdfDocumentOpenMode.Append)
+ {
+ // Prepare used fonts.
+ _fontTable?.PrepareForSave();
+ // Let catalog do the rest.
+ Catalog.PrepareForSave();
+ SaveIncrementally(writer);
+ return;
+ }
+
PrepareForSave();
effectiveSecurityHandler?.PrepareForWriting();
@@ -390,15 +396,96 @@ void DoSave(PdfWriter writer)
{
if (writer != null!)
{
- AfterSave(this, new PdfDocumentEventArgs(writer));
-
writer.Stream.Flush();
+ AfterSave?.Invoke(writer);
// DO NOT CLOSE WRITER HERE
}
_state |= DocumentState.Saved;
}
}
+ ///
+ /// Saves changes made to the document as an incremental update.
+ /// If the document was not modified, this method does nothing.
+ ///
+ ///
+ /// Note that when updating a document that is linearized, the document will no longer be linearized
+ /// as the update changes the file-length and may invalidate the existing hint-tables.
+ /// Acrobat(Reader) may complain that the file is damaged and need to be repaired.
+ ///
+ ///
+ internal void SaveIncrementally(PdfWriter writer)
+ {
+ if (IrefTable.ModifiedObjects.Count == 0)
+ return;
+
+ _securitySettings?.EffectiveSecurityHandler?.PrepareForWriting();
+
+ writer.Stream.Seek(0, SeekOrigin.End);
+ // there may be the line "%%EOF" at the end of the file, make sure we start on a new line
+ writer.WriteRaw('\n');
+
+ var objects = new List>(IrefTable.ModifiedObjects.Count);
+
+ foreach (var iref in IrefTable.ModifiedObjects.Values.OrderBy(it => it.ObjectNumber))
+ {
+ iref.Position = writer.Position;
+ iref.Value.WriteObject(writer);
+ objects.Add(new(iref.ObjectNumber, iref.GenerationNumber, iref.Position));
+ }
+ SizeType startxref = writer.Position;
+
+ writer.WriteRaw("xref\n");
+
+ writer.WriteRaw(Invariant($"0 1\n"));
+ writer.WriteRaw(Invariant($"{0:0000000000} {65535:00000} f \n"));
+
+ // build chunks of consecutively numbered objects
+ var startIndex = 0;
+ var chunk = new List>(objects.Count);
+ do
+ {
+ chunk.Clear();
+ chunk.AddRange(objects.Skip(startIndex).TakeWhile((it, idx) =>
+ {
+ return idx == 0 || it.Item1 == objects[idx + startIndex - 1].Item1 + 1;
+ }));
+ startIndex += chunk.Count;
+
+ writer.WriteRaw(Invariant($"{chunk[0].Item1} {chunk.Count}\n"));
+ foreach (var element in chunk)
+ {
+ // Acrobat is very pedantic; it must be exactly 20 bytes per line.
+ writer.WriteRaw(Invariant($"{element.Item3:0000000000} {element.Item2:00000} n \n"));
+ }
+ } while (startIndex < objects.Count);
+
+ var newTrailer = new PdfTrailer(this);
+ // copy all entries from the previous trailer, as specified in the spec
+ foreach (var key in Trailer.Elements.Keys)
+ {
+ // skip these as we provide new values for them
+ if (key == PdfTrailer.Keys.Prev || key == PdfTrailer.Keys.Size)
+ continue;
+ newTrailer.Elements[key] = Trailer.Elements[key];
+ if (key == PdfTrailer.Keys.ID)
+ {
+ // first id stays the same, second is updated for each update
+ var id1 = Trailer.GetDocumentID(0);
+ var docID = Guid.NewGuid().ToByteArray();
+ string id2 = PdfEncoders.RawEncoding.GetString(docID, 0, docID.Length);
+ newTrailer.Elements.SetObject(PdfTrailer.Keys.ID, new PdfArray(this,
+ new PdfString(id1, PdfStringFlags.HexLiteral), new PdfString(id2, PdfStringFlags.HexLiteral)));
+ }
+ }
+ newTrailer.Size = IrefTable.MaxObjectNumber + 1;
+ newTrailer.Elements.SetObject(PdfTrailer.Keys.Prev, new PdfLongIntegerObject(this, Trailer.Position));
+
+ writer.WriteRaw("trailer\n");
+ newTrailer.WriteObject(writer);
+ writer.WriteEof(this, startxref);
+ }
+
///
/// Dispatches PrepareForSave to the objects that need it.
///
@@ -581,7 +668,12 @@ internal DocumentHandle Handle
///
/// Returns a value indicating whether the document is read only or can be modified.
///
- public bool IsReadOnly => (_openMode != PdfDocumentOpenMode.Modify);
+ public bool IsReadOnly => (_openMode != PdfDocumentOpenMode.Modify && _openMode != PdfDocumentOpenMode.Append);
+
+ ///
+ /// Gets a value indicating whether the document was opened in append-mode
+ ///
+ public bool IsAppending => _openMode == PdfDocumentOpenMode.Append;
internal Exception DocumentNotImported()
{
@@ -662,7 +754,22 @@ public PdfPageMode PageMode
///
/// Get the AcroForm dictionary.
///
- public PdfAcroForm AcroForm => Catalog.AcroForm;
+ public PdfAcroForm? AcroForm => Catalog.AcroForm;
+
+ ///
+ /// Gets the existing or creates a new one, if there is no in the current document
+ ///
+ /// The associated with this document
+ public PdfAcroForm GetOrCreateAcroForm()
+ {
+ var form = AcroForm;
+ if (form == null)
+ {
+ form = new PdfAcroForm(this);
+ Catalog.AcroForm = form;
+ }
+ return form;
+ }
///
/// Gets or sets the default language of the document.
@@ -839,7 +946,7 @@ public void AddEmbeddedFile(string name, Stream stream)
///
public void Flatten()
{
- for (int idx = 0; idx < AcroForm.Fields.Count; idx++)
+ for (int idx = 0; idx < AcroForm?.Fields.Count; idx++)
{
AcroForm.Fields[idx].ReadOnly = true;
}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs
index 1f167908..af685f28 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs
@@ -191,7 +191,7 @@ public TrimMargins TrimMargins
///
public PdfRectangle MediaBox
{
- get => Elements.GetRectangle(InheritablePageKeys.MediaBox, true);
+ get => Elements.GetRectangle(InheritablePageKeys.MediaBox);
set => Elements.SetRectangle(InheritablePageKeys.MediaBox, value);
}
@@ -200,7 +200,7 @@ public PdfRectangle MediaBox
///
public PdfRectangle CropBox
{
- get => Elements.GetRectangle(InheritablePageKeys.CropBox, true);
+ get => Elements.GetRectangle(InheritablePageKeys.CropBox);
set => Elements.SetRectangle(InheritablePageKeys.CropBox, value);
}
@@ -209,7 +209,7 @@ public PdfRectangle CropBox
///
public PdfRectangle BleedBox
{
- get => Elements.GetRectangle(Keys.BleedBox, true);
+ get => Elements.GetRectangle(Keys.BleedBox);
set => Elements.SetRectangle(Keys.BleedBox, value);
}
@@ -218,7 +218,7 @@ public PdfRectangle BleedBox
///
public PdfRectangle ArtBox
{
- get => Elements.GetRectangle(Keys.ArtBox, true);
+ get => Elements.GetRectangle(Keys.ArtBox);
set => Elements.SetRectangle(Keys.ArtBox, value);
}
@@ -227,7 +227,7 @@ public PdfRectangle ArtBox
///
public PdfRectangle TrimBox
{
- get => Elements.GetRectangle(Keys.TrimBox, true);
+ get => Elements.GetRectangle(Keys.TrimBox);
set => Elements.SetRectangle(Keys.TrimBox, value);
}
@@ -631,7 +631,7 @@ internal override void WriteObject(PdfWriter writer)
if (TransparencyUsed && !Elements.ContainsKey(Keys.Group) &&
_document.Options.ColorMode != PdfColorMode.Undefined)
{
- var group = new PdfDictionary();
+ var group = new PdfDictionary(Owner);
Elements["/Group"] = group;
if (_document.Options.ColorMode != PdfColorMode.Cmyk)
group.Elements.SetName("/CS", "/DeviceRGB");
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs
index 75c924a2..9d83a5ce 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfString.cs
@@ -420,21 +420,7 @@ 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 217bb259..7a39fa94 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj
@@ -59,6 +59,7 @@
+
diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/WriterTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/WriterTests.cs
index 2b03e397..ad72ce60 100644
--- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/WriterTests.cs
+++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/IO/WriterTests.cs
@@ -2,15 +2,17 @@
// See the LICENSE file in the solution root for more information.
using FluentAssertions;
-using PdfSharp.Diagnostics;
using PdfSharp.Drawing;
-using PdfSharp.Fonts;
-using PdfSharp.Pdf;
using PdfSharp.Pdf.IO;
using PdfSharp.Quality;
-using PdfSharp.Snippets.Font;
-using PdfSharp.TestHelper;
+using PdfSharp.Pdf.Signatures;
using Xunit;
+using System.Security.Cryptography.X509Certificates;
+using PdfSharp.Pdf.AcroForms;
+
+#if WPF
+using System.IO;
+#endif
namespace PdfSharp.Tests.IO
{
@@ -29,5 +31,109 @@ public void Write_import_file()
Action save = () => doc.Save(filename);
save.Should().Throw();
}
+
+ [Fact]
+ public void Append_To_File()
+ {
+ var sourceFile = IOUtility.GetAssetsPath("archives/grammar-by-example/GBE/ReferencePDFs/WPF 1.31/Table-Layout.pdf")!;
+ var targetFile = Path.Combine(Path.GetTempPath(), "AA-Append.pdf");
+ File.Copy(sourceFile, targetFile, true);
+
+ using var fs = File.Open(targetFile, FileMode.Open, FileAccess.ReadWrite);
+ var doc = PdfReader.Open(fs, PdfDocumentOpenMode.Append);
+ var numPages = doc.PageCount;
+ var numContentsPerPage = new List();
+ foreach (var page in doc.Pages)
+ {
+ // remember count of existing contents
+ numContentsPerPage.Add(page.Contents.Elements.Count);
+ // add new content
+ using var gfx = XGraphics.FromPdfPage(page);
+ gfx.DrawString("I was added", new XFont("Arial", 16), new XSolidBrush(XColors.Red), 40, 40);
+ }
+
+ doc.Save(fs, true);
+
+ // verify that the new content is picked up
+ var idx = 0;
+ doc = PdfReader.Open(targetFile, PdfDocumentOpenMode.Import);
+ doc.PageCount.Should().Be(numPages);
+ foreach (var page in doc.Pages)
+ {
+ var count = page.Contents.Elements.Count;
+ count.Should().Be(numContentsPerPage[idx] + 1);
+ idx++;
+ }
+ }
+
+ [Fact]
+ public void Sign()
+ {
+ /**
+ Easy way to create a self-signed certificate for testing.
+ Put the following code in a file called "makecert.ps1" and execute it from PowerShell (tested with 7.4.2).
+ (Adapt the variables to your liking)
+
+ $date = Get-Date
+ # mark valid for 10 years
+ $date = $date.AddYears(10)
+ # define some variables
+ $issuedTo = "FooBar"
+ $subject = "CN=" + $issuedTo
+ $friendlyName = $issuedTo
+ $exportFileName = $issuedTo + ".pfx"
+ # create certificate and add to personal store
+ $cert = New-SelfSignedCertificate -Type Custom -Subject $subject -KeyUsage DigitalSignature,NonRepudiation -KeyUsageProperty Sign -FriendlyName $friendlyName -CertStoreLocation "Cert:\CurrentUser\My" -NotAfter $date
+ # specify password for exported certificate
+ $password = ConvertTo-SecureString -String "1234" -Force -AsPlainText
+ # export to current folder in pfx format
+ Export-PfxCertificate -Cert $cert -FilePath $exportFileName -Password $password
+ */
+ var cert = new X509Certificate2(@"C:\Data\packdat.pfx", "1234");
+ // sign 2 times
+ for (var i = 1; i <= 2; i++)
+ {
+ var options = new PdfSignatureOptions
+ {
+ Certificate = cert,
+ FieldName = "Signature-" + Guid.NewGuid().ToString("N"),
+ PageIndex = 0,
+ Rectangle = new XRect(120 * i, 40, 100, 60),
+ Location = "My PC",
+ Reason = "Approving Rev #" + i,
+ // Signature appearances can also consist of an image (Rectangle should be adapted to image's aspect ratio)
+ //Image = XImage.FromFile(@"C:\Data\stamp.png")
+ };
+
+ string sourceFile;
+ string targetFile;
+ // first signature
+ if (i == 1)
+ {
+ sourceFile = IOUtility.GetAssetsPath("archives/grammar-by-example/GBE/ReferencePDFs/WPF 1.31/Table-Layout.pdf")!;
+ targetFile = Path.Combine(Path.GetTempPath(), "AA-Signed.pdf");
+ }
+ // second signature
+ else
+ {
+ sourceFile = Path.Combine(Path.GetTempPath(), "AA-Signed.pdf");
+ targetFile = Path.Combine(Path.GetTempPath(), "AA-Signed-2.pdf");
+ }
+ File.Copy(sourceFile, targetFile, true);
+
+ using var fs = File.Open(targetFile, FileMode.Open, FileAccess.ReadWrite);
+ var signer = new PdfSigner(fs, options);
+ var resultStream = signer.Sign();
+ // overwrite input document
+ fs.Seek(0, SeekOrigin.Begin);
+ resultStream.CopyTo(fs);
+ }
+
+ using var finalDoc = PdfReader.Open(Path.Combine(Path.GetTempPath(), "AA-Signed-2.pdf"), PdfDocumentOpenMode.Modify);
+ var acroForm = finalDoc.AcroForm;
+ acroForm.Should().NotBeNull();
+ var signatureFields = acroForm!.GetAllFields().OfType().ToList();
+ signatureFields.Count.Should().Be(2);
+ }
}
}