diff --git a/PdfSharpCore.Test/IO/ModificationDateTests.cs b/PdfSharpCore.Test/IO/ModificationDateTests.cs
new file mode 100644
index 00000000..19b9011d
--- /dev/null
+++ b/PdfSharpCore.Test/IO/ModificationDateTests.cs
@@ -0,0 +1,125 @@
+using System;
+using System.IO;
+using System.Text;
+using FluentAssertions;
+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.ASCII.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.ASCII.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..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);
}
- // Change modification date
- document.Info.ModificationDate = DateTime.Now;
+ // 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();
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";