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 cd70d0e6..efa87689 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs @@ -23,6 +23,14 @@ public PdfWriter(Stream pdfStream, PdfDocument document, PdfStandardSecurityHand Layout = document.Options.Layout; } + /// + /// When a PdfWriter was created for saving to a file path, this + /// contains the full path. The constructor currently accepts only + /// a Stream, so the owner (PdfDocument.SaveAsync(path)) will set this. + /// This is used as metadata for possible incremental append operations. + /// + internal string? FullPath { get; set; } + public void Close(bool closeUnderlyingStream) { if (closeUnderlyingStream) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureOptions.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureOptions.cs index c51bc051..8afcf91e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureOptions.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureOptions.cs @@ -45,5 +45,11 @@ public class DigitalSignatureOptions() /// The page index, zero-based, of the page showing the signature. /// public int PageIndex { get; init; } + + /// + /// When true, the PDF will be signed incrementally instead of re-saving the whole file. + /// This preserves existing signatures. + /// + public bool AppendSignature { get; set; } = false; } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs index f1f4b752..7d91329c 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -62,13 +62,19 @@ public static DigitalSignatureHandler ForDocument(PdfDocument document, IDigital internal async Task ComputeSignatureAndRange(PdfWriter writer) { - var (rangedStreamToSign, byteRangeArray) = GetRangeToSignAndByteRangeArray(writer.Stream); + if (Options.AppendSignature && writer.FullPath != null) + { + await ComputeIncrementalSignatureAsync(writer.Stream).ConfigureAwait(continueOnCapturedContext: false); + return; + } + + (RangedStream rangedStreamToSign, PdfArray byteRangeArray) = GetRangeToSignAndByteRangeArray(writer.Stream); Debug.Assert(_signatureFieldByteRangePlaceholder != null); _signatureFieldByteRangePlaceholder.WriteActualObject(byteRangeArray, writer); // Computing signature from document’s digest. - var signature = await Signer.GetSignatureAsync(rangedStreamToSign).ConfigureAwait(false); + byte[] signature = await Signer.GetSignatureAsync(rangedStreamToSign).ConfigureAwait(false); Debug.Assert(_placeholderItem != null); int expectedLength = _placeholderItem.Size; @@ -84,12 +90,62 @@ internal async Task ComputeSignatureAndRange(PdfWriter writer) writer.Write(PdfEncoders.RawEncoding.GetBytes(FormatHex(signature))); // Fill up the allocated placeholder. Signature is sometimes considered invalid if there are spaces after '>'. - for (int x = signature.Length; x < expectedLength; ++x) + for (int i = signature.Length; i < expectedLength; i++) writer.WriteRaw("00"); writer.WriteRaw('>'); } + /// + /// Writes the computed digital signature into the existing PDF stream during an incremental signing operation. + /// + /// + /// + internal async Task ComputeIncrementalSignatureAsync(Stream targetStream) + { + if (targetStream == null) + throw new ArgumentNullException("targetStream"); + + if (!targetStream.CanRead || !targetStream.CanSeek || !targetStream.CanWrite) + throw new InvalidOperationException("Target stream must be readable, seekable and writable for incremental signature."); + + (RangedStream rangedStreamToSign, PdfArray byteRangeArray) = GetRangeToSignAndByteRangeArray(targetStream); + + PdfWriter writer = new PdfWriter(targetStream, Document, null) + { + Layout = PdfWriterLayout.Compact + }; + + Debug.Assert(_signatureFieldByteRangePlaceholder != null); + _signatureFieldByteRangePlaceholder.WriteActualObject(byteRangeArray, writer); + + byte[] signature = await Signer.GetSignatureAsync(rangedStreamToSign).ConfigureAwait(false); + + Debug.Assert(_placeholderItem != null); + int expectedLength = _placeholderItem.Size; + if (signature.Length > expectedLength) + throw new Exception($"The actual digest length {signature.Length} is larger than the approximation made {expectedLength}. Not enough room in the placeholder to fit the signature."); + + targetStream.Position = _placeholderItem.StartPosition; + targetStream.WriteByte(60); + + string text = PdfEncoders.ToHexStringLiteral(signature, unicode: false, prefix: false, null); + int num = text.Length - 2; + byte[] array2 = new byte[num]; + PdfEncoders.RawEncoding.GetBytes(text, 1, num, array2, 0); + + targetStream.Write(array2, 0, array2.Length); + + for (int i = signature.Length; i < expectedLength; i++) + { + byte[] bytes = PdfEncoders.RawEncoding.GetBytes("00"); + targetStream.Write(bytes, 0, bytes.Length); + } + + targetStream.WriteByte(62); + targetStream.Flush(); + } + string FormatHex(byte[] bytes) // ...use RawEncoder { #if NET6_0_OR_GREATER @@ -112,7 +168,7 @@ string FormatHex(byte[] bytes) // ...use RawEncoder /// (RangedStream rangedStream, PdfArray byteRangeArray) GetRangeToSignAndByteRangeArray(Stream stream) { - Debug.Assert( _placeholderItem !=null, nameof(_placeholderItem) + " must not be null here."); + Debug.Assert(_placeholderItem != null, nameof(_placeholderItem) + " must not be null here."); SizeType firstRangeOffset = 0; SizeType firstRangeLength = _placeholderItem.StartPosition; @@ -143,38 +199,95 @@ internal async Task AddSignatureComponentsAsync() if (Options.PageIndex >= Document.PageCount) throw new ArgumentOutOfRangeException($"Signature page doesn't exist, specified page was {Options.PageIndex + 1} but document has only {Document.PageCount} page(s)."); - var signatureSize = await Signer.GetSignatureSizeAsync().ConfigureAwait(false); - _placeholderItem = new(signatureSize); + int signatureSize = await Signer.GetSignatureSizeAsync().ConfigureAwait(false); + _placeholderItem = new PdfSignaturePlaceholderItem(signatureSize); _signatureFieldByteRangePlaceholder = new PdfPlaceholderObject(ByteRangePlaceholderLength); - var signatureDictionary = GetSignatureDictionary(_placeholderItem, _signatureFieldByteRangePlaceholder); - var signatureField = GetSignatureField(signatureDictionary); - - var annotations = Document.Pages[Options.PageIndex].Elements.GetArray(PdfPage.Keys.Annots); - if (annotations == null) - Document.Pages[Options.PageIndex].Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField)); + PdfSignature2 signatureDictionary = GetSignatureDictionary(_placeholderItem, _signatureFieldByteRangePlaceholder); + if (Options.AppendSignature) + { + AddIncrementalSignatureComponents(signatureDictionary); + return; + } else - annotations.Elements.Add(signatureField); + { + PdfSignatureField signatureField = GetSignatureField(signatureDictionary); - // acroform + PdfArray? annotations = Document.Pages[Options.PageIndex].Elements.GetArray(PdfPage.Keys.Annots); + if (annotations == null) + Document.Pages[Options.PageIndex].Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField)); + else + annotations.Elements.Add(signatureField); - var catalog = Document.Catalog; + PdfCatalog catalog = Document.Catalog; - if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null) - catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document)); + SetAcroFormsAndSixFlagsOnCatalog(catalog); + + if (catalog.AcroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) == null) + catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, new PdfAcroField.PdfAcroFieldCollection(new PdfArray())); + catalog.AcroForm.Fields.Elements.Add(signatureField); + } + } + + /// + /// Adds the required AcroForm and annotation entries for an incremental digital signature. + /// + /// + internal void AddIncrementalSignatureComponents(PdfSignature2 signatureDictionary) + { + PdfCatalog catalog = Document.Catalog; + SetAcroFormsAndSixFlagsOnCatalog(catalog); + + PdfAcroForm acroForm = catalog.AcroForm; + int valueOrDefault = (acroForm.Fields?.Elements?.Count).GetValueOrDefault(); + PdfDictionary pdfDictionary = GetDocumentDictionary(signatureDictionary, valueOrDefault); - if (!catalog.AcroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags)) - catalog.AcroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3)); + Document.Internals.AddObject(pdfDictionary); + + Debug.Assert(pdfDictionary.Reference != null); + if (acroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) is PdfArray pdfArray) + pdfArray.Elements.Add(pdfDictionary.Reference); else { - var sigFlagVersion = catalog.AcroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags); - if (sigFlagVersion < 3) - catalog.AcroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + PdfArray pdfArray2 = new PdfArray(Document); + pdfArray2.Elements.Add(pdfDictionary.Reference); + acroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, pdfArray2); } - if (catalog.AcroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) == null) - catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, new PdfAcroField.PdfAcroFieldCollection(new PdfArray())); - catalog.AcroForm.Fields.Elements.Add(signatureField); + if (!acroForm.Elements.ContainsKey(PdfAcroForm.Keys.DR)) + acroForm.Elements.Add(PdfAcroForm.Keys.DR, new PdfDictionary(Document)); + + if (!acroForm.Elements.ContainsKey(PdfAcroForm.Keys.DA)) + acroForm.Elements.Add(PdfAcroForm.Keys.DA, new PdfString("/Helv 0 Tf 0 g")); + + } + + PdfDictionary GetDocumentDictionary(PdfSignature2 signatureDictionary, int valueOrDefault) + { + PdfDictionary pdfDictionary = new PdfDictionary(Document); + + pdfDictionary.Elements.Add(PdfAcroField.Keys.FT, new PdfName("/Sig")); + pdfDictionary.Elements.Add(PdfAcroField.Keys.T, new PdfString($"Signature{valueOrDefault + 1}")); + pdfDictionary.Elements.Add(PdfAcroField.Keys.V, signatureDictionary); + pdfDictionary.Elements.Add(PdfAcroField.Keys.Ff, new PdfInteger(4)); + pdfDictionary.Elements.Add(PdfSignatureField.Keys.Type, new PdfName("/Annot")); + pdfDictionary.Elements.Add("/Subtype", new PdfName("/Widget")); + pdfDictionary.Elements.Add("/P", Document.Pages[Options.PageIndex].Reference); + pdfDictionary.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle)); + + return pdfDictionary; + } + + PdfCatalog SetAcroFormsAndSixFlagsOnCatalog(PdfCatalog catalog) + { + if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null) + catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document)); + PdfAcroForm acroForm = catalog.AcroForm; + if (!acroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags)) + acroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3)); + else if (acroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags) < 3) + acroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + return catalog; } PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) @@ -201,13 +314,18 @@ PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) Signer = Signer.CertificateName }; // TODO_OLD Call RenderCustomAppearance(); here. - signatureField.PrepareForSave(); // TODO_OLD PdfSignatureField.PrepareForSave() is not triggered automatically so let's call it manually from here, but it would be better to be called automatically. + signatureField.PrepareForSave(); // TODO_OLD PdfSignatureField.PrepareForSave() is not triggered automatically so let's call it manually from here, but it would be better to be called automatically + + // Se vazio, define para imprimir (requisito PDF/A para SigField) + signatureField.Elements.SetInteger("/F", 4); Document.Internals.AddObject(signatureField); return signatureField; } + + PdfSignature2 GetSignatureDictionary(PdfSignaturePlaceholderItem contents, PdfPlaceholderObject byteRange) { PdfSignature2 signatureDic = new(Document); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index d1eb2a39..0c7d3cd7 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -1,22 +1,22 @@ // PDFsharp - A .NET library for processing PDF // See the LICENSE file in the solution root for more information. -using System.Reflection; -using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using PdfSharp.Drawing; using PdfSharp.Events; using PdfSharp.Fonts.Internal; using PdfSharp.Logging; +using PdfSharp.Pdf.AcroForms; using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Filters; using PdfSharp.Pdf.Internal; using PdfSharp.Pdf.IO; -using PdfSharp.Pdf.AcroForms; -using PdfSharp.Pdf.Filters; using PdfSharp.Pdf.Security; using PdfSharp.Pdf.Signatures; using PdfSharp.Pdf.Structure; using PdfSharp.UniversalAccessibility; +using System.Reflection; +using System.Runtime.InteropServices; // ReSharper disable InconsistentNaming // ReSharper disable ConvertPropertyToExpressionBody @@ -148,10 +148,19 @@ void Dispose(bool disposing) /// /// Temporary hack to set a value that tells PDFsharp to create a PDF/A conform document. /// - public void SetPdfA() // HACK_OLD + public void SetPdfA() // HACK_OLD { _isPdfA = true; - _ = UAManager.ForDocument(this); + + try + { + _ = UAManager.ForDocument(this); + } + catch (Exception ex) + { + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning)) + PdfSharpLogHost.Logger.LogWarning($"SetPdfA: UAManager.ForDocument failed: {ex.Message}"); + } } /// @@ -240,15 +249,22 @@ public void Save(string path) public async Task SaveAsync(string path) { EnsureNotYetSaved(); - if (!CanModify) throw new InvalidOperationException(PsMsgs.CannotModify); + + bool isIncremental = _digitalSignatureHandler?.Options.AppendSignature ?? false; + + FileAccess access = (isIncremental ? FileAccess.ReadWrite : ((_digitalSignatureHandler == null) ? FileAccess.Write : FileAccess.ReadWrite)); + FileMode mode = (isIncremental ? FileMode.Open : FileMode.Create); + + using FileStream stream = new FileStream(path, mode, access, FileShare.None); + + if (isIncremental) + { + stream.Seek(0L, SeekOrigin.End); + _incrementalSave = true; + } - // We need ReadWrite when adding a signature. Write is sufficient if not adding a signature. - var fileAccess = _digitalSignatureHandler == null ? FileAccess.Write : FileAccess.ReadWrite; - - // ReSharper disable once UseAwaitUsing because we need no DisposeAsync for a simple FileStream. - using var stream = new FileStream(path, FileMode.Create, fileAccess, FileShare.None); await SaveAsync(stream).ConfigureAwait(false); } @@ -272,14 +288,14 @@ public async Task SaveAsync(Stream stream, bool closeStream = false) if (!stream.CanWrite) throw new InvalidOperationException(PsMsgs.StreamMustBeWritable); - + if (!CanModify) throw new InvalidOperationException(PsMsgs.CannotModify); // #PDF-A if (IsPdfA) PrepareForPdfA(); - + // TODO_OLD: more diagnostic checks string message = ""; if (!CanSave(ref message)) @@ -287,27 +303,26 @@ public async Task SaveAsync(Stream stream, bool closeStream = false) // Get security handler if document gets encrypted. var effectiveSecurityHandler = SecuritySettings.EffectiveSecurityHandler; - + PdfWriter? writer = null; try { Debug.Assert(ReferenceEquals(_document, this)); - writer = new(stream, _document, effectiveSecurityHandler); + writer = new (stream, _document, effectiveSecurityHandler); + if (stream is FileStream { Name: not null } fileStream) + writer.FullPath = fileStream.Name; + await DoSaveAsync(writer).ConfigureAwait(false); } finally { - if (stream != null!) + if (stream != null) { if (closeStream) - { stream.Close(); - } - else - { - if (stream is { CanRead: true, CanSeek: true }) - stream.Position = 0; // Reset the stream position if the stream is kept open. - } + else if (stream != null && stream.CanRead && stream.CanSeek) + stream.Position = 0L; + } writer?.Close(closeStream); } @@ -338,9 +353,7 @@ async Task DoSaveAsync(PdfWriter writer) // Remove XRefTrailer if (Trailer is PdfCrossReferenceStream crossReferenceStream) - { Trailer = new PdfTrailer(crossReferenceStream); - } var effectiveSecurityHandler = _securitySettings?.EffectiveSecurityHandler; if (effectiveSecurityHandler != null) @@ -353,28 +366,32 @@ async Task DoSaveAsync(PdfWriter writer) } else Trailer.Elements.Remove(PdfTrailer.Keys.Encrypt); - + PrepareForSave(); - + effectiveSecurityHandler?.PrepareForWriting(); - writer.WriteFileHeader(this); - var irefs = IrefTable.AllReferences; + if (_incrementalSave) + writer.Stream.Seek(0L, SeekOrigin.End); + else + writer.WriteFileHeader(this); + + PdfReference[] irefs = IrefTable.AllReferences; int count = irefs.Length; - for (int idx = 0; idx < count; idx++) + for (int i = 0; i < count; i++) { - PdfReference iref = irefs[idx]; + PdfReference iref = irefs[i]; #if DEBUG_ if (iref.ObjectNumber == 378) _ = typeof(int); #endif iref.Position = writer.Position; - var obj = iref.Value; + PdfObject obj = iref.Value; // Enter indirect object in SecurityHandler to allow object encryption key generation for this object. effectiveSecurityHandler?.EnterObject(obj.ObjectID); - + obj.WriteObject(writer); } @@ -382,7 +399,7 @@ async Task DoSaveAsync(PdfWriter writer) effectiveSecurityHandler?.LeaveObject(); // ReSharper disable once RedundantCast. Redundant only if 64 bit. - var startXRef = (SizeType)writer.Position; + long startXRef = (SizeType)writer.Position; IrefTable.WriteObject(writer); writer.WriteRaw("trailer\n"); Trailer.Elements.SetInteger("/Size", count + 1); @@ -393,71 +410,141 @@ async Task DoSaveAsync(PdfWriter writer) // Prepare for signing. if (_digitalSignatureHandler != null) await _digitalSignatureHandler.ComputeSignatureAndRange(writer).ConfigureAwait(false); - - //if (encrypt) - //{ - // state &= ~DocumentState.SavingEncrypted; - // //_securitySettings.SecurityHandler.EncryptDocument(); - //} + + if (_incrementalSave) + { + writer.Stream.Flush(); + writer.Stream.Position = writer.Stream.Length; + } } finally { - //await writer.Stream.FlushAsync().ConfigureAwait(false); writer.Stream.Flush(); - // Do not close the stream writer here. _state |= DocumentState.Saved; } } - void PrepareForPdfA() // Just a first hack. + void PrepareForPdfA() { var internals = Internals; - Debug.Assert(_uaManager != null); - // UAManager sets MarkInformation. - if (_uaManager == null) + InitializePdfAComponents(); + + // If catalog already has OutputIntents, assume PDF/A intent already set -> skip adding. + if (Catalog.Elements.GetObject(PdfCatalog.Keys.OutputIntents) != null) { - // Marked must be true in MarkInfo. - var markInfo = new PdfMarkInformation(); - //internals.AddObject(markInfo); + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + PdfSharpLogHost.Logger.LogDebug("PrepareForPdfA: OutputIntents already present -> skip."); + return; + } + + PdfArray outputIntentsArray = new PdfArray(this); + var outputIntent = new PdfDictionary(this); + outputIntentsArray.Elements.Add(outputIntent); + + outputIntent.Elements.SetName("/Type", "/OutputIntent"); + outputIntent.Elements.SetName("/S", "/GTS_PDFA1"); + outputIntent.Elements.SetString("/OutputConditionIdentifier", "sRGB"); + outputIntent.Elements.SetString("/RegistryName", "http://www.color.org"); + outputIntent.Elements.SetString("/Info", "Creator: ColorOrg Manufacturer:IEC Model:sRGB"); + + PdfDictionary? profileObject = BuildOrReuseColorProfile(internals); + + if (profileObject?.Reference != null) + outputIntent.Elements.SetReference("/DestOutputProfile", profileObject.Reference); + + // Finally set OutputIntents in catalog safely — use SetValue/SetReference not Add to avoid duplicate key + if (internals.Catalog.Elements.ContainsKey(PdfCatalog.Keys.OutputIntents)) + internals.Catalog.Elements.SetValue(PdfCatalog.Keys.OutputIntents, outputIntentsArray); + else + internals.Catalog.Elements.Add(PdfCatalog.Keys.OutputIntents, outputIntentsArray); + } - markInfo.Elements.SetBoolean(PdfMarkInformation.Keys.Marked, true); - //internals.Catalog.Elements.SetReference(PdfCatalog.Keys.MarkInfo, markInfo); - internals.Catalog.Elements.Add(PdfCatalog.Keys.MarkInfo, markInfo); + /// + /// Ensures UA manager initialization and logs debug warnings if setup fails. + /// + void InitializePdfAComponents() + { + try + { + if (_uaManager == null) + _ = UAManager.ForDocument(this); + } + catch (Exception ex) + { + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + PdfSharpLogHost.Logger.LogDebug($"PrepareForPdfA: UAManager.ForDocument() failed: {ex.Message}"); } + } + + /// + /// Attempts to reuse an existing ICC color profile or embeds a new sRGB profile if needed. + /// + PdfDictionary? BuildOrReuseColorProfile(PdfInternals internals) + { + try + { + var existing = FindExistingOutputProfile(internals); + if (existing != null) + return existing; - var outputIntentsArray = new PdfArray(this); - //internals.AddObject(outputIntentsArray); - var outputIntents = new PdfDictionary(this); - outputIntentsArray.Elements.Add(outputIntents); + using var profileStream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("PdfSharp.Resources.sRGB2014.icc") + ?? throw new InvalidOperationException("Embedded color profile was not found."); - outputIntents.Elements.Add("/Type", new PdfName("/OutputIntent")); - outputIntents.Elements.Add("/S", new PdfName("/GTS_PDFA1")); - outputIntents.Elements.Add("/OutputConditionIdentifier", new PdfString("sRGB")); - outputIntents.Elements.Add("/RegistryName", new PdfString("http://www.color.org")); - outputIntents.Elements.Add("/Info", new PdfString("Creator: ColorOrg Manufacturer:IEC Model:sRGB")); + var profile = new byte[profileStream.Length]; + var read = profileStream.Read(profile, 0, (int)profileStream.Length); + if (read != profileStream.Length) + throw new InvalidOperationException("Embedded color profile was not read."); - var profileStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("PdfSharp.Resources.sRGB2014.icc") - ?? throw new InvalidOperationException("Embedded color profile was not found."); + var fd = new FlateDecode(); + byte[] profileCompressed = fd.Encode(profile, Options.FlateEncodeMode); - var profile = new byte[profileStream.Length]; - var read = profileStream.Read(profile, 0, (int)profileStream.Length); - if (read != profileStream.Length) - throw new InvalidOperationException("Embedded color profile was not read."); + var profileObject = new PdfDictionary(this); + IrefTable.Add(profileObject); - var fd = new FlateDecode(); - byte[] profileCompressed = fd.Encode(profile, Options.FlateEncodeMode); + profileObject.Stream = new PdfDictionary.PdfStream(profileCompressed, profileObject); + profileObject.Elements.SetInteger("/N", 3); + profileObject.Elements.SetInteger("/Length", profileCompressed.Length); + profileObject.Elements.SetName("/Filter", "/FlateDecode"); - var profileObject = new PdfDictionary(this); - IrefTable.Add(profileObject); - profileObject.Stream = new PdfDictionary.PdfStream(profileCompressed, profileObject); - profileObject.Elements["/N"] = new PdfInteger(3); - profileObject.Elements["/Length"] = new PdfInteger(profileCompressed.Length); - profileObject.Elements["/Filter"] = new PdfName("/FlateDecode"); + return profileObject; + } + catch (Exception ex) + { + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning)) + PdfSharpLogHost.Logger.LogWarning($"PrepareForPdfA: ICC embedding failed: {ex.Message}"); + return null; + } + } - outputIntents.Elements.Add("/DestOutputProfile", profileObject.Reference); - //internals.Catalog.Elements.SetReference(PdfCatalog.Keys.OutputIntents, outputIntentsArray); - internals.Catalog.Elements.Add(PdfCatalog.Keys.OutputIntents, outputIntentsArray); + PdfDictionary? FindExistingOutputProfile(PdfInternals internals) + { + try + { + var oi = internals.Catalog.Elements.GetObject(PdfCatalog.Keys.OutputIntents); + if (oi is PdfArray arr && arr.Elements.Count > 0) + { + var first = arr.Elements[0]; + if (first is PdfReference r && r.Value is PdfDictionary d) + { + var dest = d.Elements.GetReference("/DestOutputProfile"); + if (dest != null && dest.Value is PdfDictionary profileDict) + return profileDict; + } + else if (first is PdfDictionary d2) + { + var dest = d2.Elements.GetReference("/DestOutputProfile"); + if (dest != null && dest.Value is PdfDictionary profileDict) + return profileDict; + } + } + } + catch + { + // ignore - fallback to null + } + return null; } /// @@ -475,7 +562,7 @@ internal override void PrepareForSave() info.Creator = PdfSharpProductVersionInformation.Producer; // We set Producer if it is not yet set. - var pdfProducer = PdfSharpProductVersionInformation.Creator; + string pdfProducer = PdfSharpProductVersionInformation.Creator; #if DEBUG // Add OS suffix only in DEBUG build. pdfProducer += $" under {RuntimeInformation.OSDescription}"; @@ -483,15 +570,10 @@ internal override void PrepareForSave() // Keep original producer if file was imported. This is 'PDF created by' in Adobe Reader. string producer = info.Producer; if (producer.Length == 0) - { producer = pdfProducer; - } - else - { - // Prevent endless concatenation if file is edited with PDFsharp more than once. - if (!producer.StartsWith(PdfSharpProductVersionInformation.Title, StringComparison.Ordinal)) - producer = $"{pdfProducer} (Original: {producer})"; - } + else if (!producer.StartsWith(PdfSharpProductVersionInformation.Title, StringComparison.Ordinal)) + producer = $"{pdfProducer} (Original: {producer})"; + info.Elements.SetString(PdfDocumentInformation.Keys.Producer, producer); // Prepare used fonts. @@ -500,15 +582,12 @@ internal override void PrepareForSave() // Let catalog do the rest. Catalog.PrepareForSave(); -#if true // Remove all unreachable objects (e.g. from deleted pages). int removed = IrefTable.Compact(); if (removed != 0 && PdfSharpLogHost.Logger.IsEnabled(LogLevel.Information)) - { PdfSharpLogHost.Logger.LogInformation($"PrepareForSave: Number of deleted unreachable objects: {removed}"); - } + IrefTable.Renumber(); -#endif // #PDF-UA // Create PdfMetadata now to include the final document information in XMP generation. @@ -998,6 +1077,11 @@ internal void EnsureNotYetSaved() internal PdfDocumentOpenMode _openMode; internal UAManager? _uaManager; internal DigitalSignatureHandler? _digitalSignatureHandler; + /// + /// When true, the document will be saved incrementally instead of being rewritten entirely. + /// Used for appending signatures without breaking previous ones. + /// + internal bool _incrementalSave; } #if true_ diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfMetadata.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfMetadata.cs index 9bbef518..b2c53fff 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfMetadata.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfMetadata.cs @@ -63,7 +63,10 @@ static DateTime SpecifyLocalDateTimeKindIfUnspecified(DateTime value) => value.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(value, DateTimeKind.Local) : value; var creationDate = SpecifyLocalDateTimeKindIfUnspecified(_document.Info.CreationDate).ToString("yyyy-MM-ddTHH:mm:ssK"); - var modificationDate = creationDate; + + // CORREÇÃO: Usar a ModDate do documento, que deve ser atualizada no PrepareForSave + var modificationDate = SpecifyLocalDateTimeKindIfUnspecified(_document.Info.ModificationDate).ToString("yyyy-MM-ddTHH:mm:ssK"); + var author = _document.Info.Author; var creator = _document.Info.Creator; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs index 9d451f45..94126a3e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs @@ -805,7 +805,19 @@ string IContentStream.GetFormName(XForm form) internal override void WriteObject(PdfWriter writer) { - // #PDF-A + // NEW SECTION (FIX FOR PDF/A PROHIBITED TRANSPARENCY) + // If the document is PDF/A, remove the "/Group" entry before writing the object. + if (_document.IsPdfA) + { + // If the page contains a group key, remove it to ensure PDF/A-1A/1B compliance. + // The XMP value (Object 17) should define the compliance, and WriteObject + // must not write anything that contradicts that compliance. + if (Elements.ContainsKey(Keys.Group)) + { + Elements.Remove(Keys.Group); + } + } + // Suppress transparency group if PDF-A is required. if (!_document.IsPdfA) { diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/signatures/DefaultSignerTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/signatures/DefaultSignerTests.cs index deb4e645..84a3fd40 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/signatures/DefaultSignerTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/signatures/DefaultSignerTests.cs @@ -276,6 +276,77 @@ public void Sign_existing_file_Default() PdfFileUtility.ShowDocumentIfDebugging(filename); } + [Fact] + public void Sign_existing_pdf_incrementally_and_preserve_pdfa() + { + + IOUtility.EnsureAssetsVersion(RequiredAssets); + + var pdfFolder = IOUtility.GetAssetsPath("archives/samples-1.5/PDFs"); + var pdfFile = Path.Combine(pdfFolder ?? throw new InvalidOperationException("Call Download-Assets.ps1 before running the tests."), "SomeLayout.pdf"); + var certificate = DefaultSignerTests.GetCertificate("test-cert_rsa_1024"); + + string signedFile = PdfFileUtility.GetTempPdfFullFileName("PDFsharp/UnitTest/incremental/signed"); + + // Act - First signature + using (PdfDocument document = PdfReader.Open(pdfFile, PdfDocumentOpenMode.Modify)) + { + document.SetPdfA(); + var options = new DigitalSignatureOptions + { + ContactInfo = "John Doe", + Location = "Seattle", + Reason = "License Agreement", + Rectangle = new XRect(100, 100, 200, 50), + AppearanceHandler = new SignatureAppearanceHandler() + }; + + var signer = new PdfSharpDefaultSigner(certificate, PdfMessageDigestType.SHA512); + var handler = DigitalSignatureHandler.ForDocument(document, signer, options); + handler.Document.Save(signedFile); + } + + // Start first viewer. + PdfFileUtility.ShowDocumentIfDebugging(signedFile); + + // Act - Second signature (incremental) + using (var documentSecondSignature = PdfReader.Open(signedFile, PdfDocumentOpenMode.Modify)) + { + documentSecondSignature.SetPdfA(); + var optionsSecondSignature = new DigitalSignatureOptions + { + AppendSignature = true, + ContactInfo = "John Doe", + Location = "Seattle", + Reason = "License Agreement", + Rectangle = new XRect(100, 200, 200, 50), + AppearanceHandler = new SignatureAppearanceHandler() + }; + + var signerSecondSignature = new PdfSharpDefaultSigner(certificate, PdfMessageDigestType.SHA512); + var handlersignerSecondSignature = DigitalSignatureHandler.ForDocument(documentSecondSignature, signerSecondSignature, optionsSecondSignature); + handlersignerSecondSignature.Document.Save(signedFile); + } + + // Start secondS viewer. + PdfFileUtility.ShowDocumentIfDebugging(signedFile); + + var exists = File.Exists(signedFile); + if (!exists) + throw new PdfSharpException("Signed file not created."); + var finalBytes = File.ReadAllBytes(signedFile); + var finalBytesLength = finalBytes.Length; + if (finalBytesLength == 0) + throw new PdfSharpException("Signed file is empty."); + + // Quick check: incremental signature should not break PDF/A + // (simple heuristic: OutputIntents must still exist) + using var finalDoc = PdfReader.Open(signedFile); + var outputIntentsExists = finalDoc.Catalog.Elements.ContainsKey("/OutputIntents"); + if (!outputIntentsExists) + throw new PdfSharpException("Signed file is not PDF/A compliant anymore."); + } + [SkippableFact] public void Sign_with_Certificate_from_Store() {