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()
{