From a4febb2dc316d25a0f5c69d6be0bda76fd10bd82 Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Tue, 28 Jul 2026 12:53:43 +1000 Subject: [PATCH 1/3] Date a document when it is written, not when it is opened Opening a document for modification set its modification date to the current time, there and then. Nothing had been modified yet, and nothing need ever be: a caller that opened a document to read its dates was handed a date that its own reading had just written, and a caller that opened one and thought better of it left with a document dated as though it had been edited. There was no way to tell a document that had been changed from one that had only been looked at, which is the one thing a modification date is for. Stamp it in PrepareForSave instead, where the creator and the producer are already filled in, and only for a document PdfReader opened for modification. A document written from scratch is dated by its creation date alone, as it has always been, and carries no /ModDate at all. A date the caller has chosen is left alone, which is what opening used to leave room for by stamping first and saving later. The property records that it was set from outside, and the stamp goes in through the element rather than the property, so that saving twice stamps twice rather than taking the first stamp for a date somebody asked for. The test for what opening reports compares it against what opening the same file read only reports, which never stamped anything. That asks whether opening for modification answers differently, and leaves out of it how a date is spelled in a file and what time zone it comes back in. --- PdfSharpCore.Test/IO/ModificationDateTests.cs | 125 ++++++++++++++++++ PdfSharpCore/Pdf.IO/PdfReader.cs | 4 +- PdfSharpCore/Pdf/PdfDocument.cs | 10 ++ PdfSharpCore/Pdf/PdfDocumentInformation.cs | 15 ++- 4 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 PdfSharpCore.Test/IO/ModificationDateTests.cs diff --git a/PdfSharpCore.Test/IO/ModificationDateTests.cs b/PdfSharpCore.Test/IO/ModificationDateTests.cs new file mode 100644 index 00000000..0ce52b75 --- /dev/null +++ b/PdfSharpCore.Test/IO/ModificationDateTests.cs @@ -0,0 +1,125 @@ +using System; +using System.IO; +using System.Text; +using AwesomeAssertions; +using PdfSharpCore.Pdf; +using PdfSharpCore.Pdf.IO; +using Xunit; + +namespace PdfSharpCore.Test.IO +{ + /// + /// Opening a document for modification stamped it with the current time there and then, before + /// anything had been modified and whether or not it was ever written. Reading a document to look + /// at its dates changed the date being looked at, which left no way to tell a document that had + /// been edited from one that had only been opened. + /// See https://github.com/ststeiger/PdfSharpCore/issues/365. + /// + public class ModificationDateTests + { + private static readonly DateTime ADateInTheFile = new DateTime(2001, 2, 3, 4, 5, 6); + + [Fact] + public void OpeningADocumentForModificationReportsTheDateTheFileCarries() + { + var pdf = ADocumentModifiedOn(ADateInTheFile); + + var opened = Pdf.IO.PdfReader.Open(new MemoryStream(pdf, false), PdfDocumentOpenMode.Modify); + + // Reading the same file without asking to modify it never stamped anything, so it says what + // the file says. Comparing the two keeps the question away from how a date is spelled in a + // file and asks only whether opening for modification answers differently. + var readOnly = Pdf.IO.PdfReader.Open(new MemoryStream(pdf, false), PdfDocumentOpenMode.ReadOnly); + opened.Info.ModificationDate.Should().Be(readOnly.Info.ModificationDate); + } + + [Fact] + public void OpeningADocumentWithNoModificationDateForModificationDoesNotGiveItOne() + { + var pdf = ADocumentModifiedOn(null); + + var opened = Pdf.IO.PdfReader.Open(new MemoryStream(pdf, false), PdfDocumentOpenMode.Modify); + + opened.Info.ModificationDate.Should().Be(DateTime.MinValue); + } + + [Fact] + public void WritingADocumentOpenedForModificationStampsItWithTheTimeItWasWritten() + { + var pdf = ADocumentModifiedOn(ADateInTheFile); + var document = Pdf.IO.PdfReader.Open(new MemoryStream(pdf, false), PdfDocumentOpenMode.Modify); + + var before = DateTime.Now; + using var written = new MemoryStream(); + document.Save(written, false); + + document.Info.ModificationDate.Should().BeOnOrAfter(before).And.BeOnOrBefore(DateTime.Now); + Encoding.Latin1.GetString(written.ToArray()).Should().Contain("/ModDate"); + } + + /// + /// The date is stamped by setting the element rather than the property, so that the stamp of + /// one save is not mistaken for a date the caller chose when the next save comes around. + /// + [Fact] + public void WritingADocumentTwiceStampsItTwice() + { + var pdf = ADocumentModifiedOn(ADateInTheFile); + var document = Pdf.IO.PdfReader.Open(new MemoryStream(pdf, false), PdfDocumentOpenMode.Modify); + + using var first = new MemoryStream(); + document.Save(first, false); + var afterTheFirstSave = document.Info.ModificationDate; + + var between = DateTime.Now; + using var second = new MemoryStream(); + document.Save(second, false); + + document.Info.ModificationDate.Should().BeOnOrAfter(between); + afterTheFirstSave.Should().BeOnOrBefore(between); + } + + [Fact] + public void WritingADocumentDoesNotStampOverAModificationDateTheCallerChose() + { + var pdf = ADocumentModifiedOn(ADateInTheFile); + var document = Pdf.IO.PdfReader.Open(new MemoryStream(pdf, false), PdfDocumentOpenMode.Modify); + + var chosen = new DateTime(1999, 12, 31, 23, 59, 58); + document.Info.ModificationDate = chosen; + using var written = new MemoryStream(); + document.Save(written, false); + + document.Info.ModificationDate.Should().Be(chosen); + } + + /// + /// A document that was never read from a file is dated by its creation date alone, as it + /// always has been. Nothing about it has been modified, so there is no modification to date. + /// + [Fact] + public void WritingANewlyAuthoredDocumentDoesNotStampIt() + { + var document = new PdfDocument(); + document.AddPage(); + + using var written = new MemoryStream(); + document.Save(written, false); + + document.Info.ModificationDate.Should().Be(DateTime.MinValue); + Encoding.Latin1.GetString(written.ToArray()).Should().NotContain("/ModDate"); + } + + private static byte[] ADocumentModifiedOn(DateTime? modificationDate) + { + var document = new PdfDocument(); + document.AddPage(); + if (modificationDate.HasValue) + document.Info.ModificationDate = modificationDate.Value; + + using var pdf = new MemoryStream(); + document.Save(pdf, false); + return pdf.ToArray(); + } + } +} diff --git a/PdfSharpCore/Pdf.IO/PdfReader.cs b/PdfSharpCore/Pdf.IO/PdfReader.cs index 2cdb0b90..f1d4db39 100644 --- a/PdfSharpCore/Pdf.IO/PdfReader.cs +++ b/PdfSharpCore/Pdf.IO/PdfReader.cs @@ -582,8 +582,8 @@ public static PdfDocument Open(Stream stream, string password, PdfDocumentOpenMo document.Internals.SecondDocumentID = PdfEncoders.RawEncoding.GetString(agTemp, 0, agTemp.Length); } - // Change modification date - document.Info.ModificationDate = DateTime.Now; + // The modification date is stamped when the document is written, in + // PdfDocument.PrepareForSave. Opening a document does not modify it. // Remove all unreachable objects int removed = document._irefTable.Compact(); diff --git a/PdfSharpCore/Pdf/PdfDocument.cs b/PdfSharpCore/Pdf/PdfDocument.cs index 674ef4b0..741f71b8 100644 --- a/PdfSharpCore/Pdf/PdfDocument.cs +++ b/PdfSharpCore/Pdf/PdfDocument.cs @@ -412,6 +412,16 @@ internal override void PrepareForSave() } info.Elements.SetString(PdfDocumentInformation.Keys.Producer, producer); + // Stamp a document opened for modification with the time it was written. This used to be + // done when the document was opened, which meant that reading one to look at its dates + // changed the very date being looked at, whether or not anything was ever written. Set the + // element rather than the property, so that saving twice stamps twice and neither save is + // taken for a date the caller chose. + // IsImported tells a document PdfReader opened from a newly created one, which shares the + // default open mode of Modify and is dated by its creation date alone, as it always was. + if (IsImported && _openMode == PdfDocumentOpenMode.Modify && !info.ModificationDateIsTheCallersOwn) + info.Elements.SetDateTime(PdfDocumentInformation.Keys.ModDate, DateTime.Now); + // Prepare used fonts. if (_fontTable != null) _fontTable.PrepareForSave(); diff --git a/PdfSharpCore/Pdf/PdfDocumentInformation.cs b/PdfSharpCore/Pdf/PdfDocumentInformation.cs index 55116c30..a6ab948b 100644 --- a/PdfSharpCore/Pdf/PdfDocumentInformation.cs +++ b/PdfSharpCore/Pdf/PdfDocumentInformation.cs @@ -117,9 +117,20 @@ public DateTime CreationDate public DateTime ModificationDate { get { return Elements.GetDateTime(Keys.ModDate, DateTime.MinValue); } - set { Elements.SetDateTime(Keys.ModDate, value); } + set + { + Elements.SetDateTime(Keys.ModDate, value); + ModificationDateIsTheCallersOwn = true; + } } + /// + /// Gets a value indicating whether the caller has set the modification date itself. Writing a + /// document that was opened for modification stamps it with the time it was written, but not + /// over a date the caller has chosen. A date read from a file is not the caller's own. + /// + internal bool ModificationDateIsTheCallersOwn { get; private set; } + // TODO CustomProperties and meta data /// @@ -128,7 +139,7 @@ public DateTime ModificationDate internal sealed class Keys : KeysBase { /// - /// (Optional; PDF 1.1) The document’s title. + /// (Optional; PDF 1.1) The document�s title. /// [KeyInfo(KeyType.String | KeyType.Optional)] public const string Title = "/Title"; From ef641ab23aba6d396d30b28d2df6bc5c6fa03ef7 Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Tue, 28 Jul 2026 14:13:48 +1000 Subject: [PATCH 2/3] Say what the comment can vouch for Opening for modification changes plenty about the document in memory: the block above the comment gives it new document IDs, and the one below drops unreachable objects, flattens the page tree and renumbers. Saying that opening does not modify it overstated what had changed to what anyone reading the file would take as a promise. Narrow it to the date. --- PdfSharpCore/Pdf.IO/PdfReader.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/PdfSharpCore/Pdf.IO/PdfReader.cs b/PdfSharpCore/Pdf.IO/PdfReader.cs index f1d4db39..e42a2813 100644 --- a/PdfSharpCore/Pdf.IO/PdfReader.cs +++ b/PdfSharpCore/Pdf.IO/PdfReader.cs @@ -582,8 +582,9 @@ public static PdfDocument Open(Stream stream, string password, PdfDocumentOpenMo document.Internals.SecondDocumentID = PdfEncoders.RawEncoding.GetString(agTemp, 0, agTemp.Length); } - // The modification date is stamped when the document is written, in - // PdfDocument.PrepareForSave. Opening a document does not modify it. + // The modification date is not stamped here. It is stamped when the document is + // written, in PdfDocument.PrepareForSave, so that opening a document to read its + // dates does not change the date it is read for. // Remove all unreachable objects int removed = document._irefTable.Compact(); From d6cd1a11ee42db51250694732f88ba27c716cb6e Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Tue, 28 Jul 2026 14:50:16 +1000 Subject: [PATCH 3/3] Use the assertion library and the encoding this repository has The change comes from a fork that has since moved to AwesomeAssertions and to target frameworks where Encoding.Latin1 exists. Neither is true here: FluentAssertions is what the test project references, and Encoding.Latin1 arrived in .NET 5 while this project still targets netcoreapp3.1. The bytes being searched for are ASCII either way. --- PdfSharpCore.Test/IO/ModificationDateTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PdfSharpCore.Test/IO/ModificationDateTests.cs b/PdfSharpCore.Test/IO/ModificationDateTests.cs index 0ce52b75..19b9011d 100644 --- a/PdfSharpCore.Test/IO/ModificationDateTests.cs +++ b/PdfSharpCore.Test/IO/ModificationDateTests.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Text; -using AwesomeAssertions; +using FluentAssertions; using PdfSharpCore.Pdf; using PdfSharpCore.Pdf.IO; using Xunit; @@ -54,7 +54,7 @@ public void WritingADocumentOpenedForModificationStampsItWithTheTimeItWasWritten document.Save(written, false); document.Info.ModificationDate.Should().BeOnOrAfter(before).And.BeOnOrBefore(DateTime.Now); - Encoding.Latin1.GetString(written.ToArray()).Should().Contain("/ModDate"); + Encoding.ASCII.GetString(written.ToArray()).Should().Contain("/ModDate"); } /// @@ -107,7 +107,7 @@ public void WritingANewlyAuthoredDocumentDoesNotStampIt() document.Save(written, false); document.Info.ModificationDate.Should().Be(DateTime.MinValue); - Encoding.Latin1.GetString(written.ToArray()).Should().NotContain("/ModDate"); + Encoding.ASCII.GetString(written.ToArray()).Should().NotContain("/ModDate"); } private static byte[] ADocumentModifiedOn(DateTime? modificationDate)