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); + } } }