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.

var muhammara = require("@muhammara/native");

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(inputPath) {
  var reader = muhammara.createReader(inputPath);

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

console.log(JSON.stringify(readBookmarks("input.pdf"), 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 [].

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