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);