Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/core/xref.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ import {
InvalidPDFException,
warn,
} from "../shared/util.js";
import { CIRCULAR_REF, Cmd, Dict, isCmd, Ref, RefSet } from "./primitives.js";
import {
CIRCULAR_REF,
Cmd,
Dict,
isCmd,
isDict,
Ref,
RefSet,
} from "./primitives.js";
import { Lexer, Parser } from "./parser.js";
import {
MissingDataException,
Expand Down Expand Up @@ -701,8 +709,10 @@ class XRef {
}

// When no trailer dictionary candidate exists, try picking the first
// dictionary that contains a /Root entry (fixes issue18986.pdf).
// dictionary that contains a /Root entry (fixes issue18986.pdf), or else
// the first /Catalog dictionary (e.g. for files truncated before the end).
if (!trailerDicts.length) {
let catalogRef = null;
// In case, this.#entries is a sparse array we don't want to
// iterate over empty entries so we use the `in` operator instead of
// using for..of on entries() or a for with the array length.
Expand All @@ -716,7 +726,10 @@ class XRef {

try {
obj = this.fetch(ref);
} catch {
} catch (ex) {
if (ex instanceof MissingDataException) {
throw ex;
}
continue;
}
if (obj instanceof BaseStream) {
Expand All @@ -725,6 +738,14 @@ class XRef {
if (obj instanceof Dict && obj.has("Root")) {
return obj;
}
if (!catalogRef && isDict(obj, "Catalog")) {
catalogRef = ref;
}
}
if (catalogRef) {
const dict = new Dict(this);
dict.set("Root", catalogRef);
return dict;
}
}

Expand Down
83 changes: 83 additions & 0 deletions test/unit/api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,89 @@ describe("api", function () {
await loadingTask.destroy();
});

it("creates pdf doc from PDF file truncated before its trailer", async function () {
const compressedPixels =
"789c014000bfff00254a6f94b9de03284d7297bce1062b50759abfe4092e53789dc2e70c31567ba0c5ea0f34597ea3c8ed12375c81a6cbf0153a5f84a9cef3183d6287acd1f61ba08d1e61";
const streamObject = (num, content) =>
`${num} 0 obj\n<< /Length ${content.length} >>\nstream\n` +
`${content}\nendstream\nendobj\n`;
const imageObject = (num, data, complete) =>
`${num} 0 obj\n<< /Type /XObject /Subtype /Image /Width 4 ` +
`/Height 4 /ColorSpace /DeviceCMYK /BitsPerComponent 8 ` +
`/Filter [/ASCIIHexDecode /FlateDecode] ` +
`/Length ${compressedPixels.length + 1} >>\nstream\n` +
`${data}${complete ? ">\nendstream\nendobj\n" : ""}`;
const pageContents = [
"q 72 0 0 72 0 0 cm /Im0 Do /Im1 Do Q\n" +
"BT /F1 12 Tf 72 720 Td (Synthetic map page one) Tj ET",
"q 72 0 0 72 0 0 cm /Im0 Do /Im1 Do Q\n" +
"BT /F1 12 Tf 72 720 Td (Synthetic map page two) Tj ET",
"q 72 0 0 72 0 0 cm /Im0 Do /Im1 Do Q\n" +
"BT /F1 12 Tf 72 720 Td (Synthetic map page three) Tj ET",
"q 72 0 0 72 0 0 cm /Im0 Do /Im1 Do Q\n" +
"BT /F1 12 Tf 72 720 Td (Synthetic map page four) Tj ET",
];
const pageObjects = [
[3, 4, 5],
[6, 15, 16],
[8, 25, 26],
[10, 32, 33],
];
let pdf =
"%PDF-1.3\n" +
streamObject(4, pageContents[0]) +
"5 0 obj\n<< /ProcSet [/PDF /Text /ImageC] " +
"/XObject << /Im0 7 0 R /Im1 17 0 R >> " +
"/Font << /F1 11 0 R >> /ExtGState << /GS1 9 0 R >> >>\n" +
"endobj\n" +
"9 0 obj\n<< /Type /ExtGState /ca 1 /CA 1 >>\nendobj\n" +
"11 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\n" +
"endobj\n" +
streamObject(15, pageContents[1]) +
"16 0 obj\n<< /ProcSet [/PDF /Text /ImageC] " +
"/XObject << /Im0 7 0 R /Im1 17 0 R >> " +
"/Font << /F1 11 0 R >> /ExtGState << /GS1 9 0 R >> >>\n" +
"endobj\n" +
streamObject(25, pageContents[2]) +
"26 0 obj\n<< /ProcSet [/PDF /Text /ImageC] " +
"/XObject << /Im0 7 0 R /Im1 17 0 R >> " +
"/Font << /F1 11 0 R >> /ExtGState << /GS1 9 0 R >> >>\n" +
"endobj\n" +
streamObject(32, pageContents[3]) +
"33 0 obj\n<< /ProcSet [/PDF /Text /ImageC] " +
"/XObject << /Im0 7 0 R /Im1 17 0 R >> " +
"/Font << /F1 11 0 R >> /ExtGState << /GS1 9 0 R >> >>\n" +
"endobj\n" +
imageObject(7, compressedPixels, /* complete = */ true) +
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" +
"2 0 obj\n<< /Type /Pages /Kids [3 0 R 6 0 R 8 0 R 10 0 R] " +
"/Count 4 >>\nendobj\n";
for (const [pageObj, contentObj, resourcesObj] of pageObjects) {
pdf +=
`${pageObj} 0 obj\n<< /Type /Page /Parent 2 0 R ` +
`/MediaBox [0 0 612 792] /Resources ${resourcesObj} 0 R ` +
`/Contents ${contentObj} 0 R >>\nendobj\n`;
}
// Keep the document tree intact, then truncate the final image stream.
pdf += imageObject(
17,
compressedPixels.slice(0, compressedPixels.length >> 1),
/* complete = */ false
);

const loadingTask = getDocument({
data: stringToBytes(pdf),
});
const pdfDocument = await loadingTask.promise;
expect(pdfDocument.numPages).toEqual(4);

const page = await pdfDocument.getPage(1);
const textContent = await page.getTextContent();
expect(mergeText(textContent.items)).toContain("Synthetic map page one");

await loadingTask.destroy();
});

it("creates pdf doc from non-existent URL", async function () {
const loadingTask = getDocument(
buildGetDocumentParams("non-existent.pdf")
Expand Down
Loading