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