Read a stream whose length is kept in an object stream (#456) - #493
Open
jafin wants to merge 1 commit into
Open
Conversation
The /Length of a stream may be an indirect reference, and the object it points to is allowed to live inside an object stream. GetStreamLength resolved that reference by seeking to the object's offset in the file, but a compressed object has no offset of its own: PdfObjectStream marks it with -1, and MoveToObject answers a position of -1 by throwing PositionNotFoundException. The exception names the length object rather than the stream that asked for it, which is why the reports of this speak of a low object number resolved with a negative position. The value was there to be had. PdfReader reads every compressed object before it reads the rest of the indirect objects, so by the time a content stream is parsed the object holding its length has been read and is sitting in the cross-reference table. Take it from there, and read from the file only for an object that has a position to read from. Reported in issue ststeiger#456, with two documents that both fail this way. In one of them the page content is 5 0 obj, its length is 6 0 R, and object 6 is the integer 1152 stored in object stream 14. Both open now, and every stream in them reports a length that matches the bytes read.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #456, reproduced with both documents attached to that issue.
What happens
The
/Lengthof a stream may be an indirect reference, and the object it points to is allowed to live inside an object stream.Parser.GetStreamLengthresolved that reference by seeking to the object's offset in the file:A compressed object has no offset of its own.
PdfObjectStream.ReadReferencesmarks it with-1("HACK: -1 indicates compressed object"), andMoveToObjectanswers a position of-1by throwing. The exception names the length object rather than the stream that asked for it, which is why every report of this speaks of a low object number.Both attachments in the issue are laid out this way. In
100-Fast-Wholesale-Ltd-1.pdf:5 0 obj <</Length 6 0 R /Filter /FlateDecode>> stream …— the page content6is the integer1152, stored in object stream14as a type-2 cross-reference entryThis also explains the two workarounds found in the thread.
qpdf --object-streams=disablemoves object 6 into the file body, where it has a position.PdfReadAccuracy.Moderateonly swallows the exception inPdfReader's catch block, which is why it stops the throw without producing any content.The fix
The value was already there to be had:
PdfReader.Openreads every compressed object before it reads the rest of the indirect objects, so by the time a content stream is parsed the object holding its length has been read and is sitting in the cross-reference table.GetStreamLengthnow takes it from there, and reads from the file only for an object that has a position to read from. Behaviour is otherwise unchanged, including the existing exceptions for a length that cannot be resolved at all.Verification
Both documents from the issue now open in
Modify,ImportandReadOnlyunderStrictaccuracy. Beyond not throwing:/Lengthmatches the bytes actually read, across both documentsq 0.1 0 0 0.1 0 0 cm … BT /R7 60 Tf … TJ ET)PdfSharpCore.Test/IO/IndirectStreamLengthTests.csbuilds a minimal document with this structure in code, so no third party's file is committed as a binary asset. All five tests fail without the change with the identicalObject with ID 6 0 resolved with negative position, and pass with it.Test run on net8.0: 35 passed, 5 failed, 1 skipped. The five failures are the
XTextFormatterTestrendering comparisons, which fail identically on an unmodified master here because they shell out to Ghostscript (gswin64c.exe, exit 127) and it is not installed on this machine. Baseline on master is 26 passed with the same 5 failures, so this adds 5 passing tests and changes nothing else.Note for reviewers
PdfSharpCore/Pdf.IO/Parser.csis stored in CP1252, not UTF-8 — a few«»bytes sit in comments that predate this branch. An editor that writes the file back as UTF-8 replaces them with U+FFFD and adds unrelated noise to the diff. The change here was applied byte-safely; the diff touches onlyGetStreamLengthand the helper below it.