From 36412b6442ffa95c5b749d09208a39a04ba3ad49 Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Tue, 28 Jul 2026 08:11:42 +1000 Subject: [PATCH] Read a stream whose length is kept in an object stream 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 #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. --- .../IO/IndirectStreamLengthTests.cs | 151 ++++++++++++++++++ PdfSharpCore/Pdf.IO/Parser.cs | 33 +++- 2 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 PdfSharpCore.Test/IO/IndirectStreamLengthTests.cs diff --git a/PdfSharpCore.Test/IO/IndirectStreamLengthTests.cs b/PdfSharpCore.Test/IO/IndirectStreamLengthTests.cs new file mode 100644 index 00000000..1b9f5720 --- /dev/null +++ b/PdfSharpCore.Test/IO/IndirectStreamLengthTests.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.IO; +using FluentAssertions; +using PdfSharpCore.Pdf; +using PdfSharpCore.Pdf.Advanced; +using PdfSharpCore.Pdf.IO; +using PdfSharpCore.Pdf.IO.enums; +using Xunit; + +namespace PdfSharpCore.Test.IO +{ + /// + /// The /Length of a stream may be an indirect reference, and the object it points to is allowed + /// to live inside an object stream. Such an object has no position of its own in the file, so it + /// can only be read through the object stream that holds it. + /// See https://github.com/ststeiger/PdfSharpCore/issues/456. + /// + public class IndirectStreamLengthTests + { + private const string Content = "BT /F1 12 Tf 20 40 Td (Issue 456) Tj ET"; + + [Theory] + [InlineData(PdfDocumentOpenMode.Modify)] + [InlineData(PdfDocumentOpenMode.Import)] + [InlineData(PdfDocumentOpenMode.ReadOnly)] + public void ADocumentWhoseStreamLengthLivesInAnObjectStreamCanBeRead(PdfDocumentOpenMode openMode) + { + using (var input = new MemoryStream(BuildDocumentWithTheStreamLengthInAnObjectStream())) + { + var document = Pdf.IO.PdfReader.Open(input, openMode, PdfReadAccuracy.Strict); + + document.PageCount.Should().Be(1); + } + } + + [Fact] + public void TheStreamIsReadWithTheLengthTakenFromTheObjectStream() + { + using (var input = new MemoryStream(BuildDocumentWithTheStreamLengthInAnObjectStream())) + { + var document = Pdf.IO.PdfReader.Open(input, PdfDocumentOpenMode.Modify); + + var streams = new List(); + foreach (PdfContent content in document.Pages[0].Contents) + streams.Add(content.Stream.Value); + + streams.Should().ContainSingle() + .Which.Should().Equal(Latin1Bytes(Content)); + } + } + + [Fact] + public void ADocumentWhoseStreamLengthLivesInAnObjectStreamCanBeSavedAndReadBack() + { + using (var input = new MemoryStream(BuildDocumentWithTheStreamLengthInAnObjectStream())) + using (var output = new MemoryStream()) + { + var inputDocument = Pdf.IO.PdfReader.Open(input, PdfDocumentOpenMode.Import); + + var merged = new PdfDocument(); + foreach (PdfPage page in inputDocument.Pages) + merged.AddPage(page); + merged.Save(output, false); + + output.Position = 0; + Pdf.IO.PdfReader.Open(output, PdfDocumentOpenMode.Import).PageCount.Should().Be(1); + } + } + + /// + /// Builds a single page document that declares the length of its content stream as "6 0 R", + /// where object 6 is an integer stored inside object stream 7. The document is addressed by + /// a cross-reference stream, because object streams require one. + /// + private static byte[] BuildDocumentWithTheStreamLengthInAnObjectStream() + { + var pdf = new MemoryStream(); + var offsets = new Dictionary(); + + void Write(string text) + { + var bytes = Latin1Bytes(text); + pdf.Write(bytes, 0, bytes.Length); + } + + void WriteObject(int number, string body) + { + offsets[number] = pdf.Position; + Write(number + " 0 obj\n" + body + "\nendobj\n"); + } + + Write("%PDF-1.5\n"); + WriteObject(1, "<>"); + WriteObject(2, "<>"); + WriteObject(3, "<>>>/Contents 5 0 R>>"); + WriteObject(4, "<>"); + // The length of this stream is object 6, which is not in the file but in object stream 7. + WriteObject(5, "<>stream\n" + Content + "\nendstream"); + + // Object stream 7 holds a single object: the integer 6, the length of the stream above. + const string objectStreamHeader = "6 0 "; + var objectStream = objectStreamHeader + Content.Length; + WriteObject(7, "<>stream\n" + objectStream + "\nendstream"); + + // The cross-reference stream, uncompressed, with three fields of 1, 4 and 2 bytes. + var startOfCrossReferenceStream = pdf.Position; + offsets[8] = startOfCrossReferenceStream; + var xref = new MemoryStream(); + + void WriteEntry(int type, long field2, int field3) + { + xref.WriteByte((byte)type); + xref.WriteByte((byte)(field2 >> 24)); + xref.WriteByte((byte)(field2 >> 16)); + xref.WriteByte((byte)(field2 >> 8)); + xref.WriteByte((byte)field2); + xref.WriteByte((byte)(field3 >> 8)); + xref.WriteByte((byte)field3); + } + + WriteEntry(0, 0, 65535); + for (var number = 1; number <= 5; number++) + WriteEntry(1, offsets[number], 0); + WriteEntry(2, 7, 0); // Object 6 is the object at index 0 of object stream 7. + WriteEntry(1, offsets[7], 0); + WriteEntry(1, offsets[8], 0); + var xrefBytes = xref.ToArray(); + + Write("8 0 obj\n<>stream\n"); + pdf.Write(xrefBytes, 0, xrefBytes.Length); + Write("\nendstream\nendobj\n"); + Write("startxref\n" + startOfCrossReferenceStream + "\n%%EOF\n"); + + return pdf.ToArray(); + } + + /// + /// The document is plain ASCII, so a character is a byte. Written out rather than taken from + /// Encoding, which does not offer Latin-1 on every framework this builds for. + /// + private static byte[] Latin1Bytes(string text) + { + var bytes = new byte[text.Length]; + for (var i = 0; i < text.Length; i++) + bytes[i] = (byte)text[i]; + return bytes; + } + } +} diff --git a/PdfSharpCore/Pdf.IO/Parser.cs b/PdfSharpCore/Pdf.IO/Parser.cs index e39a162f..18eb150e 100644 --- a/PdfSharpCore/Pdf.IO/Parser.cs +++ b/PdfSharpCore/Pdf.IO/Parser.cs @@ -375,14 +375,39 @@ private int GetStreamLength(PdfDictionary dict) throw new InvalidOperationException("Cannot retrieve stream length."); } - var state = SaveState(); - object length = ReadObject(null, reference.ObjectID, false, false); - RestoreState(state); - var len = ((PdfIntegerObject)length).Value; + // The object holding the length may have been parsed already. That is always the case + // when it lives in an object stream: such an object has no position of its own in the + // file, so it cannot be read by moving the input stream to it. + var len = ResolveLengthObject(reference).Value; dict.Elements["/Length"] = new PdfInteger(len); return len; } + /// + /// Gets the integer object an indirect /Length entry points to. + /// + private PdfIntegerObject ResolveLengthObject(PdfReference reference) + { + if (reference.Value is PdfIntegerObject resolved) + return resolved; + + // The reference may be a temporary one created while the cross-reference table was + // under construction, so look the object up in the table as well. + var iref = _document != null ? _document._irefTable[reference.ObjectID] : null; + if (iref != null && iref.Value is PdfIntegerObject known) + return known; + + var state = SaveState(); + try + { + return (PdfIntegerObject)ReadObject(null, reference.ObjectID, false, false); + } + finally + { + RestoreState(state); + } + } + public PdfArray ReadArray(PdfArray array, bool includeReferences) { Debug.Assert(Symbol == Symbol.BeginArray);