Extracting PDF Bookmarks in React with JavaScript

A bookmark in a PDF records the document's outline structure, and a bookmark can hold child bookmarks of its own, nesting level by level into a tree. Reading that information has plenty of practical uses — exporting it as a table of contents, generating site navigation from bookmark titles, or locating a specific page for further processing. All of it calls for a program that can walk the whole bookmark tree and pull out the content of each node.

Spire.PDF for JavaScript processes PDF documents directly in the browser based on WebAssembly, managing input and output files through a virtual file system (VFS), with no backend service required. The core entry point for extracting bookmarks is the PdfDocument.Bookmarks property, which returns a PdfBookmarkCollection; each PdfBookmark object in it exposes its own child bookmark collection, so together they form the complete bookmark tree. Every bookmark node provides properties such as Title and DisplayStyle for reading its appearance, and Destination.Page combined with PdfPageCollection.IndexOf gives you the page number the bookmark points to.

This article covers two core features:

For installation and project configuration, refer to Integrating Spire.PDF for JavaScript in a React Project. The following examples assume Spire.PDF is installed and the WebAssembly module has been initialized.


Extracting All PDF Bookmarks

PdfDocument.Bookmarks returns only the top-level bookmark collection, while bookmarks themselves can contain child bookmarks. To read out every bookmark in the document you need a recursive function that walks the PdfBookmarkCollection level by level, reads the Title (bookmark title) and DisplayStyle (text style) of each node, and records them with indentation by level, producing a complete outline list in the end.

function App() {
  const extractAllBookmarks = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check whether the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF file to be processed into the VFS
    const inputFileName = 'Sample.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // Create a PdfDocument object and load the PDF document
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // String that holds the extracted result
    let content = 'All bookmarks in the PDF document:\r\n';

    // Recursively traverse the bookmark collection, recording each title and text style with indentation by level
    const collectBookmarks = (bookmarks, indent) => {
      for (let i = 0; i < bookmarks.Count; i++) {
        let bookmark = bookmarks.get_Item(i);

        // Record the title and text style of the current bookmark
        content += indent + bookmark.Title + ' (' + bookmark.DisplayStyle.toString() + ')\r\n';

        // If there are child bookmarks, process them recursively with more indentation
        if (bookmark.Count > 0) {
          collectBookmarks(bookmark, indent + '    ');
        }
      }
    };

    // Start extracting from the top-level bookmarks
    collectBookmarks(doc.Bookmarks, '');

    // Write the extracted result to a file and trigger the download
    const outputFileName = 'AllBookmarks.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, content);
    doc.Close();

    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Extract All PDF Bookmarks</h1>
      <button onClick={extractAllBookmarks}>
        Start Extracting
      </button>
    </div>
  );
}

export default App;

The list of all bookmark titles and text styles extracted recursively

The list of all bookmark titles and text styles extracted recursively


Getting the Page Number of a Bookmark

Besides its title, a bookmark also carries a jump destination. Through PdfBookmark.Destination.Page you can obtain the PdfPage object the bookmark points to, and then use PdfPageCollection.IndexOf to get its index within the document. Because the index starts from 0, adding 1 gives the page number shown in a reader. This is commonly used to export a bookmark list as a "title — page number" table of contents.

function App() {
  const getBookmarkPageNumber = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check whether the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF file to be processed into the VFS
    const inputFileName = 'Sample.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // Create a PdfDocument object and load the PDF document
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // Read the page number each top-level bookmark points to, one by one
    let content = 'Bookmarks and their page numbers:\r\n';
    for (let i = 0; i < doc.Bookmarks.Count; i++) {
      let bookmark = doc.Bookmarks.get_Item(i);

      // Destination.Page gives the page the bookmark points to; IndexOf returns its 0-based index
      let pageNumber = doc.Pages.IndexOf(bookmark.Destination.Page) + 1;

      content += bookmark.Title + ' — Page ' + pageNumber + '\r\n';
    }

    // Write the extracted result to a file and trigger the download
    const outputFileName = 'BookmarkPageNumber.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, content);
    doc.Close();

    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Get the Page Number of a Bookmark</h1>
      <button onClick={getBookmarkPageNumber}>
        Start Extracting
      </button>
    </div>
  );
}

export default App;

The title of each bookmark and the page number it points to

The title of each bookmark and the page number it points to


FAQ

Why is the number of extracted bookmarks smaller than the outline shown in a reader

Reason: doc.Bookmarks returns only the top-level bookmark collection, and its Count counts only the nodes at that level. Child bookmarks nested under a chapter are reached through the node's own collection and are otherwise not counted.

Solution: Traverse the whole bookmark tree recursively and add up the nodes at every level:

function countBookmarks(bookmarks) {
  let total = 0;
  for (let i = 0; i < bookmarks.Count; i++) {
    total += 1;
    // Recursively add the child bookmarks
    total += countBookmarks(bookmarks.get_Item(i));
  }
  return total;
}

const total = countBookmarks(doc.Bookmarks);

Why is the extracted DisplayStyle always Regular

Reason: PdfBookmark.DisplayStyle returns the text style a bookmark is displayed with in the outline panel. Only when the bookmark itself is explicitly set to a style such as Bold or Italic will the value read back differ from the default Regular. It reflects the bookmark's appearance setting, not the font used by the bookmark title in the page content.

Solution: Record the enum value as it is; if you only need to tell whether it is bold or italic, compare it against the PdfTextStyle values one by one:

let style = 'Regular';
if (bookmark.DisplayStyle === pdfModule.PdfTextStyle.Bold) {
  style = 'Bold';
} else if (bookmark.DisplayStyle === pdfModule.PdfTextStyle.Italic) {
  style = 'Italic';
}

Why does the page number from Destination.Page differ from the one shown in a reader

Reason: PdfPageCollection.IndexOf returns the page's index within the collection, counting from 0, while a reader shows page numbers from 1, so using the index directly is off by one.

Solution: Add 1 to the index to match what the reader shows:

// The index is 0-based, so add 1 to get the page number shown in a reader
let pageNumber = doc.Pages.IndexOf(bookmark.Destination.Page) + 1;

In addition, if a bookmark points to a page that no longer exists (for example, the target page was deleted), Destination may be empty, so check for null before reading it:

if (bookmark.Destination && bookmark.Destination.Page) {
  let pageNumber = doc.Pages.IndexOf(bookmark.Destination.Page) + 1;
}

Get a Free License

If you wish to remove the evaluation message from the result document or remove feature limitations, please contact sales to obtain a temporary license valid for 30 days.