Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions PdfSharpCore.Test/IO/ModificationDateTests.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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");
}

/// <summary>
/// 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.
/// </summary>
[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);
}

/// <summary>
/// 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.
/// </summary>
[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();
}
}
}
5 changes: 3 additions & 2 deletions PdfSharpCore/Pdf.IO/PdfReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 10 additions & 0 deletions PdfSharpCore/Pdf/PdfDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
15 changes: 13 additions & 2 deletions PdfSharpCore/Pdf/PdfDocumentInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

/// <summary>
/// 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.
/// </summary>
internal bool ModificationDateIsTheCallersOwn { get; private set; }

// TODO CustomProperties and meta data

/// <summary>
Expand All @@ -128,7 +139,7 @@ public DateTime ModificationDate
internal sealed class Keys : KeysBase
{
/// <summary>
/// (Optional; PDF 1.1) The document’s title.
/// (Optional; PDF 1.1) The document�s title.
/// </summary>
[KeyInfo(KeyType.String | KeyType.Optional)]
public const string Title = "/Title";
Expand Down
Loading