Skip to content

Read a stream whose length is kept in an object stream (#456) - #493

Open
jafin wants to merge 1 commit into
ststeiger:masterfrom
jafin:upstream-456-stream-length-in-object-stream
Open

Read a stream whose length is kept in an object stream (#456)#493
jafin wants to merge 1 commit into
ststeiger:masterfrom
jafin:upstream-456-stream-length-in-object-stream

Conversation

@jafin

@jafin jafin commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #456, reproduced with both documents attached to that issue.

What happens

Error: Object with ID 6 0 resolved with negative position

The /Length of a stream may be an indirect reference, and the object it points to is allowed to live inside an object stream. Parser.GetStreamLength resolved that reference by seeking to the object's offset in the file:

object length = ReadObject(null, reference.ObjectID, false, false);   // -> MoveToObject -> throw

A compressed object has no offset of its own. PdfObjectStream.ReadReferences marks it with -1 ("HACK: -1 indicates compressed object"), and MoveToObject answers a position of -1 by 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 content
  • object 6 is the integer 1152, stored in object stream 14 as a type-2 cross-reference entry

This also explains the two workarounds found in the thread. qpdf --object-streams=disable moves object 6 into the file body, where it has a position. PdfReadAccuracy.Moderate only swallows the exception in PdfReader'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.Open 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. GetStreamLength now 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, Import and ReadOnly under Strict accuracy. Beyond not throwing:

  • every stream's declared /Length matches the bytes actually read, across both documents
  • content streams decompress to valid operators (q 0.1 0 0 0.1 0 0 cm … BT /R7 60 Tf … TJ ET)
  • both documents save and re-open

PdfSharpCore.Test/IO/IndirectStreamLengthTests.cs builds 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 identical Object 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 XTextFormatterTest rendering 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.cs is 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 only GetStreamLength and the helper below it.

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.
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.

Error: Object with ID 6 0 resolved with negative position

1 participant