Skip to content

Read PDF Bookmarks

PDF bookmarks are outline dictionaries rooted at the catalog's Outlines entry. Traverse First for children and Next for siblings. This example returns bookmark titles and one-based page numbers for direct array destinations.

import { createMuhammaraWasm } from "@muhammara/wasm";

function entry(reader, dictionary, key) {
  return dictionary.exists(key)
    ? reader.queryDictionaryObject(dictionary, key)
    : undefined;
}

function destinationPage(reader, item, pageNumbers) {
  var destination = entry(reader, item, "Dest");
  var array = destination && destination.toPDFArray();
  var page = array && array.queryObject(0).toPDFIndirectObjectReference();

  return page ? pageNumbers[page.getObjectID()] || null : null;
}

function readItems(reader, item, pageNumbers) {
  var items = [];

  while (item) {
    var title = entry(reader, item, "Title");
    var first = entry(reader, item, "First");

    items.push({
      title: title ? title.toText() : "",
      page: destinationPage(reader, item, pageNumbers),
      children: first
        ? readItems(reader, first.toPDFDictionary(), pageNumbers)
        : [],
    });

    var next = entry(reader, item, "Next");
    item = next ? next.toPDFDictionary() : undefined;
  }

  return items;
}

function readBookmarks(muhammara, bytes) {
  var reader = muhammara.createReader(bytes);

  try {
    var pageNumbers = {};
    for (var index = 0; index < reader.getPagesCount(); ++index) {
      pageNumbers[reader.getPageObjectID(index)] = index + 1;
    }

    var catalog = entry(reader, reader.getTrailer(), "Root").toPDFDictionary();
    var outlines = entry(reader, catalog, "Outlines");
    var first = outlines && entry(reader, outlines.toPDFDictionary(), "First");

    return first ? readItems(reader, first.toPDFDictionary(), pageNumbers) : [];
  } finally {
    reader.end();
  }
}

var muhammara = await createMuhammaraWasm();
var bytes = new Uint8Array(await pdfFile.arrayBuffer());
console.log(JSON.stringify(readBookmarks(muhammara, bytes), null, 2));

The direct destination array begins with an indirect reference to a page; the example maps that object ID to a one-based page number. A bookmark can instead use a named destination or an A action such as GoToR, URI, or Launch. Those cases intentionally return null here and need application-specific handling. A PDF without an Outlines entry has no bookmarks and returns [].

The browser example's Inspect PDF tab lists bookmarks in its report. It reads at most 50 bookmarks, eight levels deep, so a cyclic outline in an untrusted upload cannot loop forever.

This uses the low-level reader rather than Recipe. See Inspect PDF Objects for dictionary and indirect-object traversal.