diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index fe9f2272..50ade2c5 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -11,6 +11,7 @@
+
@@ -99,6 +100,9 @@
+
+
+
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj
index 51dc232e..b8983300 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj
+++ b/src/foundation/src/PDFsharp/src/PdfSharp-gdi/PdfSharp-gdi.csproj
@@ -319,7 +319,16 @@
-
+
+
+
+
+
+
+
+
+
+
@@ -419,7 +428,12 @@
-
+
+
+
+
+
+
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj
index e32b4515..eec83d62 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj
+++ b/src/foundation/src/PDFsharp/src/PdfSharp-wpf/PdfSharp-wpf.csproj
@@ -318,7 +318,16 @@
-
+
+
+
+
+
+
+
+
+
+
@@ -426,8 +435,9 @@
-
+
+
+
+
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfAcroField.cs
index 79afe41b..bc425422 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)
+ { }
+
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..0596d61b 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,33 @@ 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];
+ 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 d1f84691..4f15fe08 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.AcroForms/PdfSignatureField.cs
@@ -2,6 +2,7 @@
// See the LICENSE file in the solution root for more information.
using PdfSharp.Pdf.IO;
+using PdfSharp.Pdf.Signatures;
namespace PdfSharp.Pdf.AcroForms
{
@@ -15,32 +16,44 @@ public sealed class PdfSignatureField : PdfAcroField
///
internal PdfSignatureField(PdfDocument document)
: base(document)
- { }
+ {
+ Elements[PdfAcroField.Keys.FT] = new PdfName("/Sig");
+ }
internal PdfSignatureField(PdfDictionary dict)
: base(dict)
{ }
///
- /// Writes a key/value pair of this signature field dictionary.
+ /// Gets or sets the value for this field
///
- internal override void WriteDictionaryElement(PdfWriter writer, PdfName key)
+ public new PdfSignatureValue? Value
{
- // Don’t encrypt Contents key’s value (PDF Reference 2.0: 7.6.2, Page 71).
- if (key.Value == Keys.Contents)
+ 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
{
- var effectiveSecurityHandler = writer.EffectiveSecurityHandler;
- writer.EffectiveSecurityHandler = null;
- base.WriteDictionaryElement(writer, key);
- writer.EffectiveSecurityHandler = effectiveSecurityHandler;
+ if (value is not null)
+ Elements.SetReference(PdfAcroField.Keys.V, value);
+ else
+ Elements.Remove(PdfAcroField.Keys.V);
}
- else
- base.WriteDictionaryElement(writer, key);
}
+ PdfSignatureValue? sigValue;
///
/// 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..2eaa522d 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;
@@ -152,14 +152,31 @@ public PdfNameDictionary Names
///
/// Gets 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;
}
+ internal set
+ {
+ if (value != null)
+ {
+ if (!value.IsIndirect)
+ throw new InvalidOperationException("Setting the AcroForm requires an indirect object");
+ 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..5ecb9f60 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
}
///
@@ -135,7 +135,8 @@ internal override void WriteObject(PdfWriter writer)
//Elements["/Filter"] = new PdfName("/FlateDecode");
Elements.SetName("/Filter", "/FlateDecode");
}
- Elements.SetInteger("/Length", Stream.Length);
+ // avoid adding this to "ModifiedObjects" while saving (caused "CollectionWasModified"-Exception)
+ Owner.IrefTable.IgnoreModify(() => Elements.SetInteger("/Length", Stream.Length));
}
base.WriteObject(writer);
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..7c0e6a3c 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,8 @@ 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 +65,8 @@ 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..98dfcc4d 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfCrossReferenceTable.cs
@@ -27,11 +27,52 @@ public PdfCrossReferenceTable(PdfDocument document)
public Dictionary ObjectTable = [];
///
+ /// Used to collect modified objects for incremental updates
+ ///
+ internal Dictionary ModifiedObjects = [];
+
+ ///
+ /// Used to collect deleted objects for incremental updates
+ ///
+ internal HashSet DeletedObjects = [];
+
/// 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; }
+ internal bool ReadyForModification { get; set; }
+
+ internal void MarkAsModified(PdfReference? pdfReference)
+ {
+ if (pdfReference == null || !ReadyForModification)
+ 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 = ReadyForModification;
+ ReadyForModification = false;
+ try
+ {
+ action();
+ }
+ finally
+ {
+ ReadyForModification = prev;
+ }
+ }
+
///
/// Adds a cross-reference entry to the table. Used when parsing the trailer.
///
@@ -58,6 +99,12 @@ public void Add(PdfReference iref)
#endif
}
ObjectTable.Add(iref.ObjectID, iref);
+
+ // new objects must be treated like modified objects for incremental updates
+ if (ReadyForModification && _document.IsAppending)
+ {
+ ModifiedObjects[iref.ObjectID] = iref;
+ }
}
///
@@ -77,6 +124,12 @@ 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 for incremental updates
+ if (ReadyForModification && _document.IsAppending)
+ {
+ ModifiedObjects[value.ObjectID] = value.ReferenceNotNull;
+ }
}
///
@@ -231,8 +284,7 @@ internal int Compact()
ids.Add(iref.ObjectNumber, 0);
}
- //
- Dictionary refs = new Dictionary();
+ var refs = new Dictionary();
foreach (PdfReference iref in irefs)
{
refs.Add(iref, 0);
@@ -264,7 +316,10 @@ internal int Compact()
#endif
MaxObjectNumber = 0;
+ // remember list of currently known object-IDs
+ var allObjectIds = new HashSet(ObjectTable.Keys);
ObjectTable.Clear();
+ DeletedObjects.Clear();
foreach (PdfReference iref in irefs)
{
// This if is needed for corrupt PDF files from the wild.
@@ -276,6 +331,12 @@ internal int Compact()
MaxObjectNumber = Math.Max(MaxObjectNumber, iref.ObjectNumber);
}
}
+ // if initial object-ID is not in the list of final objects, mark as deleted
+ foreach (var objId in allObjectIds)
+ {
+ if (!ObjectTable.ContainsKey(objId))
+ DeletedObjects.Add(objId);
+ }
//CheckConsistence();
removed -= ObjectTable.Count;
return removed;
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..86c3bd64 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.ReadyForModification = true;
}
///
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..5e90ac37 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;
}
@@ -1351,8 +1357,8 @@ internal PdfTrailer ReadTrailer()
// Reference: 3.4.7 Cross-Reference Streams / Page 93
// 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);
+ // The parsed integer is the object ID of the cross-reference stream.
+ 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".
@@ -1433,7 +1436,10 @@ PdfTrailer ReadXRefStream(PdfCrossReferenceTable xrefTable)
ReadSymbol(Symbol.BeginDictionary);
var objectID = new PdfObjectID(number, generation);
- var xrefStream = new PdfCrossReferenceStream(_document);
+ var xrefStream = new PdfCrossReferenceStream(_document)
+ {
+ Position = xrefStart
+ };
ReadDictionary(xrefStream, false);
ReadSymbol(Symbol.BeginStream);
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..ad906e90 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfReader.cs
@@ -2,6 +2,7 @@
// See the LICENSE file in the solution root for more information.
using Microsoft.Extensions.Logging;
+using Microsoft.VisualBasic;
using PdfSharp.Internal;
using PdfSharp.Logging;
using PdfSharp.Pdf.Advanced;
@@ -345,7 +346,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 +450,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 +486,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 +526,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 +549,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..55394b5e 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
@@ -33,7 +33,11 @@ public enum PdfDocumentOpenMode
/// call the Info property at the imported document. This option is very fast and needs less memory
/// and is e.g. useful for browsing information about a collection of PDF documents in a user interface.
///
- [Obsolete("InformationOnly is not implemented, use Import instead.")]
- InformationOnly,
+ InformationOnly, // TODO: not yet implemented
+
+ ///
+ /// Comparable to but changes are appended to the document when saving
+ ///
+ Append
}
}
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..5b0e01c5
--- /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.Signer != null)
+ {
+ sb.AppendFormat("Signed by {0}\n", options.Signer);
+ }
+ 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
new file mode 100644
index 00000000..8e8173c5
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DefaultSigner.cs
@@ -0,0 +1,108 @@
+// PDFsharp - A .NET library for processing PDF
+// See the LICENSE file in the solution root for more information.
+
+#if NET6_0_OR_GREATER
+
+using System.Net.Http.Headers;
+#if WPF
+ using System.Net.Http;
+#endif
+#endif
+using System.Security.Cryptography;
+using System.Security.Cryptography.Pkcs;
+using System.Security.Cryptography.X509Certificates;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ public class DefaultSigner : ISigner
+ {
+ private static readonly Oid SignatureTimeStampOin = new Oid("1.2.840.113549.1.9.16.2.14");
+ private static readonly string TimestampQueryContentType = "application/timestamp-query";
+ private static readonly string TimestampReplyContentType = "application/timestamp-reply";
+
+ private readonly PdfSignatureOptions options;
+
+ public DefaultSigner(PdfSignatureOptions signatureOptions)
+ {
+ if (signatureOptions?.Certificate is null)
+ throw new ArgumentException("Missing certificate in signature options");
+
+ options = signatureOptions;
+ }
+
+ public byte[] GetSignedCms(Stream documentStream, PdfDocument document)
+ {
+ var range = new byte[documentStream.Length];
+ documentStream.Position = 0;
+ documentStream.Read(range, 0, range.Length);
+
+ return GetSignedCms(range, document);
+ }
+
+ public byte[] GetSignedCms(byte[] range, PdfDocument document)
+ {
+ // Sign the byte range
+ var contentInfo = new ContentInfo(range);
+ var signedCms = new SignedCms(contentInfo, true);
+ var signer = new CmsSigner(options.Certificate)/* { IncludeOption = X509IncludeOption.WholeChain }*/;
+ signer.UnsignedAttributes.Add(new Pkcs9SigningTime());
+
+ signedCms.ComputeSignature(signer, true);
+
+ if (options.TimestampAuthorityUri is not null)
+ Task.Run(() => AddTimestampFromTSAAsync(signedCms)).Wait();
+
+ var bytes = signedCms.Encode();
+
+ return bytes;
+ }
+
+ public string? GetName()
+ {
+ return options.Certificate?.GetNameInfo(X509NameType.SimpleName, false);
+ }
+
+ private async Task AddTimestampFromTSAAsync(SignedCms signedCms)
+ {
+ // Generate our nonce to identify the pair request-response
+ byte[] nonce = new byte[8];
+#if NET6_0_OR_GREATER
+ nonce = RandomNumberGenerator.GetBytes(8);
+#else
+ using var cryptoProvider = new RNGCryptoServiceProvider();
+ cryptoProvider.GetBytes(nonce = new Byte[8]);
+#endif
+#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.
+ var request = Rfc3161TimestampRequest.CreateFromSignerInfo(
+ newSignerInfo,
+ HashAlgorithmName.SHA256,
+ requestSignerCertificates: true, // ask TSA to embed its signing certificate in the timestamp token
+ nonce: nonce);
+
+ var client = new HttpClient();
+ var content = new ReadOnlyMemoryContent(request.Encode());
+ content.Headers.ContentType = new MediaTypeHeaderValue(TimestampQueryContentType);
+ var httpResponse = await client.PostAsync(options.TimestampAuthorityUri, content).ConfigureAwait(false);
+
+ // Process our response
+ if (!httpResponse.IsSuccessStatusCode)
+ {
+ throw new CryptographicException(
+ $"There was a error from the timestamp authority. It responded with {httpResponse.StatusCode} {(int)httpResponse.StatusCode}: {httpResponse.Content}");
+ }
+ if (httpResponse.Content.Headers.ContentType?.MediaType != TimestampReplyContentType)
+ {
+ throw new CryptographicException("The reply from the time stamp server was in a invalid format.");
+ }
+ var data = await httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
+ var timestampToken = request.ProcessResponse(data, out _);
+
+ // The RFC3161 sign certificate is separate to the contents that was signed, we need to add it to the unsigned attributes.
+ newSignerInfo.AddUnsignedAttribute(new AsnEncodedData(SignatureTimeStampOin, timestampToken.AsSignedCms().Encode()));
+#endif
+ }
+ }
+}
\ No newline at end of file
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..0dd7c40e
--- /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
new file mode 100644
index 00000000..08bb848a
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/ISigner.cs
@@ -0,0 +1,11 @@
+
+namespace PdfSharp.Pdf.Signatures
+{
+ public interface ISigner
+ {
+ byte[] GetSignedCms(Stream documentStream, PdfDocument document);
+ byte[] GetSignedCms(byte[] range, PdfDocument document);
+
+ string? GetName();
+ }
+}
diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs
new file mode 100644
index 00000000..e70be425
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureOptions.cs
@@ -0,0 +1,73 @@
+using PdfSharp.Drawing;
+using System.Security.Cryptography.X509Certificates;
+
+namespace PdfSharp.Pdf.Signatures
+{
+ public class PdfSignatureOptions
+ {
+ ///
+ /// 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? Signer { 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; }
+
+ ///
+ /// Create a certification signature. Not yet implemented.
+ /// See chapter 12.8 (Digital Signatures) in Pdf Reference (DocMDP / FieldMDP)
+ ///
+ public bool Certify { get; set; }
+
+ ///
+ /// Rectangle of the Signature-Field's Annotation.
+ /// Specify an empty rectangle to create an invisible signature.
+ ///
+ public XRect Rectangle { get; set; } = XRect.Empty;
+
+ ///
+ /// Page index, 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; }
+ }
+}
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..721f4e47
--- /dev/null
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureValue.cs
@@ -0,0 +1,399 @@
+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);
+
+ //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..87c55b5a
--- /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;
+ document = PdfReader.Open(documentStream, PdfDocumentOpenMode.Append);
+ 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.Signer))
+ options.Signer = 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 might be null here (in GDI build)
+ 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/PdfArray.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfArray.cs
index 3c75d163..eae75f96 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.ReadyForModification)
+ 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.
///
@@ -379,7 +413,32 @@ public PdfItem this[int index]
{
if (value == null!)
throw new ArgumentNullException(nameof(value));
- _elements[index] = value;
+ if (index < 0 || index >= _elements.Count)
+ throw new ArgumentOutOfRangeException(nameof(index), $"Index ({index}) must be greater than or equal to zero and less than {_elements.Count}.");
+
+ // no need to set ContainingReference for indirect objcets
+ if (!(value is PdfObject { IsIndirect: true }))
+ {
+ if (value is PdfDictionary dict)
+ dict.ContainingReference = _ownerArray?.Reference ?? _ownerArray?.ContainingReference;
+ else if (value is PdfArray ary)
+ ary.ContainingReference = _ownerArray?.Reference ?? _ownerArray?.ContainingReference;
+ }
+ // TODO: use reference of indirect objects as in PdfDictionary ?
+ //if (value is PdfObject { IsIndirect: true } obj)
+ // value = obj.Reference!;
+
+ // minor optimization
+ if (_ownerArray != null && _ownerArray.Owner.IsAppending && _ownerArray.Owner.IrefTable.ReadyForModification)
+ {
+ var prevItem = this[index];
+ _elements[index] = value;
+ // incremental updates: do not mark as modified if we don't have to
+ if (value != prevItem)
+ _ownerArray?.SetModified(true);
+ }
+ else
+ _elements[index] = value;
}
}
@@ -389,6 +448,7 @@ public PdfItem this[int index]
public void RemoveAt(int index)
{
_elements.RemoveAt(index);
+ _ownerArray?.SetModified(true);
}
///
@@ -396,7 +456,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 +468,7 @@ public bool Remove(PdfItem item)
public void Insert(int index, PdfItem value)
{
_elements.Insert(index, value);
+ _ownerArray?.SetModified(true);
}
///
@@ -420,6 +484,8 @@ public bool Contains(PdfItem value)
///
public void Clear()
{
+ if (_elements.Count > 0)
+ _ownerArray?.SetModified(true);
_elements.Clear();
}
@@ -444,6 +510,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..ac993680 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.ReadyForModification)
+ 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,15 +576,13 @@ 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)
{
var value = new PdfRectangle();
var obj = this[key];
if (obj == null)
{
- if (create)
- this[key] = value = new PdfRectangle();
- return value;
+ return PdfRectangle.Empty;
}
if (obj is PdfReference reference)
obj = reference.Value;
@@ -559,26 +591,19 @@ 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.
///
public void SetRectangle(string key, PdfRectangle rect)
- => _elements[key] = rect;
+ => this[key] = rect;
/// Converts the specified value to XMatrix.
/// If the value does not exist, the function returns an identity matrix.
@@ -619,7 +644,7 @@ public XMatrix GetMatrix(string key)
/// Sets the entry to a direct matrix value, represented by an array with six values.
///
public void SetMatrix(string key, XMatrix matrix)
- => _elements[key] = PdfLiteral.FromMatrix(matrix);
+ => this[key] = PdfLiteral.FromMatrix(matrix);
///
/// Converts the specified value to DateTime.
@@ -667,7 +692,7 @@ public DateTime GetDateTime(string key, DateTime defaultValue)
/// Sets the entry to a direct datetime value.
///
public void SetDateTime(string key, DateTime value)
- => _elements[key] = new PdfDate(value);
+ => this[key] = new PdfDate(value);
internal int GetEnumFromName(string key, object defaultValue, bool create)
{
@@ -694,7 +719,7 @@ internal void SetEnumAsName(string key, object value)
{
if (value is not Enum)
throw new ArgumentException(nameof(value));
- _elements[key] = new PdfName("/" + value);
+ this[key] = new PdfName("/" + value);
}
///
@@ -864,6 +889,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 +952,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
@@ -1019,7 +1048,7 @@ public void SetValue(string key, PdfItem value)
"You try to set an indirect object directly into a dictionary.");
// HACK?
- _elements[key] = value;
+ this[key] = value;
}
///
@@ -1144,7 +1173,21 @@ public PdfItem? this[string key]
#endif
if (value is PdfObject { IsIndirect: true } obj)
value = obj.Reference;
- _elements[key] = value;
+ else if (value is PdfDictionary dict)
+ dict.ContainingReference = _ownerDictionary.Reference ?? _ownerDictionary.ContainingReference;
+ else if (value is PdfArray ary)
+ ary.ContainingReference = _ownerDictionary.Reference ?? _ownerDictionary.ContainingReference;
+ // minor optimzation
+ if (_ownerDictionary.Owner.IsAppending && _ownerDictionary.Owner.IrefTable.ReadyForModification)
+ {
+ var prevItem = _elements.ContainsKey(key) ? this[key] : null;
+ _elements[key] = value;
+ // incremental updates: do not mark as modified if we don't have to
+ if (value != prevItem)
+ _ownerDictionary.SetModified(true);
+ }
+ else
+ _elements[key] = value;
}
}
@@ -1167,10 +1210,22 @@ public PdfItem? this[PdfName key]
throw new ArgumentException("A dictionary with stream cannot be a direct value.");
}
#endif
-
if (value is PdfObject { IsIndirect: true } obj)
value = obj.Reference;
- _elements[key.Value] = value;
+ else if (value is PdfDictionary vdict)
+ vdict.ContainingReference = _ownerDictionary.Reference ?? _ownerDictionary.ContainingReference;
+ else if (value is PdfArray varray)
+ varray.ContainingReference = _ownerDictionary.Reference ?? _ownerDictionary.ContainingReference;
+ // minor optimzation
+ if (_ownerDictionary.Owner.IsAppending && _ownerDictionary.Owner.IrefTable.ReadyForModification)
+ {
+ var prevItem = _elements.ContainsKey(key.Value) ? this[key.Value] : null;
+ // incremental updates: do not mark as modified if we don't have to
+ if (value != prevItem)
+ _ownerDictionary.SetModified(true);
+ }
+ else
+ _elements[key.Value] = value;
}
}
@@ -1179,7 +1234,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 +1278,8 @@ public bool Contains(KeyValuePair item)
///
public void Clear()
{
+ if (_elements.Count > 0)
+ _ownerDictionary.SetModified(true);
_elements.Clear();
}
@@ -1237,8 +1297,13 @@ public void Add(string key, PdfItem? value)
// If object is indirect automatically convert value to reference.
if (value is PdfObject { IsIndirect: true } obj)
value = obj.Reference;
+ else if (value is PdfDictionary dict)
+ dict.ContainingReference = _ownerDictionary.Reference ?? _ownerDictionary.ContainingReference;
+ else if (value is PdfArray ary)
+ ary.ContainingReference = _ownerDictionary.Reference ?? _ownerDictionary.ContainingReference;
_elements.Add(key, value);
+ _ownerDictionary.SetModified(true);
}
///
@@ -1432,6 +1497,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 9b3ad75d..12a8369d 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs
@@ -25,6 +25,14 @@ namespace PdfSharp.Pdf
[DebuggerDisplay("(Name={" + nameof(Name) + "})")] // A name makes debugging easier
public sealed class PdfDocument : PdfObject, IDisposable
{
+ ///
+ /// Used to report that saving the document has been finished.
+ ///
+ ///
+ internal delegate void AfterSaveCallback(PdfWriter writer);
+
+ internal AfterSaveCallback? AfterSave;
+
#if DEBUG_
static PdfDocument()
{
@@ -180,7 +188,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.
@@ -312,7 +320,7 @@ void DoSave(PdfWriter writer)
{
PdfSharpLogHost.Logger.PdfDocumentSaved(Name);
- if (_pages == null || _pages.Count == 0)
+ if (Pages == null || Pages.Count == 0)
{
if (OutStream != null)
{
@@ -327,7 +335,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;
@@ -342,6 +353,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();
@@ -377,12 +398,117 @@ void DoSave(PdfWriter writer)
if (writer != null!)
{
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.
+ ///
+ ///
+ /// See chapters 7.5.6 and H.7 in PdfReference (1.7) for details on incremental updates
+ /// 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)
+ {
+ IrefTable.Compact();
+
+ // nothing changed or deleted ? nothing to do here
+ if (IrefTable.ModifiedObjects.Count == 0 && IrefTable.DeletedObjects.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 xrefEntries = new List>(IrefTable.ModifiedObjects.Count + IrefTable.DeletedObjects.Count);
+ // write updated objects
+ foreach (var iref in IrefTable.ModifiedObjects.Values)
+ {
+ iref.Position = writer.Position;
+ iref.Value.WriteObject(writer);
+ xrefEntries.Add(new(iref.ObjectNumber, iref.GenerationNumber, iref.Position, 'n'));
+ }
+ // add deleted objects to the mix
+ var deleteObjectsOrdered = new List(IrefTable.DeletedObjects);
+ deleteObjectsOrdered.Sort((a, b) => a.ObjectNumber.CompareTo(b.ObjectNumber));
+ for (var i = 0; i < deleteObjectsOrdered.Count; i++)
+ {
+ var objId = deleteObjectsOrdered[i];
+ // "position"-value of deleted objects are the object-numbers of the next deleted object
+ var nextDeletedObjectNumber = i + 1 < deleteObjectsOrdered.Count
+ ? deleteObjectsOrdered[i + 1].ObjectNumber : 0;
+ // increment generation number of deleted objects
+ xrefEntries.Add(new(objId.ObjectNumber, objId.GenerationNumber + 1, nextDeletedObjectNumber, 'f'));
+ }
+ // sort by object number
+ xrefEntries.Sort((a, b) => a.Item1.CompareTo(b.Item1));
+ // if we have deleted objetcs, use first one as head for new xref-table
+ var nextFreeObject = deleteObjectsOrdered.Count > 0 ? deleteObjectsOrdered[0].ObjectNumber : 0;
+
+ SizeType startxref = writer.Position;
+
+ writer.WriteRaw("xref\n");
+
+ writer.WriteRaw(Invariant($"0 1\n"));
+ writer.WriteRaw(Invariant($"{nextFreeObject:0000000000} {65535:00000} f \n"));
+
+ // build chunks of consecutively numbered objects
+ var startIndex = 0;
+ var chunk = new List>(xrefEntries.Count);
+ do
+ {
+ chunk.Clear();
+ chunk.AddRange(xrefEntries.Skip(startIndex).TakeWhile((it, idx) =>
+ {
+ return idx == 0 || it.Item1 == xrefEntries[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} {element.Item4} \n"));
+ }
+ } while (startIndex < xrefEntries.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.
///
@@ -565,7 +691,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()
{
@@ -646,7 +777,26 @@ 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);
+ IrefTable.Add(new PdfReference(form));
+ if (form.Reference != null)
+ form.Reference.Document = this;
+ Catalog.AcroForm = form;
+ }
+ return form;
+ }
+
///
/// Gets or sets the default language of the document.
@@ -823,7 +973,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/PdfSharp.csproj b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj
index cefdd52c..c656b314 100644
--- a/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj
+++ b/src/foundation/src/PDFsharp/src/PdfSharp/PdfSharp.csproj
@@ -58,4 +58,9 @@
+
+
+
+
+
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..890cd005 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,16 @@
// 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 System.IO;
+using PdfSharp.Pdf.AcroForms;
+using PdfSharp.Pdf.Signatures;
using Xunit;
+using System.Security.Cryptography.X509Certificates;
namespace PdfSharp.Tests.IO
{
@@ -29,5 +30,136 @@ 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 c = page.Contents.Elements.Count;
+ c.Should().Be(numContentsPerPage[idx] + 1);
+ idx++;
+ }
+ }
+
+ [Fact]
+ public void Update_With_Deletion()
+ {
+ // create input file
+ Append_To_File();
+
+ var sourceFile = Path.Combine(Path.GetTempPath(), "AA-Append.pdf");
+ var targetFile = Path.Combine(Path.GetTempPath(), "AA-Append-Delete.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.Pages.Count;
+
+ var firstPage = doc.Pages[0];
+ // page will not be deleted, because it is referenced by other objects (Outlines)
+ // delete contentes as well, so we have at least SOME "f"-entries in the new xref-table
+ firstPage.Contents.Elements.Clear();
+ doc.Pages.Remove(firstPage);
+
+ doc.Save(fs, true);
+
+ doc = PdfReader.Open(targetFile, PdfDocumentOpenMode.Import);
+ doc.PageCount.Should().Be(numPages - 1);
+ // new xref-table was checked manually (opened in notepad)
+ }
+
+ [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);
+ }
}
}