Skip to content

Add append-only incremental update (ModifyIncremental + PdfDocument.SaveIncremental) - #386

Open
AlexanderV wants to merge 1 commit into
empira:masterfrom
AlexanderV:upstream/incremental-update
Open

Add append-only incremental update (ModifyIncremental + PdfDocument.SaveIncremental)#386
AlexanderV wants to merge 1 commit into
empira:masterfrom
AlexanderV:upstream/incremental-update

Conversation

@AlexanderV

Copy link
Copy Markdown

This PR adds an append-only incremental update: the bytes of the original file are written unchanged, followed by the new and the modified objects and a cross-reference section chained to the one of the original file by its /Prev entry.

Because no byte of the original file is touched, a digital signature the file already contains stays valid and further signatures can be added one by one — which is not possible with Save, since it rewrites the whole file.

This PR adds public API, so please tell me if you would rather discuss it in an issue first — I am happy to rename anything or to change the approach.

New API

  • PdfDocumentOpenMode.ModifyIncremental — like Modify, but the reader neither compacts nor renumbers the cross-reference table, so the object numbers of the file are preserved. Appended at the end of the enum to keep the values of the existing members stable.
  • PdfDocument.SaveIncremental / SaveIncrementalAsync (stream and path overloads, closeStream like Save).
  • PdfDocument.MarkAsModified(PdfObject) — see below.
  • DigitalSignatureHandler declares the objects it modifies, so signing needs no extra code.

Detecting modified objects

An incremental update writes only new and modified objects. New objects are detected automatically (the reader remembers the object identifiers of the file). PDFsharp cannot detect the modification of an object, so a modified object has to be declared with MarkAsModified. This is the one design decision I am least sure about — an alternative would be to write all objects, which is always correct but doubles the file size.

Rejected with a dedicated exception

  • encrypted documents — appending unencrypted objects would corrupt the file;
  • documents with a cross-reference stream — chaining a cross-reference table to a cross-reference stream is not valid PDF;
  • removing an object — that needs a free entry in the cross-reference section, which is not written.

Known limitation, documented in the XML comments: the bytes of the original file are kept in memory between opening and saving the document, because the stream they were read from is not necessarily open anymore when the document is saved.

Tests

IO/IncrementalUpdateTests.cs, 9 cases: the original bytes are preserved byte for byte, the cross-reference sections are chained, the object numbers survive, two signatures are layered one after the other, and each of the rejected cases. They use a dummy IDigitalSigner, so they need no certificate, no assets and no network. The test for the cross-reference stream builds such a file in the test itself.

Verified on top of current master: builds for net10.0 and netstandard2.0, full PdfSharp.Tests suite green (261 passed, 0 failed).

This PR is independent of my other PRs; each of them applies to master on its own.

New open mode ModifyIncremental and new function PdfDocument.SaveIncremental.

An incremental update writes the bytes of the original file unchanged,
followed by the new and the modified objects and a cross-reference section
chained to the cross-reference section of the original file by its /Prev
entry. Because no byte of the original file is touched, a digital signature
of the original file stays valid and further signatures can be added one by
one, which is not possible with Save.

Opening a document with Modify runs IrefTable.Compact() and Renumber(). Both
are fatal here, because the objects of the original file are written unchanged
and are referenced by their original object numbers. ModifyIncremental behaves
like Modify but skips both steps, keeps the bytes and the object identifiers of
the original file, and is treated as writable by CanModify and IsReadOnly.

PDFsharp cannot detect the modification of an object, therefore modified
objects must be declared with the new function PdfDocument.MarkAsModified.
DigitalSignatureHandler declares the objects it modifies.

Encrypted documents, documents with a cross-reference stream and the removal
of an object are rejected with a dedicated exception, because appending
unencrypted objects, chaining a cross-reference table to a cross-reference
stream respectively omitting the free entry of a removed object is not valid.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants