From d1d3c5e048cbd8f155ac6aa7f682f108fbf81efa Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Thu, 6 Nov 2025 14:05:20 -0300 Subject: [PATCH 01/11] =?UTF-8?q?Feature:=20possibilitar=20assinatura=20in?= =?UTF-8?q?cremental=20e=20manter=20n=C3=ADvel=20de=20conformidade=20PDF/A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PDFsharp/src/PdfSharp/Pdf.IO/PdfWriter.cs | 8 + .../Pdf.Signatures/DigitalSignatureOptions.cs | 6 + .../Pdf.Signatures/PdfSignatureHandler.cs | 217 ++++++++++++++-- .../PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 236 +++++++++++++++--- 4 files changed, 403 insertions(+), 64 deletions(-) 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..9671bff5 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -8,6 +8,8 @@ using PdfSharp.Pdf.Advanced; using PdfSharp.Pdf.Internal; using PdfSharp.Pdf.IO; +using System.Text; +using System.Text.RegularExpressions; namespace PdfSharp.Pdf.Signatures { @@ -62,6 +64,20 @@ public static DigitalSignatureHandler ForDocument(PdfDocument document, IDigital internal async Task ComputeSignatureAndRange(PdfWriter writer) { + // If caller requested an append/incremental-sign option, branch to incremental flow. + // NOTE: still experimental - full incremental algorithm requires careful creation + // of incremental objects and trailer. For now we prepare the point of interception + // and leave the default behavior untouched unless AppendSignature == true. + if (Options.AppendSignature && writer.FullPath != null) + { + // For now call a helper that will attempt an incremental signature. + // The helper below is a minimal placeholder and currently will + // attempt to write the computed signature into the existing file + // using the placeholder offsets determined during save. + await ComputeIncrementalSignatureAsync(writer.Stream).ConfigureAwait(false); + return; + } + var (rangedStreamToSign, byteRangeArray) = GetRangeToSignAndByteRangeArray(writer.Stream); Debug.Assert(_signatureFieldByteRangePlaceholder != null); @@ -90,6 +106,88 @@ internal async Task ComputeSignatureAndRange(PdfWriter writer) writer.WriteRaw('>'); } + /// + /// Minimal placeholder for incremental-sign attempt. + /// This function is intentionally conservative: it tries to update the file + /// in-place only if the placeholder offsets appear valid. A robust incremental + /// implementation requires writing incremental xref & trailer; we'll extend + /// this later. For now this helper tries to write the signature bytes at the + /// reserved placeholder offset in the existing file (works when offsets align). + /// + // Antes (exemplo): + // internal async Task ComputeIncrementalSignatureAsync(string path, Stream ignored) { ... } + + // Depois: novo método que usa o stream já aberto. + internal async Task ComputeIncrementalSignatureAsync(Stream targetStream) + { + if (targetStream is null) + throw new ArgumentNullException(nameof(targetStream)); + + if (!targetStream.CanRead || !targetStream.CanSeek || !targetStream.CanWrite) + throw new InvalidOperationException("Target stream must be readable, seekable and writable for incremental signature."); + + // IMPORTANT: aqui reutilizamos exatamente a lógica que precisava do arquivo aberto: + // - obter ranged stream e byte range via GetRangeToSignAndByteRangeArray + // - escrever o byteRange placeholder (já feito normalmente antes) + // - calcular a assinatura sobre o ranged stream + // - escrever a assinatura no placeholder + + // Observe: suponho que _signatureFieldByteRangePlaceholder e _placeholderItem já foram inicializados + // por AddSignatureComponentsAsync, como na implementação padrão. + var (rangedStream, byteRangeArray) = GetRangeToSignAndByteRangeArray(targetStream); + + // Escreve o ByteRange atual (substitui a placeholder na posição certa) + Debug.Assert(_signatureFieldByteRangePlaceholder != null); + // Note: WriteActualObject precisa de um PdfWriter em sua implementação atual. Se WriteActualObject + // aceita um writer, preferir reusar o writer. Se não aceitar, adaptar para escrever diretamente no stream. + // Aqui assumimos que você tem acesso a um writer (ou que WriteActualObject tem overload). + // Se for necessário, você pode criar um PdfWriter temporário que usa targetStream e o Document. + // Exemplo (se WriteActualObject usa PdfWriter): + var tempWriter = new PdfWriter(targetStream, Document, /*effectiveSecurityHandler*/ null) + { + Layout = PdfWriterLayout.Compact + }; + _signatureFieldByteRangePlaceholder.WriteActualObject(byteRangeArray, tempWriter); + + // Calcula a assinatura (rangedStream é um stream que representa os ranges a assinar) + byte[] signature = await Signer.GetSignatureAsync(rangedStream).ConfigureAwait(false); + + // Verifica tamanho + Debug.Assert(_placeholderItem != null); + int expectedLength = _placeholderItem.Size; + if (signature.Length > expectedLength) + throw new Exception($"Actual signature length {signature.Length} exceeds placeholder {expectedLength}."); + + // Escreve a assinatura hex no local reservado + targetStream.Position = _placeholderItem.StartPosition; + // write '<' + targetStream.WriteByte((byte)'<'); + + // convert signature to hex literal like "" + var hexLiteral = PdfEncoders.ToHexStringLiteral(signature, false, false, null); // returns string with angle brackets + + // Option A (recommended): copy inner chars directly into byte[] without creating an extra substring + var contentLength = hexLiteral.Length - 2; // exclude '<' and '>' + var writeBytes = new byte[contentLength]; + PdfEncoders.RawEncoding.GetBytes(hexLiteral, 1, contentLength, writeBytes, 0); // copy from string to bytes + targetStream.Write(writeBytes, 0, writeBytes.Length); + + // pad remainder with '00' pairs if placeholder larger than signature + for (int i = signature.Length; i < expectedLength; i++) + { + // each pad is two ascii characters '0''0' representing a byte in hex, but in original code 1 '00' per byte is written as literal '0''0' + // however original used writer.WriteRaw("00"); — here we write the ascii bytes for "00". + var pad = PdfEncoders.RawEncoding.GetBytes("00"); + targetStream.Write(pad, 0, pad.Length); + } + + // write '>' + targetStream.WriteByte((byte)'>'); + + targetStream.Flush(); + } + + string FormatHex(byte[] bytes) // ...use RawEncoder { #if NET6_0_OR_GREATER @@ -112,7 +210,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; @@ -141,40 +239,107 @@ string FormatHex(byte[] bytes) // ...use RawEncoder 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)."); + 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); + int signatureSize = await Signer.GetSignatureSizeAsync().ConfigureAwait(false); _placeholderItem = new(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)); - else - annotations.Elements.Add(signatureField); - - // acroform - - var catalog = Document.Catalog; + var signatureDictionary = + GetSignatureDictionary(_placeholderItem, _signatureFieldByteRangePlaceholder); - if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null) - catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document)); - - if (!catalog.AcroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags)) - catalog.AcroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3)); + // ================================================================ + // 🔒 PATCH — aplica apenas se for assinatura incremental + // ================================================================ + if (Options.AppendSignature) + { + PdfCatalog catalog = Document.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 + { + int sigFlagVersion = acroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags); + if (sigFlagVersion < 3) + acroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + } + + int signatureCount = acroForm.Fields?.Elements?.Count ?? 0; + + PdfDictionary sigField = new PdfDictionary(Document); + sigField.Elements["/FT"] = new PdfName("/Sig"); + sigField.Elements["/T"] = new PdfString($"Signature{signatureCount + 1}"); + sigField.Elements["/V"] = signatureDictionary; + sigField.Elements["/Ff"] = new PdfInteger(1 << 2); + sigField.Elements["/Type"] = new PdfName("/Annot"); + sigField.Elements["/Subtype"] = new PdfName("/Widget"); + sigField.Elements["/Rect"] = new PdfRectangle(Options.Rectangle); + sigField.Elements["/P"] = Document.Pages[Options.PageIndex].Reference; + + Document.Internals.AddObject(sigField); + + if (acroForm.Elements["/Fields"] is PdfArray fieldsArray) + { + fieldsArray.Elements.Add(sigField.Reference); + } + else + { + PdfArray newFields = new PdfArray(Document); + newFields.Elements.Add(sigField.Reference); + acroForm.Elements["/Fields"] = newFields; + } + + if (!acroForm.Elements.ContainsKey("/DR")) + acroForm.Elements.Add("/DR", new PdfDictionary(Document)); + + if (!acroForm.Elements.ContainsKey("/DA")) + acroForm.Elements.Add("/DA", new PdfString("/Helv 0 Tf 0 g")); + } else { - var sigFlagVersion = catalog.AcroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags); - if (sigFlagVersion < 3) - catalog.AcroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + // ================================================================ + // ⚙️ Fluxo original — primeira assinatura + // ================================================================ + PdfDictionary signatureField = GetSignatureField(signatureDictionary); + + 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); + + PdfCatalog catalog = Document.Catalog; + + if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null) + catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document)); + + if (!catalog.AcroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags)) + catalog.AcroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3)); + else + { + int sigFlagVersion = catalog.AcroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags); + if (sigFlagVersion < 3) + catalog.AcroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + } + + if (catalog.AcroForm.Elements.GetValue(PdfAcroForm.Keys.Fields) == null) + catalog.AcroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, + new PdfAcroField.PdfAcroFieldCollection(new PdfArray())); + + catalog.AcroForm.Fields.Elements.Add(signatureField); } - 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); + PdfDictionary markInfo = + Document.Catalog.Elements.GetDictionary("/MarkInfo") + ?? new PdfDictionary(Document); + + markInfo.Elements.SetBoolean("/Marked", true); + Document.Catalog.Elements["/MarkInfo"] = markInfo; } PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index d1eb2a39..758b5755 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -1,22 +1,28 @@ // 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; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; // ReSharper disable InconsistentNaming // ReSharper disable ConvertPropertyToExpressionBody @@ -139,6 +145,56 @@ void Dispose(bool disposing) _state = DocumentState.Disposed | DocumentState.Saved; } + // --- ADDITIONAL FIELDS FOR INCREMENTAL SUPPORT --- + internal long _previousStartXref = -1; // -1 means unknown + + // Helper: get startxref position from an existing file by scanning tail + static long GetStartXrefFromFile(string path) + { + try + { + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + const int tailRead = 8192; + int toRead = (int)Math.Min(tailRead, fs.Length); + fs.Seek(-toRead, SeekOrigin.End); + byte[] tail = new byte[toRead]; + fs.Read(tail, 0, toRead); + string tailStr = Encoding.ASCII.GetString(tail); + int ix = tailStr.LastIndexOf("startxref", StringComparison.OrdinalIgnoreCase); + if (ix < 0) return -1; + string after = tailStr.Substring(ix); + var m = Regex.Match(after, @"startxref\s*(\d+)", RegexOptions.IgnoreCase); + if (m.Success && long.TryParse(m.Groups[1].Value, out long pos)) + return pos; + } + catch + { + // ignore and return -1 + } + return -1; + } + + static int GetPreviousTrailerSizeFromFile(string path, long startxref) + { + if (startxref < 0) return -1; + try + { + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + fs.Seek(startxref, SeekOrigin.Begin); + using var sr = new StreamReader(fs, Encoding.ASCII, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true); + string text = sr.ReadToEnd(); + var m = Regex.Match(text, @"trailer[\s\S]*?/Size\s+(\d+)", RegexOptions.IgnoreCase); + if (m.Success && int.TryParse(m.Groups[1].Value, out int size)) + return size; + } + catch + { + // ignore + } + return -1; + } + + /// /// Gets or sets a user-defined object that contains arbitrary information associated with this document. /// The tag is not used by PDFsharp. @@ -244,14 +300,47 @@ public async Task SaveAsync(string path) if (!CanModify) throw new InvalidOperationException(PsMsgs.CannotModify); - // We need ReadWrite when adding a signature. Write is sufficient if not adding a signature. - var fileAccess = _digitalSignatureHandler == null ? FileAccess.Write : FileAccess.ReadWrite; + // Verifica se deve realizar salvamento incremental (para preservar assinaturas anteriores) + var appendSignature = _digitalSignatureHandler?.Options.AppendSignature == true; + + // Capture previous startxref if we will append to existing file + if (appendSignature && File.Exists(path)) + { + try + { + _previousStartXref = GetStartXrefFromFile(path); + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + PdfSharpLogHost.Logger.LogDebug($"SaveAsync: previous startxref = {_previousStartXref}"); + } + catch + { + _previousStartXref = -1; + } + } + else + { + _previousStartXref = -1; + } + + var fileAccess = appendSignature ? FileAccess.ReadWrite : + _digitalSignatureHandler == null ? FileAccess.Write : FileAccess.ReadWrite; + + // Se for incremental, abrimos o arquivo existente sem recriá-lo. + var fileMode = appendSignature ? FileMode.Open : FileMode.Create; + + using var stream = new FileStream(path, fileMode, fileAccess, FileShare.None); + + // Se for incremental, posiciona no final do arquivo existente. + if (appendSignature) + { + stream.Seek(0, SeekOrigin.End); + _incrementalSave = true; + } - // 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); } + /// /// Saves the document to the specified stream. /// @@ -293,6 +382,14 @@ public async Task SaveAsync(Stream stream, bool closeStream = false) { Debug.Assert(ReferenceEquals(_document, this)); writer = new(stream, _document, effectiveSecurityHandler); + // If this SaveAsync was invoked via SaveAsync(path) above, + // the calling code has just created the FileStream from the path. + // We can capture the path via the 'stream' if it is a FileStream. + if (stream is FileStream fs && fs.Name != null) + { + // record the underlying file path for potential incremental routines + writer.FullPath = fs.Name; + } await DoSaveAsync(writer).ConfigureAwait(false); } finally @@ -332,11 +429,11 @@ async Task DoSaveAsync(PdfWriter writer) try { - // Prepare for signing. + // Prepare for signing: this will add the placeholder objects (signature dictionary etc.) if (_digitalSignatureHandler != null) await _digitalSignatureHandler.AddSignatureComponentsAsync().ConfigureAwait(false); - // Remove XRefTrailer + // Remove XRefTrailer (unchanged) if (Trailer is PdfCrossReferenceStream crossReferenceStream) { Trailer = new PdfTrailer(crossReferenceStream); @@ -358,16 +455,25 @@ async Task DoSaveAsync(PdfWriter writer) effectiveSecurityHandler?.PrepareForWriting(); - writer.WriteFileHeader(this); + // --- Incremental signature mode (append mode) --- + if (_incrementalSave) + { + // posiciona o stream no final do arquivo existente para append incremental + writer.Stream.Seek(0, SeekOrigin.End); + // Não escrevemos header para incremental — apenas adicionamos atualização incremental. + } + + // Somente escrevemos o cabeçalho quando NÃO estamos no modo incremental. + if (!_incrementalSave) + { + writer.WriteFileHeader(this); + } + var irefs = IrefTable.AllReferences; int count = irefs.Length; for (int idx = 0; idx < count; idx++) { PdfReference iref = irefs[idx]; -#if DEBUG_ - if (iref.ObjectNumber == 378) - _ = typeof(int); -#endif iref.Position = writer.Position; var obj = iref.Value; @@ -378,33 +484,67 @@ async Task DoSaveAsync(PdfWriter writer) obj.WriteObject(writer); } - // Leaving only the last indirect object in SecurityHandler is sufficient, as this is the first time no indirect object is entered anymore. + // Leaving only the last indirect object in SecurityHandler is sufficient effectiveSecurityHandler?.LeaveObject(); - // ReSharper disable once RedundantCast. Redundant only if 64 bit. + // ---------- MUDANÇA CRÍTICA ---------- + // Agora que os objetos novos (incluindo placeholders) foram escritos no stream, + // podemos calcular a assinatura (ela depende das posições/offsets já escritas). + if (_digitalSignatureHandler != null) + { + await _digitalSignatureHandler.ComputeSignatureAndRange(writer).ConfigureAwait(false); + // ComputeSignatureAndRange deve escrever a assinatura no placeholder (conteúdo /Contents). + } + + // Agora gravamos o xref subseção e trailer (baseado na posição atual do writer). var startXRef = (SizeType)writer.Position; + + // Escreve apenas a xref subseção relativa aos irefs já preparados. IrefTable.WriteObject(writer); writer.WriteRaw("trailer\n"); - Trailer.Elements.SetInteger("/Size", count + 1); + + // Se incremental: preencher /Prev com startxref anterior e calcular /Size adequadamente. + if (_incrementalSave && _previousStartXref >= 0) + { + // Tentar extrair o previousSize do trailer existente no arquivo + int previousSize = GetPreviousTrailerSizeFromFile(writer.FullPath ?? "", _previousStartXref); + if (previousSize > 0) + { + // new size = previousSize + número de objetos adicionados + // count representa o número total de referências IrefTable.AllReferences no documento atual; + // se previousSize for válido, calculamos incremento. Aqui usamos um fallback conservador. + Trailer.Elements.SetInteger("/Size", previousSize + (count + 1)); + } + else + { + Trailer.Elements.SetInteger("/Size", count + 1); + } + + // Set /Prev to previous startxref + // Nota: Trailer.Elements aceita chaves string diretamente + if (!Trailer.Elements.ContainsKey("/Prev")) + Trailer.Elements.Add("/Prev", new PdfInteger((int)_previousStartXref)); + else + Trailer.Elements.SetInteger("/Prev", (int)_previousStartXref); + } + else + { + Trailer.Elements.SetInteger("/Size", count + 1); + } + Trailer.WriteObject(writer); writer.WriteEof(this, startXRef); - // #Signature: What about encryption + signing ?? - // Prepare for signing. - if (_digitalSignatureHandler != null) - await _digitalSignatureHandler.ComputeSignatureAndRange(writer).ConfigureAwait(false); - - //if (encrypt) - //{ - // state &= ~DocumentState.SavingEncrypted; - // //_securitySettings.SecurityHandler.EncryptDocument(); - //} + // If incremental, flush and leave stream at end + 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; } } @@ -500,19 +640,34 @@ 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)) + // # Remoção de unreachable objects - Apenas quando NÃO estamos em incremental mode. + if (!_incrementalSave) { - PdfSharpLogHost.Logger.LogInformation($"PrepareForSave: Number of deleted unreachable objects: {removed}"); + int removed = IrefTable.Compact(); + if (removed != 0 && PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Information)) + { + PdfSharpLogHost.Logger.LogInformation($"PrepareForSave: Number of deleted unreachable objects: {removed}"); + } + IrefTable.Renumber(); + } + else + { + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + PdfSharpLogHost.Logger.LogDebug("PrepareForSave: incremental save -> skipping Compact/Renumber to preserve existing object numbers."); } - IrefTable.Renumber(); -#endif // #PDF-UA // Create PdfMetadata now to include the final document information in XMP generation. - Catalog.Elements.SetReference(PdfCatalog.Keys.Metadata, new PdfMetadata(this)); + // BUT: do NOT overwrite existing Metadata, pois isso quebra PDF/A. + if (Catalog.Elements.GetObject(PdfCatalog.Keys.Metadata) == null) + { + Catalog.Elements.SetReference(PdfCatalog.Keys.Metadata, new PdfMetadata(this)); + } + else + { + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + PdfSharpLogHost.Logger.LogDebug("PrepareForSave: existing Metadata found -> not overwriting XMP."); + } } /// @@ -998,6 +1153,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_ From 12db63f4b7fdbd1e87e482e1facaf97b8e1ee3a9 Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Thu, 6 Nov 2025 16:06:00 -0300 Subject: [PATCH 02/11] Feature: revert nivel conformidade pdfa. Assinaturas normal --- .../Pdf.Signatures/PdfSignatureHandler.cs | 269 ++++++---------- .../PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 293 +++++------------- 2 files changed, 173 insertions(+), 389 deletions(-) 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 9671bff5..cb80996b 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -64,45 +64,26 @@ public static DigitalSignatureHandler ForDocument(PdfDocument document, IDigital internal async Task ComputeSignatureAndRange(PdfWriter writer) { - // If caller requested an append/incremental-sign option, branch to incremental flow. - // NOTE: still experimental - full incremental algorithm requires careful creation - // of incremental objects and trailer. For now we prepare the point of interception - // and leave the default behavior untouched unless AppendSignature == true. if (Options.AppendSignature && writer.FullPath != null) { - // For now call a helper that will attempt an incremental signature. - // The helper below is a minimal placeholder and currently will - // attempt to write the computed signature into the existing file - // using the placeholder offsets determined during save. - await ComputeIncrementalSignatureAsync(writer.Stream).ConfigureAwait(false); + await ComputeIncrementalSignatureAsync(writer.Stream).ConfigureAwait(continueOnCapturedContext: false); return; } - - var (rangedStreamToSign, 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); - - 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."); - - // Write the signature at the space reserved by placeholder item. + var (stream, obj) = GetRangeToSignAndByteRangeArray(writer.Stream); + _signatureFieldByteRangePlaceholder.WriteActualObject(obj, writer); + byte[] array = await Signer.GetSignatureAsync(stream).ConfigureAwait(continueOnCapturedContext: false); + int size = _placeholderItem.Size; + if (array.Length > size) + { + throw new Exception($"The actual digest length {array.Length} is larger than the approximation made {size}. Not enough room in the placeholder to fit the signature."); + } writer.Stream.Position = _placeholderItem.StartPosition; - - // When the signature includes a timestamp, the exact length is unknown until the signature is definitely calculated. - // Therefore, we write the angle brackets here and override the placeholder white spaces. writer.WriteRaw('<'); - 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) + writer.Write(PdfEncoders.RawEncoding.GetBytes(FormatHex(array))); + for (int i = array.Length; i < size; i++) + { writer.WriteRaw("00"); - + } writer.WriteRaw('>'); } @@ -120,74 +101,44 @@ internal async Task ComputeSignatureAndRange(PdfWriter writer) // Depois: novo método que usa o stream já aberto. internal async Task ComputeIncrementalSignatureAsync(Stream targetStream) { - if (targetStream is null) - throw new ArgumentNullException(nameof(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."); - - // IMPORTANT: aqui reutilizamos exatamente a lógica que precisava do arquivo aberto: - // - obter ranged stream e byte range via GetRangeToSignAndByteRangeArray - // - escrever o byteRange placeholder (já feito normalmente antes) - // - calcular a assinatura sobre o ranged stream - // - escrever a assinatura no placeholder - - // Observe: suponho que _signatureFieldByteRangePlaceholder e _placeholderItem já foram inicializados - // por AddSignatureComponentsAsync, como na implementação padrão. - var (rangedStream, byteRangeArray) = GetRangeToSignAndByteRangeArray(targetStream); - - // Escreve o ByteRange atual (substitui a placeholder na posição certa) - Debug.Assert(_signatureFieldByteRangePlaceholder != null); - // Note: WriteActualObject precisa de um PdfWriter em sua implementação atual. Se WriteActualObject - // aceita um writer, preferir reusar o writer. Se não aceitar, adaptar para escrever diretamente no stream. - // Aqui assumimos que você tem acesso a um writer (ou que WriteActualObject tem overload). - // Se for necessário, você pode criar um PdfWriter temporário que usa targetStream e o Document. - // Exemplo (se WriteActualObject usa PdfWriter): - var tempWriter = new PdfWriter(targetStream, Document, /*effectiveSecurityHandler*/ null) + } + (RangedStream rangedStream, PdfArray byteRangeArray) rangeToSignAndByteRangeArray = GetRangeToSignAndByteRangeArray(targetStream); + RangedStream item = rangeToSignAndByteRangeArray.rangedStream; + PdfArray item2 = rangeToSignAndByteRangeArray.byteRangeArray; + PdfWriter writer = new PdfWriter(targetStream, Document, null) { Layout = PdfWriterLayout.Compact }; - _signatureFieldByteRangePlaceholder.WriteActualObject(byteRangeArray, tempWriter); - - // Calcula a assinatura (rangedStream é um stream que representa os ranges a assinar) - byte[] signature = await Signer.GetSignatureAsync(rangedStream).ConfigureAwait(false); - - // Verifica tamanho - Debug.Assert(_placeholderItem != null); - int expectedLength = _placeholderItem.Size; - if (signature.Length > expectedLength) - throw new Exception($"Actual signature length {signature.Length} exceeds placeholder {expectedLength}."); - - // Escreve a assinatura hex no local reservado + _signatureFieldByteRangePlaceholder.WriteActualObject(item2, writer); + byte[] array = await Signer.GetSignatureAsync(item).ConfigureAwait(continueOnCapturedContext: false); + int size = _placeholderItem.Size; + if (array.Length > size) + { + throw new Exception($"Actual signature length {array.Length} exceeds placeholder {size}."); + } targetStream.Position = _placeholderItem.StartPosition; - // write '<' - targetStream.WriteByte((byte)'<'); - - // convert signature to hex literal like "" - var hexLiteral = PdfEncoders.ToHexStringLiteral(signature, false, false, null); // returns string with angle brackets - - // Option A (recommended): copy inner chars directly into byte[] without creating an extra substring - var contentLength = hexLiteral.Length - 2; // exclude '<' and '>' - var writeBytes = new byte[contentLength]; - PdfEncoders.RawEncoding.GetBytes(hexLiteral, 1, contentLength, writeBytes, 0); // copy from string to bytes - targetStream.Write(writeBytes, 0, writeBytes.Length); - - // pad remainder with '00' pairs if placeholder larger than signature - for (int i = signature.Length; i < expectedLength; i++) + targetStream.WriteByte(60); + string text = PdfEncoders.ToHexStringLiteral(array, 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 = array.Length; i < size; i++) { - // each pad is two ascii characters '0''0' representing a byte in hex, but in original code 1 '00' per byte is written as literal '0''0' - // however original used writer.WriteRaw("00"); — here we write the ascii bytes for "00". - var pad = PdfEncoders.RawEncoding.GetBytes("00"); - targetStream.Write(pad, 0, pad.Length); + byte[] bytes = PdfEncoders.RawEncoding.GetBytes("00"); + targetStream.Write(bytes, 0, bytes.Length); } - - // write '>' - targetStream.WriteByte((byte)'>'); - + targetStream.WriteByte(62); targetStream.Flush(); } - string FormatHex(byte[] bytes) // ...use RawEncoder { #if NET6_0_OR_GREATER @@ -239,107 +190,89 @@ string FormatHex(byte[] bytes) // ...use RawEncoder 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)."); - - int signatureSize = await Signer.GetSignatureSizeAsync().ConfigureAwait(false); - _placeholderItem = new(signatureSize); - _signatureFieldByteRangePlaceholder = new PdfPlaceholderObject(ByteRangePlaceholderLength); - - var signatureDictionary = - GetSignatureDictionary(_placeholderItem, _signatureFieldByteRangePlaceholder); - - // ================================================================ - // 🔒 PATCH — aplica apenas se for assinatura incremental - // ================================================================ + { + throw new ArgumentOutOfRangeException($"Signature page doesn't exist, specified page was {Options.PageIndex + 1} but document has only {Document.PageCount} page(s)."); + } + _placeholderItem = new PdfSignaturePlaceholderItem(await Signer.GetSignatureSizeAsync().ConfigureAwait(continueOnCapturedContext: false)); + _signatureFieldByteRangePlaceholder = new PdfPlaceholderObject(36); + PdfSignature2 signatureDictionary = GetSignatureDictionary(_placeholderItem, _signatureFieldByteRangePlaceholder); if (Options.AppendSignature) { PdfCatalog catalog = Document.Catalog; - if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null) - catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document)); - + if (catalog.Elements.GetObject("/AcroForm") == null) + { + catalog.Elements.Add("/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.ContainsKey("/SigFlags")) { - int sigFlagVersion = acroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags); - if (sigFlagVersion < 3) - acroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + acroForm.Elements.Add("/SigFlags", new PdfInteger(3)); } - - int signatureCount = acroForm.Fields?.Elements?.Count ?? 0; - - PdfDictionary sigField = new PdfDictionary(Document); - sigField.Elements["/FT"] = new PdfName("/Sig"); - sigField.Elements["/T"] = new PdfString($"Signature{signatureCount + 1}"); - sigField.Elements["/V"] = signatureDictionary; - sigField.Elements["/Ff"] = new PdfInteger(1 << 2); - sigField.Elements["/Type"] = new PdfName("/Annot"); - sigField.Elements["/Subtype"] = new PdfName("/Widget"); - sigField.Elements["/Rect"] = new PdfRectangle(Options.Rectangle); - sigField.Elements["/P"] = Document.Pages[Options.PageIndex].Reference; - - Document.Internals.AddObject(sigField); - - if (acroForm.Elements["/Fields"] is PdfArray fieldsArray) + else if (acroForm.Elements.GetInteger("/SigFlags") < 3) + { + acroForm.Elements.SetInteger("/SigFlags", 3); + } + int valueOrDefault = (acroForm.Fields?.Elements?.Count).GetValueOrDefault(); + PdfDictionary pdfDictionary = new PdfDictionary(Document); + pdfDictionary.Elements["/FT"] = new PdfName("/Sig"); + pdfDictionary.Elements["/T"] = new PdfString($"Signature{valueOrDefault + 1}"); + pdfDictionary.Elements["/V"] = signatureDictionary; + pdfDictionary.Elements["/Ff"] = new PdfInteger(4); + pdfDictionary.Elements["/Type"] = new PdfName("/Annot"); + pdfDictionary.Elements["/Subtype"] = new PdfName("/Widget"); + pdfDictionary.Elements["/Rect"] = new PdfRectangle(Options.Rectangle); + pdfDictionary.Elements["/P"] = Document.Pages[Options.PageIndex].Reference; + Document.Internals.AddObject(pdfDictionary); + if (acroForm.Elements["/Fields"] is PdfArray pdfArray) { - fieldsArray.Elements.Add(sigField.Reference); + pdfArray.Elements.Add(pdfDictionary.Reference); } else { - PdfArray newFields = new PdfArray(Document); - newFields.Elements.Add(sigField.Reference); - acroForm.Elements["/Fields"] = newFields; + PdfArray pdfArray2 = new PdfArray(Document); + pdfArray2.Elements.Add(pdfDictionary.Reference); + acroForm.Elements["/Fields"] = pdfArray2; } - if (!acroForm.Elements.ContainsKey("/DR")) + { acroForm.Elements.Add("/DR", new PdfDictionary(Document)); - + } if (!acroForm.Elements.ContainsKey("/DA")) + { acroForm.Elements.Add("/DA", new PdfString("/Helv 0 Tf 0 g")); + } } else { - // ================================================================ - // ⚙️ Fluxo original — primeira assinatura - // ================================================================ - PdfDictionary signatureField = GetSignatureField(signatureDictionary); - - 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); - - PdfCatalog catalog = Document.Catalog; - - if (catalog.Elements.GetObject(PdfCatalog.Keys.AcroForm) == null) - catalog.Elements.Add(PdfCatalog.Keys.AcroForm, new PdfAcroForm(Document)); - - if (!catalog.AcroForm.Elements.ContainsKey(PdfAcroForm.Keys.SigFlags)) - catalog.AcroForm.Elements.Add(PdfAcroForm.Keys.SigFlags, new PdfInteger(3)); + PdfSignatureField signatureField = GetSignatureField(signatureDictionary); + PdfArray array = Document.Pages[Options.PageIndex].Elements.GetArray("/Annots"); + if (array == null) + { + Document.Pages[Options.PageIndex].Elements.Add("/Annots", new PdfArray(Document, signatureField)); + } else { - int sigFlagVersion = catalog.AcroForm.Elements.GetInteger(PdfAcroForm.Keys.SigFlags); - if (sigFlagVersion < 3) - catalog.AcroForm.Elements.SetInteger(PdfAcroForm.Keys.SigFlags, 3); + array.Elements.Add(signatureField); } - - 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); + PdfCatalog catalog2 = Document.Catalog; + if (catalog2.Elements.GetObject("/AcroForm") == null) + { + catalog2.Elements.Add("/AcroForm", new PdfAcroForm(Document)); + } + if (!catalog2.AcroForm.Elements.ContainsKey("/SigFlags")) + { + catalog2.AcroForm.Elements.Add("/SigFlags", new PdfInteger(3)); + } + else if (catalog2.AcroForm.Elements.GetInteger("/SigFlags") < 3) + { + catalog2.AcroForm.Elements.SetInteger("/SigFlags", 3); + } + if (catalog2.AcroForm.Elements.GetValue("/Fields") == null) + { + catalog2.AcroForm.Elements.SetValue("/Fields", new PdfAcroField.PdfAcroFieldCollection(new PdfArray())); + } + catalog2.AcroForm.Fields.Elements.Add(signatureField); } - - PdfDictionary markInfo = - Document.Catalog.Elements.GetDictionary("/MarkInfo") - ?? new PdfDictionary(Document); - - markInfo.Elements.SetBoolean("/Marked", true); - Document.Catalog.Elements["/MarkInfo"] = markInfo; } PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index 758b5755..f8d95d57 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -296,48 +296,20 @@ public void Save(string path) public async Task SaveAsync(string path) { EnsureNotYetSaved(); - if (!CanModify) throw new InvalidOperationException(PsMsgs.CannotModify); - - // Verifica se deve realizar salvamento incremental (para preservar assinaturas anteriores) - var appendSignature = _digitalSignatureHandler?.Options.AppendSignature == true; - - // Capture previous startxref if we will append to existing file - if (appendSignature && File.Exists(path)) - { - try - { - _previousStartXref = GetStartXrefFromFile(path); - if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - PdfSharpLogHost.Logger.LogDebug($"SaveAsync: previous startxref = {_previousStartXref}"); - } - catch - { - _previousStartXref = -1; - } - } - else + + bool flag = _digitalSignatureHandler?.Options.AppendSignature ?? false; + FileAccess access = (flag ? FileAccess.ReadWrite : ((_digitalSignatureHandler == null) ? FileAccess.Write : FileAccess.ReadWrite)); + FileMode mode = (flag ? FileMode.Open : FileMode.Create); + using FileStream stream = new FileStream(path, mode, access, FileShare.None); + if (flag) { - _previousStartXref = -1; - } - - var fileAccess = appendSignature ? FileAccess.ReadWrite : - _digitalSignatureHandler == null ? FileAccess.Write : FileAccess.ReadWrite; - - // Se for incremental, abrimos o arquivo existente sem recriá-lo. - var fileMode = appendSignature ? FileMode.Open : FileMode.Create; - - using var stream = new FileStream(path, fileMode, fileAccess, FileShare.None); - - // Se for incremental, posiciona no final do arquivo existente. - if (appendSignature) - { - stream.Seek(0, SeekOrigin.End); + stream.Seek(0L, SeekOrigin.End); _incrementalSave = true; } - await SaveAsync(stream).ConfigureAwait(false); + await SaveAsync(stream).ConfigureAwait(continueOnCapturedContext: false); } @@ -358,53 +330,38 @@ public void Save(Stream stream, bool closeStream = false) public async Task SaveAsync(Stream stream, bool closeStream = false) { EnsureNotYetSaved(); - 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)) throw new PdfSharpException(message); - // Get security handler if document gets encrypted. - var effectiveSecurityHandler = SecuritySettings.EffectiveSecurityHandler; - - PdfWriter? writer = null; + PdfStandardSecurityHandler effectiveSecurityHandler = SecuritySettings.EffectiveSecurityHandler; + PdfWriter writer = null; try { - Debug.Assert(ReferenceEquals(_document, this)); - writer = new(stream, _document, effectiveSecurityHandler); - // If this SaveAsync was invoked via SaveAsync(path) above, - // the calling code has just created the FileStream from the path. - // We can capture the path via the 'stream' if it is a FileStream. - if (stream is FileStream fs && fs.Name != null) - { - // record the underlying file path for potential incremental routines - writer.FullPath = fs.Name; - } - await DoSaveAsync(writer).ConfigureAwait(false); + writer = new PdfWriter(stream, _document, effectiveSecurityHandler); + if (stream is FileStream { Name: not null } fileStream) + writer.FullPath = fileStream.Name; + + await DoSaveAsync(writer).ConfigureAwait(continueOnCapturedContext: 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); } @@ -416,126 +373,63 @@ public async Task SaveAsync(Stream stream, bool closeStream = false) async Task DoSaveAsync(PdfWriter writer) { PdfSharpLogHost.Logger.PdfDocumentSaved(Name); - if (_pages == null || _pages.Count == 0) { if (OutStream != null) - { - // Give feedback if the wrong constructor was used. throw new InvalidOperationException("Cannot save a PDF document with no pages. Do not use \"public PdfDocument(string filename)\" or \"public PdfDocument(Stream outputStream)\" if you want to open an existing PDF document from a file or stream; use PdfReader.Open() for that purpose."); - } + throw new InvalidOperationException("Cannot save a PDF document with no pages."); } try { - // Prepare for signing: this will add the placeholder objects (signature dictionary etc.) if (_digitalSignatureHandler != null) - await _digitalSignatureHandler.AddSignatureComponentsAsync().ConfigureAwait(false); - - // Remove XRefTrailer (unchanged) - if (Trailer is PdfCrossReferenceStream crossReferenceStream) - { - Trailer = new PdfTrailer(crossReferenceStream); - } - - var effectiveSecurityHandler = _securitySettings?.EffectiveSecurityHandler; - if (effectiveSecurityHandler != null) + await _digitalSignatureHandler.AddSignatureComponentsAsync().ConfigureAwait(continueOnCapturedContext: false); + + if (Trailer is PdfCrossReferenceStream trailer) + Trailer = new PdfTrailer(trailer); + + PdfStandardSecurityHandler pdfStandardSecurityHandler = _securitySettings?.EffectiveSecurityHandler; + if (pdfStandardSecurityHandler != null) { - if (effectiveSecurityHandler.Reference == null) - IrefTable.Add(effectiveSecurityHandler); - else - Debug.Assert(IrefTable.Contains(effectiveSecurityHandler.ObjectID)); - Trailer.Elements[PdfTrailer.Keys.Encrypt] = _securitySettings!.SecurityHandler.Reference; + if (pdfStandardSecurityHandler.Reference == null) + IrefTable.Add(pdfStandardSecurityHandler); + + Trailer.Elements["/Encrypt"] = _securitySettings.SecurityHandler.Reference; } else - Trailer.Elements.Remove(PdfTrailer.Keys.Encrypt); - + Trailer.Elements.Remove("/Encrypt"); + PrepareForSave(); - - effectiveSecurityHandler?.PrepareForWriting(); - - // --- Incremental signature mode (append mode) --- + + pdfStandardSecurityHandler?.PrepareForWriting(); if (_incrementalSave) - { - // posiciona o stream no final do arquivo existente para append incremental - writer.Stream.Seek(0, SeekOrigin.End); - // Não escrevemos header para incremental — apenas adicionamos atualização incremental. - } - - // Somente escrevemos o cabeçalho quando NÃO estamos no modo incremental. + writer.Stream.Seek(0L, SeekOrigin.End); + if (!_incrementalSave) - { writer.WriteFileHeader(this); - } - - var irefs = IrefTable.AllReferences; - int count = irefs.Length; - for (int idx = 0; idx < count; idx++) + + PdfReference[] allReferences = IrefTable.AllReferences; + int num = allReferences.Length; + for (int i = 0; i < num; i++) { - PdfReference iref = irefs[idx]; - iref.Position = writer.Position; - - var obj = iref.Value; - - // Enter indirect object in SecurityHandler to allow object encryption key generation for this object. - effectiveSecurityHandler?.EnterObject(obj.ObjectID); - - obj.WriteObject(writer); + PdfReference obj = allReferences[i]; + obj.Position = writer.Position; + PdfObject value = obj.Value; + pdfStandardSecurityHandler?.EnterObject(value.ObjectID); + value.WriteObject(writer); } - // Leaving only the last indirect object in SecurityHandler is sufficient - effectiveSecurityHandler?.LeaveObject(); - - // ---------- MUDANÇA CRÍTICA ---------- - // Agora que os objetos novos (incluindo placeholders) foram escritos no stream, - // podemos calcular a assinatura (ela depende das posições/offsets já escritas). - if (_digitalSignatureHandler != null) - { - await _digitalSignatureHandler.ComputeSignatureAndRange(writer).ConfigureAwait(false); - // ComputeSignatureAndRange deve escrever a assinatura no placeholder (conteúdo /Contents). - } - - // Agora gravamos o xref subseção e trailer (baseado na posição atual do writer). - var startXRef = (SizeType)writer.Position; - - // Escreve apenas a xref subseção relativa aos irefs já preparados. + pdfStandardSecurityHandler?.LeaveObject(); + long position = writer.Position; IrefTable.WriteObject(writer); writer.WriteRaw("trailer\n"); - - // Se incremental: preencher /Prev com startxref anterior e calcular /Size adequadamente. - if (_incrementalSave && _previousStartXref >= 0) - { - // Tentar extrair o previousSize do trailer existente no arquivo - int previousSize = GetPreviousTrailerSizeFromFile(writer.FullPath ?? "", _previousStartXref); - if (previousSize > 0) - { - // new size = previousSize + número de objetos adicionados - // count representa o número total de referências IrefTable.AllReferences no documento atual; - // se previousSize for válido, calculamos incremento. Aqui usamos um fallback conservador. - Trailer.Elements.SetInteger("/Size", previousSize + (count + 1)); - } - else - { - Trailer.Elements.SetInteger("/Size", count + 1); - } - - // Set /Prev to previous startxref - // Nota: Trailer.Elements aceita chaves string diretamente - if (!Trailer.Elements.ContainsKey("/Prev")) - Trailer.Elements.Add("/Prev", new PdfInteger((int)_previousStartXref)); - else - Trailer.Elements.SetInteger("/Prev", (int)_previousStartXref); - } - else - { - Trailer.Elements.SetInteger("/Size", count + 1); - } - + Trailer.Elements.SetInteger("/Size", num + 1); Trailer.WriteObject(writer); - writer.WriteEof(this, startXRef); - - // If incremental, flush and leave stream at end + writer.WriteEof(this, position); + if (_digitalSignatureHandler != null) + await _digitalSignatureHandler.ComputeSignatureAndRange(writer).ConfigureAwait(continueOnCapturedContext: false); + if (_incrementalSave) { writer.Stream.Flush(); @@ -606,68 +500,25 @@ void PrepareForPdfA() // Just a first hack. internal override void PrepareForSave() { PdfDocumentInformation info = Info; - - // The Creator is called 'Application' in Acrobat. - // The Producer is call "Created by" in Acrobat. - - // Set Creator if value is undefined. This is the 'application' in Adobe Reader. - if (info.Elements[PdfDocumentInformation.Keys.Creator] is null) + if (info.Elements["/Creator"] == null) info.Creator = PdfSharpProductVersionInformation.Producer; - - // We set Producer if it is not yet set. - var pdfProducer = PdfSharpProductVersionInformation.Creator; -#if DEBUG - // Add OS suffix only in DEBUG build. - pdfProducer += $" under {RuntimeInformation.OSDescription}"; -#endif - // 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})"; - } - info.Elements.SetString(PdfDocumentInformation.Keys.Producer, producer); - - // Prepare used fonts. + + string creator = PdfSharpProductVersionInformation.Creator; + string text = info.Producer; + if (text.Length == 0) + text = creator; + else if (!text.StartsWith("PDFsharp", StringComparison.Ordinal)) + text = creator + " (Original: " + text + ")"; + + info.Elements.SetString("/Producer", text); _fontTable?.PrepareForSave(); - - // Let catalog do the rest. Catalog.PrepareForSave(); - - // # Remoção de unreachable objects - Apenas quando NÃO estamos em incremental mode. - if (!_incrementalSave) - { - int removed = IrefTable.Compact(); - if (removed != 0 && PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Information)) - { - PdfSharpLogHost.Logger.LogInformation($"PrepareForSave: Number of deleted unreachable objects: {removed}"); - } - IrefTable.Renumber(); - } - else - { - if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - PdfSharpLogHost.Logger.LogDebug("PrepareForSave: incremental save -> skipping Compact/Renumber to preserve existing object numbers."); - } - - // #PDF-UA - // Create PdfMetadata now to include the final document information in XMP generation. - // BUT: do NOT overwrite existing Metadata, pois isso quebra PDF/A. - if (Catalog.Elements.GetObject(PdfCatalog.Keys.Metadata) == null) - { - Catalog.Elements.SetReference(PdfCatalog.Keys.Metadata, new PdfMetadata(this)); - } - else - { - if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - PdfSharpLogHost.Logger.LogDebug("PrepareForSave: existing Metadata found -> not overwriting XMP."); - } + int num = IrefTable.Compact(); + if (num != 0 && PdfSharpLogHost.Logger.IsEnabled(LogLevel.Information)) + PdfSharpLogHost.Logger.LogInformation($"PrepareForSave: Number of deleted unreachable objects: {num}"); + + IrefTable.Renumber(); + Catalog.Elements.SetReference("/Metadata", new PdfMetadata(this)); } /// From c25479528c69b52a7799a3abf7797bfda2e924ba Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Fri, 7 Nov 2025 13:45:18 -0300 Subject: [PATCH 03/11] =?UTF-8?q?Feature:=20tratamento=20de=20erros=20para?= =?UTF-8?q?=20habilitar=20o=20pdfa=20em=20arquivos=20sem=20assinatura=20an?= =?UTF-8?q?terior,=20ou=20que=20j=C3=A1=20eram=20pdfa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 172 +++++++++++++----- 1 file changed, 128 insertions(+), 44 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index f8d95d57..b59016e2 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -204,10 +204,25 @@ static int GetPreviousTrailerSizeFromFile(string path, long startxref) /// /// Temporary hack to set a value that tells PDFsharp to create a PDF/A conform document. /// - public void SetPdfA() // HACK_OLD + public void SetPdfA() { + // marca intenção PDF/A antes de qualquer construção de UAManager _isPdfA = true; - _ = UAManager.ForDocument(this); + + // Tentar criar o UAManager, mas não falhar se houver PDFs com estrutura inesperada. + try + { + // UAManager pode lançar se o catálogo estiver em estado inesperado. + // Colocamos em try/catch para não quebrar documentos já PDF/A assinados. + _ = UAManager.ForDocument(this); + } + catch (Exception ex) + { + // Logue para diagnóstico, mas não deixe falhar a execução. + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning)) + PdfSharpLogHost.Logger.LogWarning($"SetPdfA: UAManager.ForDocument failed: {ex.Message}"); + // Mantemos _isPdfA = true — PrepareForPdfA() será defensivo. + } } /// @@ -443,55 +458,124 @@ async Task DoSaveAsync(PdfWriter writer) } } - void PrepareForPdfA() // Just a first hack. + void PrepareForPdfA() { + // Safety: do minimal changes and avoid throwing for already-conform documents. var internals = Internals; - Debug.Assert(_uaManager != null); - // UAManager sets MarkInformation. - if (_uaManager == null) + // Ensure UA manager exists if possible, but don't crash when it fails. + try + { + if (_uaManager == null) + _ = UAManager.ForDocument(this); + } + catch (Exception ex) { - // 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: UAManager.ForDocument() failed: {ex.Message}"); + // continue — we'll still try to add OutputIntents safely + } + + // If catalog already has OutputIntents, assume PDF/A intent already set -> skip adding. + if (Catalog.Elements.GetObject(PdfCatalog.Keys.OutputIntents) != null) + { + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + PdfSharpLogHost.Logger.LogDebug("PrepareForPdfA: OutputIntents already present -> skip."); + return; + } + + // Create OutputIntents array (do not call Elements.Add blindly) + PdfArray outputIntentsArray = new PdfArray(this); + + // Create outputIntent dictionary + 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"); + + // Build ICC profile stream object only if not already present + PdfDictionary profileObject = null; + try + { + // Try to reuse existing profile in document if present + var existing = FindExistingOutputProfile(internals); + if (existing != null) + { + profileObject = existing; + } + else + { + // Load resource + var profileStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("PdfSharp.Resources.sRGB2014.icc") + ?? throw new InvalidOperationException("Embedded color profile was not found."); + + 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 fd = new FlateDecode(); + byte[] profileCompressed = fd.Encode(profile, Options.FlateEncodeMode); + + profileObject = new PdfDictionary(this); + IrefTable.Add(profileObject); + + // Set stream value safely (use Set operations to avoid duplicate-key) + profileObject.Stream = new PdfDictionary.PdfStream(profileCompressed, profileObject); + profileObject.Elements.SetInteger("/N", 3); + profileObject.Elements.SetInteger("/Length", profileCompressed.Length); + profileObject.Elements.SetName("/Filter", "/FlateDecode"); + } - markInfo.Elements.SetBoolean(PdfMarkInformation.Keys.Marked, true); - //internals.Catalog.Elements.SetReference(PdfCatalog.Keys.MarkInfo, markInfo); - internals.Catalog.Elements.Add(PdfCatalog.Keys.MarkInfo, markInfo); + // Link dest output profile (use SetReference to avoid duplicate Adds) + outputIntent.Elements.SetReference("/DestOutputProfile", profileObject.Reference); + } + catch (Exception ex) + { + // If something goes wrong with ICC embedding, log and continue without failing save. + if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning)) + PdfSharpLogHost.Logger.LogWarning($"PrepareForPdfA: ICC embedding failed: {ex.Message}"); } - var outputIntentsArray = new PdfArray(this); - //internals.AddObject(outputIntentsArray); - var outputIntents = new PdfDictionary(this); - outputIntentsArray.Elements.Add(outputIntents); - - 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 profileStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("PdfSharp.Resources.sRGB2014.icc") - ?? throw new InvalidOperationException("Embedded color profile was not found."); - - 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 fd = new FlateDecode(); - byte[] profileCompressed = fd.Encode(profile, Options.FlateEncodeMode); - - 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"); - - outputIntents.Elements.Add("/DestOutputProfile", profileObject.Reference); - //internals.Catalog.Elements.SetReference(PdfCatalog.Keys.OutputIntents, outputIntentsArray); - internals.Catalog.Elements.Add(PdfCatalog.Keys.OutputIntents, outputIntentsArray); + // Finally set OutputIntents in catalog safely — use SetValue/SetReference not Add to avoid duplicate key + // If there is already a value for /OutputIntents (race), replace it. + 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); + } + + 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; } /// From 530f28c0489e5dd84e9f1b541b5f00b0b13cfa57 Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Fri, 7 Nov 2025 16:21:44 -0300 Subject: [PATCH 04/11] =?UTF-8?q?Feature:=20corre=C3=A7=C3=A3o=20de=20erro?= =?UTF-8?q?s=20de=20pdfa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pdf.Signatures/PdfSignatureHandler.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) 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 cb80996b..e328285a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -292,17 +292,25 @@ PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) signatureField.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle)); - signatureField.CustomAppearanceHandler = Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() + // Se vazio, define para imprimir (requisito PDF/A para SigField) + signatureField.Elements.SetInteger("/F", 4); + + // Evita agrupamento transparente proibido pelo PDF/A + if (Document.Pages.Count > 0) { - Location = Options.Location, - Reason = Options.Reason, - 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. + var page = Document.Pages[Options.PageIndex]; + if (page?.Elements?.ContainsKey("/Group") == true) + { + var group = page.Elements.GetDictionary("/Group"); + if (group.Elements.GetName("/S") == "/Transparency") + { + // Remove transparência para compatibilidade com PDF/A + group.Elements.Remove("/S"); + } + } + } Document.Internals.AddObject(signatureField); - return signatureField; } From 324bccb4e76f6d46c4d4df592096651bfe1100d3 Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Mon, 10 Nov 2025 12:31:06 -0300 Subject: [PATCH 05/11] =?UTF-8?q?Feature:=20ajuste=20de=20erros=20ao=20val?= =?UTF-8?q?idar=20o=20n=C3=ADvel=20de=20conformidade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pdf.Signatures/PdfSignatureHandler.cs | 28 ++++++++----------- .../PDFsharp/src/PdfSharp/Pdf/PdfMetadata.cs | 5 +++- .../src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs | 19 +++++++++++-- 3 files changed, 32 insertions(+), 20 deletions(-) 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 e328285a..c57d1f7a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -291,26 +291,22 @@ PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) signatureField.Elements.Add("/P", Document.Pages[Options.PageIndex]); signatureField.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle)); + + signatureField.CustomAppearanceHandler = Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() + { + Location = Options.Location, + Reason = Options.Reason, + Signer = Signer.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 + // Se vazio, define para imprimir (requisito PDF/A para SigField) - signatureField.Elements.SetInteger("/F", 4); - - // Evita agrupamento transparente proibido pelo PDF/A - if (Document.Pages.Count > 0) - { - var page = Document.Pages[Options.PageIndex]; - if (page?.Elements?.ContainsKey("/Group") == true) - { - var group = page.Elements.GetDictionary("/Group"); - if (group.Elements.GetName("/S") == "/Transparency") - { - // Remove transparência para compatibilidade com PDF/A - group.Elements.Remove("/S"); - } - } - } + signatureField.Elements.SetInteger("/F", 4); Document.Internals.AddObject(signatureField); + return signatureField; } 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..328d440f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs @@ -805,8 +805,23 @@ string IContentStream.GetFormName(XForm form) internal override void WriteObject(PdfWriter writer) { - // #PDF-A + // NOVO TRECHO (CORREÇÃO DE TRANSPARÊNCIA PROIBIDA PELO PDF/A) + // Se o documento é PDF/A, removemos a entrada "/Group" antes de escrevermos o objeto. + if (_document.IsPdfA) + { + // Se a página contém uma chave de grupo, removemos para garantir PDF/A-1A/1B. + // O valor do XMP (Objeto 17) deve ser o que define a conformidade, e o WriteObject não deve + // escrever nada que contradiga essa conformidade. + if (Elements.ContainsKey(Keys.Group)) + { + Elements.Remove(Keys.Group); + } + } + + // #PDF-A (Lógica original do PDFSharp) // Suppress transparency group if PDF-A is required. + // **NOTA:** O bloco "if (!_document.IsPdfA)" abaixo só adiciona a transparência se não for PDF/A. + // No entanto, se o PDF for lido com o grupo já existente, ele precisa ser limpo acima. if (!_document.IsPdfA) { // Add transparency group to prevent rendering problems of Adobe viewer. @@ -823,10 +838,8 @@ internal override void WriteObject(PdfWriter writer) group.Elements.SetName("/CS", "/DeviceRGB"); else group.Elements.SetName("/CS", "/DeviceCMYK"); - // #PDF-A group.Elements.SetName("/S", "/Transparency"); - //False is default: group.Elements["/I"] = new PdfBoolean(false); //False is default: group.Elements["/K"] = new PdfBoolean(false); } From 558f8a58378cd84277cd9d902d38f6c81c5cb979 Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Wed, 12 Nov 2025 16:26:24 -0300 Subject: [PATCH 06/11] =?UTF-8?q?Feature:=20adequa=C3=A7=C3=A3o=20das=20cl?= =?UTF-8?q?asses=20PdfDocument=20e=20PdfsignatureHandler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pdf.Signatures/PdfSignatureHandler.cs | 249 ++++++++++-------- .../PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 224 ++++++++-------- 2 files changed, 238 insertions(+), 235 deletions(-) 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 c57d1f7a..953bfed4 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -8,8 +8,6 @@ using PdfSharp.Pdf.Advanced; using PdfSharp.Pdf.Internal; using PdfSharp.Pdf.IO; -using System.Text; -using System.Text.RegularExpressions; namespace PdfSharp.Pdf.Signatures { @@ -69,72 +67,81 @@ internal async Task ComputeSignatureAndRange(PdfWriter writer) await ComputeIncrementalSignatureAsync(writer.Stream).ConfigureAwait(continueOnCapturedContext: false); return; } - var (stream, obj) = GetRangeToSignAndByteRangeArray(writer.Stream); - _signatureFieldByteRangePlaceholder.WriteActualObject(obj, writer); - byte[] array = await Signer.GetSignatureAsync(stream).ConfigureAwait(continueOnCapturedContext: false); - int size = _placeholderItem.Size; - if (array.Length > size) - { - throw new Exception($"The actual digest length {array.Length} is larger than the approximation made {size}. Not enough room in the placeholder to fit the signature."); - } + + (RangedStream rangedStreamToSign, PdfArray byteRangeArray) = GetRangeToSignAndByteRangeArray(writer.Stream); + + Debug.Assert(_signatureFieldByteRangePlaceholder != null); + _signatureFieldByteRangePlaceholder!.WriteActualObject(byteRangeArray, writer); + + // Computing signature from document’s digest. + 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."); + + // Write the signature at the space reserved by placeholder item. writer.Stream.Position = _placeholderItem.StartPosition; + + // When the signature includes a timestamp, the exact length is unknown until the signature is definitely calculated. + // Therefore, we write the angle brackets here and override the placeholder white spaces. writer.WriteRaw('<'); - writer.Write(PdfEncoders.RawEncoding.GetBytes(FormatHex(array))); - for (int i = array.Length; i < size; i++) - { + writer.Write(PdfEncoders.RawEncoding.GetBytes(FormatHex(signature))); + + // Fill up the allocated placeholder. Signature is sometimes considered invalid if there are spaces after '>'. + for (int i = signature.Length; i < expectedLength; i++) writer.WriteRaw("00"); - } + writer.WriteRaw('>'); } /// - /// Minimal placeholder for incremental-sign attempt. - /// This function is intentionally conservative: it tries to update the file - /// in-place only if the placeholder offsets appear valid. A robust incremental - /// implementation requires writing incremental xref & trailer; we'll extend - /// this later. For now this helper tries to write the signature bytes at the - /// reserved placeholder offset in the existing file (works when offsets align). + /// Writes the computed digital signature into the existing PDF stream during an incremental signing operation. /// - // Antes (exemplo): - // internal async Task ComputeIncrementalSignatureAsync(string path, Stream ignored) { ... } - - // Depois: novo método que usa o stream já aberto. + /// + /// 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 rangedStream, PdfArray byteRangeArray) rangeToSignAndByteRangeArray = GetRangeToSignAndByteRangeArray(targetStream); - RangedStream item = rangeToSignAndByteRangeArray.rangedStream; - PdfArray item2 = rangeToSignAndByteRangeArray.byteRangeArray; + + (RangedStream rangedStreamToSign, PdfArray byteRangeArray) = GetRangeToSignAndByteRangeArray(targetStream); + PdfWriter writer = new PdfWriter(targetStream, Document, null) { Layout = PdfWriterLayout.Compact }; - _signatureFieldByteRangePlaceholder.WriteActualObject(item2, writer); - byte[] array = await Signer.GetSignatureAsync(item).ConfigureAwait(continueOnCapturedContext: false); - int size = _placeholderItem.Size; - if (array.Length > size) - { - throw new Exception($"Actual signature length {array.Length} exceeds placeholder {size}."); - } + + 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(array, unicode: false, prefix: false, null); + + 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 = array.Length; i < size; i++) + + 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(); } @@ -190,89 +197,97 @@ string FormatHex(byte[] bytes) // ...use RawEncoder 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)."); - } - _placeholderItem = new PdfSignaturePlaceholderItem(await Signer.GetSignatureSizeAsync().ConfigureAwait(continueOnCapturedContext: false)); - _signatureFieldByteRangePlaceholder = new PdfPlaceholderObject(36); + + int signatureSize = await Signer.GetSignatureSizeAsync().ConfigureAwait(false); + _placeholderItem = new PdfSignaturePlaceholderItem(signatureSize); + _signatureFieldByteRangePlaceholder = new PdfPlaceholderObject(ByteRangePlaceholderLength); + PdfSignature2 signatureDictionary = GetSignatureDictionary(_placeholderItem, _signatureFieldByteRangePlaceholder); if (Options.AppendSignature) { - PdfCatalog catalog = Document.Catalog; - if (catalog.Elements.GetObject("/AcroForm") == null) - { - catalog.Elements.Add("/AcroForm", new PdfAcroForm(Document)); - } - PdfAcroForm acroForm = catalog.AcroForm; - if (!acroForm.Elements.ContainsKey("/SigFlags")) - { - acroForm.Elements.Add("/SigFlags", new PdfInteger(3)); - } - else if (acroForm.Elements.GetInteger("/SigFlags") < 3) - { - acroForm.Elements.SetInteger("/SigFlags", 3); - } - int valueOrDefault = (acroForm.Fields?.Elements?.Count).GetValueOrDefault(); - PdfDictionary pdfDictionary = new PdfDictionary(Document); - pdfDictionary.Elements["/FT"] = new PdfName("/Sig"); - pdfDictionary.Elements["/T"] = new PdfString($"Signature{valueOrDefault + 1}"); - pdfDictionary.Elements["/V"] = signatureDictionary; - pdfDictionary.Elements["/Ff"] = new PdfInteger(4); - pdfDictionary.Elements["/Type"] = new PdfName("/Annot"); - pdfDictionary.Elements["/Subtype"] = new PdfName("/Widget"); - pdfDictionary.Elements["/Rect"] = new PdfRectangle(Options.Rectangle); - pdfDictionary.Elements["/P"] = Document.Pages[Options.PageIndex].Reference; - Document.Internals.AddObject(pdfDictionary); - if (acroForm.Elements["/Fields"] is PdfArray pdfArray) - { - pdfArray.Elements.Add(pdfDictionary.Reference); - } - else - { - PdfArray pdfArray2 = new PdfArray(Document); - pdfArray2.Elements.Add(pdfDictionary.Reference); - acroForm.Elements["/Fields"] = pdfArray2; - } - if (!acroForm.Elements.ContainsKey("/DR")) - { - acroForm.Elements.Add("/DR", new PdfDictionary(Document)); - } - if (!acroForm.Elements.ContainsKey("/DA")) - { - acroForm.Elements.Add("/DA", new PdfString("/Helv 0 Tf 0 g")); - } + AddIncrementalSignatureComponents(signatureDictionary); + return; } else { PdfSignatureField signatureField = GetSignatureField(signatureDictionary); - PdfArray array = Document.Pages[Options.PageIndex].Elements.GetArray("/Annots"); - if (array == null) - { - Document.Pages[Options.PageIndex].Elements.Add("/Annots", new PdfArray(Document, signatureField)); - } + + 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 - { - array.Elements.Add(signatureField); - } - PdfCatalog catalog2 = Document.Catalog; - if (catalog2.Elements.GetObject("/AcroForm") == null) - { - catalog2.Elements.Add("/AcroForm", new PdfAcroForm(Document)); - } - if (!catalog2.AcroForm.Elements.ContainsKey("/SigFlags")) - { - catalog2.AcroForm.Elements.Add("/SigFlags", new PdfInteger(3)); - } - else if (catalog2.AcroForm.Elements.GetInteger("/SigFlags") < 3) - { - catalog2.AcroForm.Elements.SetInteger("/SigFlags", 3); - } - if (catalog2.AcroForm.Elements.GetValue("/Fields") == null) - { - catalog2.AcroForm.Elements.SetValue("/Fields", new PdfAcroField.PdfAcroFieldCollection(new PdfArray())); - } - catalog2.AcroForm.Fields.Elements.Add(signatureField); + annotations.Elements.Add(signatureField); + + PdfCatalog catalog = Document.Catalog; + + 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); + + 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 + { + PdfArray pdfArray2 = new PdfArray(Document); + pdfArray2.Elements.Add(pdfDictionary.Reference); + acroForm.Elements.SetValue(PdfAcroForm.Keys.Fields, pdfArray2); } + + 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) @@ -291,7 +306,7 @@ PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) signatureField.Elements.Add("/P", Document.Pages[Options.PageIndex]); signatureField.Elements.Add("/Rect", new PdfRectangle(Options.Rectangle)); - + signatureField.CustomAppearanceHandler = Options.AppearanceHandler ?? new DefaultSignatureAppearanceHandler() { Location = Options.Location, @@ -301,15 +316,17 @@ PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) // 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 - + // Se vazio, define para imprimir (requisito PDF/A para SigField) - signatureField.Elements.SetInteger("/F", 4); + 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 b59016e2..436d82e4 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -15,14 +15,10 @@ using PdfSharp.Pdf.Signatures; using PdfSharp.Pdf.Structure; using PdfSharp.UniversalAccessibility; -using System; -using System.Diagnostics; -using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; // ReSharper disable InconsistentNaming // ReSharper disable ConvertPropertyToExpressionBody @@ -145,56 +141,6 @@ void Dispose(bool disposing) _state = DocumentState.Disposed | DocumentState.Saved; } - // --- ADDITIONAL FIELDS FOR INCREMENTAL SUPPORT --- - internal long _previousStartXref = -1; // -1 means unknown - - // Helper: get startxref position from an existing file by scanning tail - static long GetStartXrefFromFile(string path) - { - try - { - using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - const int tailRead = 8192; - int toRead = (int)Math.Min(tailRead, fs.Length); - fs.Seek(-toRead, SeekOrigin.End); - byte[] tail = new byte[toRead]; - fs.Read(tail, 0, toRead); - string tailStr = Encoding.ASCII.GetString(tail); - int ix = tailStr.LastIndexOf("startxref", StringComparison.OrdinalIgnoreCase); - if (ix < 0) return -1; - string after = tailStr.Substring(ix); - var m = Regex.Match(after, @"startxref\s*(\d+)", RegexOptions.IgnoreCase); - if (m.Success && long.TryParse(m.Groups[1].Value, out long pos)) - return pos; - } - catch - { - // ignore and return -1 - } - return -1; - } - - static int GetPreviousTrailerSizeFromFile(string path, long startxref) - { - if (startxref < 0) return -1; - try - { - using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - fs.Seek(startxref, SeekOrigin.Begin); - using var sr = new StreamReader(fs, Encoding.ASCII, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true); - string text = sr.ReadToEnd(); - var m = Regex.Match(text, @"trailer[\s\S]*?/Size\s+(\d+)", RegexOptions.IgnoreCase); - if (m.Success && int.TryParse(m.Groups[1].Value, out int size)) - return size; - } - catch - { - // ignore - } - return -1; - } - - /// /// Gets or sets a user-defined object that contains arbitrary information associated with this document. /// The tag is not used by PDFsharp. @@ -204,24 +150,18 @@ static int GetPreviousTrailerSizeFromFile(string path, long startxref) /// /// Temporary hack to set a value that tells PDFsharp to create a PDF/A conform document. /// - public void SetPdfA() + public void SetPdfA() // HACK_OLD { - // marca intenção PDF/A antes de qualquer construção de UAManager _isPdfA = true; - // Tentar criar o UAManager, mas não falhar se houver PDFs com estrutura inesperada. try { - // UAManager pode lançar se o catálogo estiver em estado inesperado. - // Colocamos em try/catch para não quebrar documentos já PDF/A assinados. _ = UAManager.ForDocument(this); } catch (Exception ex) { - // Logue para diagnóstico, mas não deixe falhar a execução. if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning)) PdfSharpLogHost.Logger.LogWarning($"SetPdfA: UAManager.ForDocument failed: {ex.Message}"); - // Mantemos _isPdfA = true — PrepareForPdfA() será defensivo. } } @@ -314,20 +254,22 @@ public async Task SaveAsync(string path) if (!CanModify) throw new InvalidOperationException(PsMsgs.CannotModify); - bool flag = _digitalSignatureHandler?.Options.AppendSignature ?? false; - FileAccess access = (flag ? FileAccess.ReadWrite : ((_digitalSignatureHandler == null) ? FileAccess.Write : FileAccess.ReadWrite)); - FileMode mode = (flag ? FileMode.Open : FileMode.Create); + 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 (flag) + + if (isIncremental) { stream.Seek(0L, SeekOrigin.End); _incrementalSave = true; } - await SaveAsync(stream).ConfigureAwait(continueOnCapturedContext: false); + await SaveAsync(stream).ConfigureAwait(false); } - /// /// Saves the document to the specified stream. /// @@ -345,28 +287,35 @@ public void Save(Stream stream, bool closeStream = false) public async Task SaveAsync(Stream stream, bool closeStream = false) { EnsureNotYetSaved(); + 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)) throw new PdfSharpException(message); + // Get security handler if document gets encrypted. + Debug.Assert(SecuritySettings.EffectiveSecurityHandler != null); PdfStandardSecurityHandler effectiveSecurityHandler = SecuritySettings.EffectiveSecurityHandler; - PdfWriter writer = null; + + PdfWriter? writer = null; try { - writer = new PdfWriter(stream, _document, effectiveSecurityHandler); + Debug.Assert(ReferenceEquals(_document, this)); + writer = new (stream, _document, effectiveSecurityHandler); if (stream is FileStream { Name: not null } fileStream) writer.FullPath = fileStream.Name; - await DoSaveAsync(writer).ConfigureAwait(continueOnCapturedContext: false); + await DoSaveAsync(writer).ConfigureAwait(false); } finally { @@ -388,62 +337,82 @@ public async Task SaveAsync(Stream stream, bool closeStream = false) async Task DoSaveAsync(PdfWriter writer) { PdfSharpLogHost.Logger.PdfDocumentSaved(Name); + if (_pages == null || _pages.Count == 0) { if (OutStream != null) + { + // Give feedback if the wrong constructor was used. throw new InvalidOperationException("Cannot save a PDF document with no pages. Do not use \"public PdfDocument(string filename)\" or \"public PdfDocument(Stream outputStream)\" if you want to open an existing PDF document from a file or stream; use PdfReader.Open() for that purpose."); - + } throw new InvalidOperationException("Cannot save a PDF document with no pages."); } try { + // Prepare for signing. if (_digitalSignatureHandler != null) - await _digitalSignatureHandler.AddSignatureComponentsAsync().ConfigureAwait(continueOnCapturedContext: false); - - if (Trailer is PdfCrossReferenceStream trailer) - Trailer = new PdfTrailer(trailer); - - PdfStandardSecurityHandler pdfStandardSecurityHandler = _securitySettings?.EffectiveSecurityHandler; - if (pdfStandardSecurityHandler != null) + await _digitalSignatureHandler.AddSignatureComponentsAsync().ConfigureAwait(false); + + // Remove XRefTrailer + if (Trailer is PdfCrossReferenceStream crossReferenceStream) + Trailer = new PdfTrailer(crossReferenceStream); + + var effectiveSecurityHandler = _securitySettings?.EffectiveSecurityHandler; + if (effectiveSecurityHandler != null) { - if (pdfStandardSecurityHandler.Reference == null) - IrefTable.Add(pdfStandardSecurityHandler); - - Trailer.Elements["/Encrypt"] = _securitySettings.SecurityHandler.Reference; + if (effectiveSecurityHandler.Reference == null) + IrefTable.Add(effectiveSecurityHandler); + else + Debug.Assert(IrefTable.Contains(effectiveSecurityHandler.ObjectID)); + Trailer.Elements[PdfTrailer.Keys.Encrypt] = _securitySettings!.SecurityHandler.Reference; } else - Trailer.Elements.Remove("/Encrypt"); + Trailer.Elements.Remove(PdfTrailer.Keys.Encrypt); PrepareForSave(); - pdfStandardSecurityHandler?.PrepareForWriting(); + effectiveSecurityHandler?.PrepareForWriting(); + if (_incrementalSave) writer.Stream.Seek(0L, SeekOrigin.End); - - if (!_incrementalSave) + else writer.WriteFileHeader(this); - PdfReference[] allReferences = IrefTable.AllReferences; - int num = allReferences.Length; - for (int i = 0; i < num; i++) + PdfReference[] irefs = IrefTable.AllReferences; + int count = irefs.Length; + for (int i = 0; i < count; i++) { - PdfReference obj = allReferences[i]; - obj.Position = writer.Position; - PdfObject value = obj.Value; - pdfStandardSecurityHandler?.EnterObject(value.ObjectID); - value.WriteObject(writer); + PdfReference iref = irefs[i]; +#if DEBUG_ + if (iref.ObjectNumber == 378) + _ = typeof(int); +#endif + iref.Position = writer.Position; + + 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); } - pdfStandardSecurityHandler?.LeaveObject(); - long position = writer.Position; + // Leaving only the last indirect object in SecurityHandler is sufficient, as this is the first time no indirect object is entered anymore. + effectiveSecurityHandler?.LeaveObject(); + + // ReSharper disable once RedundantCast. Redundant only if 64 bit. + long startXRef = (SizeType)writer.Position; IrefTable.WriteObject(writer); writer.WriteRaw("trailer\n"); - Trailer.Elements.SetInteger("/Size", num + 1); + Trailer.Elements.SetInteger("/Size", count + 1); Trailer.WriteObject(writer); - writer.WriteEof(this, position); + writer.WriteEof(this, startXRef); + + // #Signature: What about encryption + signing ?? + // Prepare for signing. if (_digitalSignatureHandler != null) - await _digitalSignatureHandler.ComputeSignatureAndRange(writer).ConfigureAwait(continueOnCapturedContext: false); + await _digitalSignatureHandler.ComputeSignatureAndRange(writer).ConfigureAwait(false); if (_incrementalSave) { @@ -458,12 +427,10 @@ async Task DoSaveAsync(PdfWriter writer) } } - void PrepareForPdfA() + void PrepareForPdfA() // Just a first hack. { - // Safety: do minimal changes and avoid throwing for already-conform documents. var internals = Internals; - // Ensure UA manager exists if possible, but don't crash when it fails. try { if (_uaManager == null) @@ -473,7 +440,6 @@ void PrepareForPdfA() { if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) PdfSharpLogHost.Logger.LogDebug($"PrepareForPdfA: UAManager.ForDocument() failed: {ex.Message}"); - // continue — we'll still try to add OutputIntents safely } // If catalog already has OutputIntents, assume PDF/A intent already set -> skip adding. @@ -498,7 +464,7 @@ void PrepareForPdfA() outputIntent.Elements.SetString("/Info", "Creator: ColorOrg Manufacturer:IEC Model:sRGB"); // Build ICC profile stream object only if not already present - PdfDictionary profileObject = null; + PdfDictionary? profileObject = null; try { // Try to reuse existing profile in document if present @@ -532,6 +498,7 @@ void PrepareForPdfA() } // Link dest output profile (use SetReference to avoid duplicate Adds) + Debug.Assert(profileObject.Reference != null); outputIntent.Elements.SetReference("/DestOutputProfile", profileObject.Reference); } catch (Exception ex) @@ -584,25 +551,44 @@ void PrepareForPdfA() internal override void PrepareForSave() { PdfDocumentInformation info = Info; - if (info.Elements["/Creator"] == null) + + // The Creator is called 'Application' in Acrobat. + // The Producer is call "Created by" in Acrobat. + + // Set Creator if value is undefined. This is the 'application' in Adobe Reader. + if (info.Elements[PdfDocumentInformation.Keys.Creator] is null) info.Creator = PdfSharpProductVersionInformation.Producer; - - string creator = PdfSharpProductVersionInformation.Creator; - string text = info.Producer; - if (text.Length == 0) - text = creator; - else if (!text.StartsWith("PDFsharp", StringComparison.Ordinal)) - text = creator + " (Original: " + text + ")"; - - info.Elements.SetString("/Producer", text); + + // We set Producer if it is not yet set. + string pdfProducer = PdfSharpProductVersionInformation.Creator; +#if DEBUG + // Add OS suffix only in DEBUG build. + pdfProducer += $" under {RuntimeInformation.OSDescription}"; +#endif + // 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 if (!producer.StartsWith(PdfSharpProductVersionInformation.Title, StringComparison.Ordinal)) + producer = $"{pdfProducer} (Original: {producer})"; + + info.Elements.SetString(PdfDocumentInformation.Keys.Producer, producer); + _fontTable?.PrepareForSave(); + + // Let catalog do the rest. Catalog.PrepareForSave(); - int num = IrefTable.Compact(); - if (num != 0 && PdfSharpLogHost.Logger.IsEnabled(LogLevel.Information)) - PdfSharpLogHost.Logger.LogInformation($"PrepareForSave: Number of deleted unreachable objects: {num}"); + + // 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(); - Catalog.Elements.SetReference("/Metadata", new PdfMetadata(this)); + + // #PDF-UA + // Create PdfMetadata now to include the final document information in XMP generation. + Catalog.Elements.SetReference(PdfCatalog.Keys.Metadata, new PdfMetadata(this)); } /// From 0b0b8ef615153d66345893ab1429dd63906b0937 Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Wed, 12 Nov 2025 16:42:06 -0300 Subject: [PATCH 07/11] Feature: pequenos ajustes para diminuir o diff dos arquivos --- .../src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs | 3 +-- .../src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) 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 953bfed4..7d91329c 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/PdfSignatureHandler.cs @@ -71,7 +71,7 @@ internal async Task ComputeSignatureAndRange(PdfWriter writer) (RangedStream rangedStreamToSign, PdfArray byteRangeArray) = GetRangeToSignAndByteRangeArray(writer.Stream); Debug.Assert(_signatureFieldByteRangePlaceholder != null); - _signatureFieldByteRangePlaceholder!.WriteActualObject(byteRangeArray, writer); + _signatureFieldByteRangePlaceholder.WriteActualObject(byteRangeArray, writer); // Computing signature from document’s digest. byte[] signature = await Signer.GetSignatureAsync(rangedStreamToSign).ConfigureAwait(false); @@ -313,7 +313,6 @@ PdfSignatureField GetSignatureField(PdfSignature2 signatureDic) Reason = Options.Reason, 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 diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index 436d82e4..873d7a10 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -304,8 +304,7 @@ public async Task SaveAsync(Stream stream, bool closeStream = false) throw new PdfSharpException(message); // Get security handler if document gets encrypted. - Debug.Assert(SecuritySettings.EffectiveSecurityHandler != null); - PdfStandardSecurityHandler effectiveSecurityHandler = SecuritySettings.EffectiveSecurityHandler; + var effectiveSecurityHandler = SecuritySettings.EffectiveSecurityHandler; PdfWriter? writer = null; try @@ -388,8 +387,8 @@ async Task DoSaveAsync(PdfWriter writer) if (iref.ObjectNumber == 378) _ = typeof(int); #endif - iref.Position = writer.Position; - + iref.Position = writer.Position; + PdfObject obj = iref.Value; // Enter indirect object in SecurityHandler to allow object encryption key generation for this object. @@ -574,6 +573,7 @@ internal override void PrepareForSave() info.Elements.SetString(PdfDocumentInformation.Keys.Producer, producer); + // Prepare used fonts. _fontTable?.PrepareForSave(); // Let catalog do the rest. From 023273fdfba1a1405ffdcc2bd9ec2f419076c698 Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Wed, 12 Nov 2025 16:48:34 -0300 Subject: [PATCH 08/11] =?UTF-8?q?Feature:=20remo=C3=A7=C3=A3o=20de=20using?= =?UTF-8?q?=20desnecess=C3=A1rio,=20tradu=C3=A7=C3=A3o=20de=20coment=C3=A1?= =?UTF-8?q?rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 2 -- .../src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs | 15 +++++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index 873d7a10..bdfaef81 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -17,8 +17,6 @@ using PdfSharp.UniversalAccessibility; using System.Reflection; using System.Runtime.InteropServices; -using System.Text; -using System.Text.RegularExpressions; // ReSharper disable InconsistentNaming // ReSharper disable ConvertPropertyToExpressionBody diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs index 328d440f..94126a3e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfPage.cs @@ -805,23 +805,20 @@ string IContentStream.GetFormName(XForm form) internal override void WriteObject(PdfWriter writer) { - // NOVO TRECHO (CORREÇÃO DE TRANSPARÊNCIA PROIBIDA PELO PDF/A) - // Se o documento é PDF/A, removemos a entrada "/Group" antes de escrevermos o objeto. + // 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) { - // Se a página contém uma chave de grupo, removemos para garantir PDF/A-1A/1B. - // O valor do XMP (Objeto 17) deve ser o que define a conformidade, e o WriteObject não deve - // escrever nada que contradiga essa conformidade. + // 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); } } - // #PDF-A (Lógica original do PDFSharp) // Suppress transparency group if PDF-A is required. - // **NOTA:** O bloco "if (!_document.IsPdfA)" abaixo só adiciona a transparência se não for PDF/A. - // No entanto, se o PDF for lido com o grupo já existente, ele precisa ser limpo acima. if (!_document.IsPdfA) { // Add transparency group to prevent rendering problems of Adobe viewer. @@ -838,8 +835,10 @@ internal override void WriteObject(PdfWriter writer) group.Elements.SetName("/CS", "/DeviceRGB"); else group.Elements.SetName("/CS", "/DeviceCMYK"); + // #PDF-A group.Elements.SetName("/S", "/Transparency"); + //False is default: group.Elements["/I"] = new PdfBoolean(false); //False is default: group.Elements["/K"] = new PdfBoolean(false); } From ba2290872cd73a459c7d3b31210ad687d97a2935 Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Thu, 13 Nov 2025 09:28:10 -0300 Subject: [PATCH 09/11] =?UTF-8?q?Feature:=20divis=C3=A3o=20de=20m=C3=A9tod?= =?UTF-8?q?o=20para=20facilitar=20a=20leitura?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 113 +++++++++--------- 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index bdfaef81..0c7d3cd7 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -424,20 +424,11 @@ async Task DoSaveAsync(PdfWriter writer) } } - void PrepareForPdfA() // Just a first hack. + void PrepareForPdfA() { var internals = Internals; - 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}"); - } + InitializePdfAComponents(); // If catalog already has OutputIntents, assume PDF/A intent already set -> skip adding. if (Catalog.Elements.GetObject(PdfCatalog.Keys.OutputIntents) != null) @@ -447,10 +438,7 @@ void PrepareForPdfA() // Just a first hack. return; } - // Create OutputIntents array (do not call Elements.Add blindly) PdfArray outputIntentsArray = new PdfArray(this); - - // Create outputIntent dictionary var outputIntent = new PdfDictionary(this); outputIntentsArray.Elements.Add(outputIntent); @@ -460,57 +448,74 @@ void PrepareForPdfA() // Just a first hack. outputIntent.Elements.SetString("/RegistryName", "http://www.color.org"); outputIntent.Elements.SetString("/Info", "Creator: ColorOrg Manufacturer:IEC Model:sRGB"); - // Build ICC profile stream object only if not already present - PdfDictionary? profileObject = null; + 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); + } + + /// + /// 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 { - // Try to reuse existing profile in document if present var existing = FindExistingOutputProfile(internals); if (existing != null) - { - profileObject = existing; - } - else - { - // Load resource - var profileStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("PdfSharp.Resources.sRGB2014.icc") - ?? throw new InvalidOperationException("Embedded color profile was not found."); - - 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 fd = new FlateDecode(); - byte[] profileCompressed = fd.Encode(profile, Options.FlateEncodeMode); - - profileObject = new PdfDictionary(this); - IrefTable.Add(profileObject); - - // Set stream value safely (use Set operations to avoid duplicate-key) - profileObject.Stream = new PdfDictionary.PdfStream(profileCompressed, profileObject); - profileObject.Elements.SetInteger("/N", 3); - profileObject.Elements.SetInteger("/Length", profileCompressed.Length); - profileObject.Elements.SetName("/Filter", "/FlateDecode"); - } + return existing; - // Link dest output profile (use SetReference to avoid duplicate Adds) - Debug.Assert(profileObject.Reference != null); - outputIntent.Elements.SetReference("/DestOutputProfile", profileObject.Reference); + using var profileStream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("PdfSharp.Resources.sRGB2014.icc") + ?? throw new InvalidOperationException("Embedded color profile was not found."); + + 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 fd = new FlateDecode(); + byte[] profileCompressed = fd.Encode(profile, Options.FlateEncodeMode); + + var profileObject = new PdfDictionary(this); + IrefTable.Add(profileObject); + + profileObject.Stream = new PdfDictionary.PdfStream(profileCompressed, profileObject); + profileObject.Elements.SetInteger("/N", 3); + profileObject.Elements.SetInteger("/Length", profileCompressed.Length); + profileObject.Elements.SetName("/Filter", "/FlateDecode"); + + return profileObject; } catch (Exception ex) { - // If something goes wrong with ICC embedding, log and continue without failing save. if (PdfSharpLogHost.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning)) PdfSharpLogHost.Logger.LogWarning($"PrepareForPdfA: ICC embedding failed: {ex.Message}"); + return null; } - - // Finally set OutputIntents in catalog safely — use SetValue/SetReference not Add to avoid duplicate key - // If there is already a value for /OutputIntents (race), replace it. - 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); } PdfDictionary? FindExistingOutputProfile(PdfInternals internals) From 084ae15089b72a1470b07a1bca8b8b9769370851 Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Mon, 17 Nov 2025 12:12:30 -0300 Subject: [PATCH 10/11] =?UTF-8?q?Feature:=20inclus=C3=A3o=20de=20teste=20d?= =?UTF-8?q?a=20assinatura=20incremental=20e=20padr=C3=A3o=20de=20conformid?= =?UTF-8?q?ade=20pdf/a-1a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pdf/signatures/DefaultSignerTests.cs | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) 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..b69bc8c8 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() { @@ -289,7 +360,8 @@ public void Sign_with_Certificate_from_Store() if (cers.Count > 0) { certificate = cers[0]; - }; + } + ; for (int idx = 0; idx < cers.Count; idx++) { From 579623c88dd534469bfc16e7b76432a94b750e86 Mon Sep 17 00:00:00 2001 From: Alex Vaz Date: Mon, 17 Nov 2025 12:15:38 -0300 Subject: [PATCH 11/11] =?UTF-8?q?Feature:=20revers=C3=A3o=20de=20altera?= =?UTF-8?q?=C3=A7=C3=A3o=20desnecess=C3=A1ria?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/PdfSharp.Tests/Pdf/signatures/DefaultSignerTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 b69bc8c8..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 @@ -360,8 +360,7 @@ public void Sign_with_Certificate_from_Store() if (cers.Count > 0) { certificate = cers[0]; - } - ; + }; for (int idx = 0; idx < cers.Count; idx++) {