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.

Editing content in Word is not difficult, but realigning the table of contents, page numbers, headers and footers afterwards is often more troublesome. As soon as a document goes through a few rounds of additions and deletions, chapter order adjustments, or migration from another template, its original table of contents entries, page numbers, and the chapter name in the header easily fall out of sync with the body text — clicking a TOC entry jumps to the wrong page, page numbers fail to continue from a certain section onward, and the header still carries the chapter title from the previous version. Checking item by item by hand is time-consuming and prone to omissions, and the longer the document, the harder it is to guarantee consistency.

Comparison with Traditional SDK API Processing

Traditional Spire.Office for .NET API Spire.Agent.Office
Driving approach Write code to handle each item: iterate paragraphs to determine levels → update the TOC field → reorder page numbers → modify headers and footers; every step requires code control Describe in natural language which structures to reconstruct, and AI completes it automatically
Code volume The TOC field, sectioned page numbers, header fields and so on each require a separate set of processing logic Only configuration code + 1 natural language instruction
Heading level recognition Relies on rigid judgment by style name or outline level, which is easily misjudged when styles are not standardized AI determines heading levels by combining semantics and styles
Section and field handling Section breaks, page number start values, and fields such as PAGE/STYLEREF must each be set manually Automatically identifies sections and field reference relationships and updates them as a group
Maintainability After the document template or structure changes, the code must be modified and a new version released The reconstruction scope and rules can be adjusted at any time in natural language

This article explains how to use the Word AI capability of Spire.Agent.Office to complete document structure reconstruction, covering two typical categories of problems, from the table of contents to page numbers, headers and footers: first let AI scan the heading levels and section information and regenerate the table of contents according to the actual headings in the body, then refresh page numbers and update the dynamic fields in headers and footers, keeping the table of contents, page numbers, headers and footers consistent with the body text.

For product installation and SpireToken configuration, refer to Integrating Spire.Agent.Office in a .NET Project. The examples below assume Spire.Agent.Office is installed and SpireToken is configured.


Table of Contents Structure Reconstruction

A table of contents that no longer matches is usually because the TOC was not updated after the body text was modified, or because the original TOC was static text typed by hand. The core idea of structure reconstruction is: load the existing document, let AI scan the headings at each level in the body, determine the hierarchical relationships and check the section positions, and regenerate a table of contents field with page numbers according to the actual headings in the body, making the TOC entries and levels correspond one-to-one with the body text, while only adjusting heading styles and not touching the body content.

using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;

// The document whose structure is to be reconstructed
string inputPath = "E:\\Input\\XX_Project_Implementation_Plan.docx";
// Save path
string savePath = "E:\\Output\\XX_Project_Implementation_Plan-Reconstructed.docx";
// SpireToken Key
string key = "**********************";
// Natural language instruction
string instruction =
    "Please reconstruct the table of contents structure of the current document: " +
    "1. Scan the headings in the body, identify the hierarchical relationships of the headings at each level, and unify the heading styles (use Heading 1 for level-1 headings, Heading 2 for level-2 headings, and so on); " +
    "2. Check the positions of the section breaks to ensure the chapter divisions are consistent with the heading levels; " +
    "3. Delete the original table of contents and regenerate a table of contents field before the body, containing headings at each level with their corresponding page numbers, fully consistent with the actual headings and levels in the body; " +
    "4. Only adjust the heading styles and the table of contents, and keep the body content unchanged. " +
    "Finally save and output in DOCX format";

// Call the Word document processing function
AIResult result = ExecuteDemoWord(instruction, inputPath, savePath, key, null);

// Execute Word document AI processing
static AIResult ExecuteDemoWord(string instruction, string inputPath, string savePath, string key, string[] attachmentPaths)
{
    // Create an AIOptions configuration object
    AIOptions options = new AIOptions();
    // Set the SpireToken Key
    options.SpireToken = key;

    // Use the Document object to process the Word document
    using (Document doc = new Document())
    {
        // Load the document whose structure is to be reconstructed
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            doc.LoadFromFile(inputPath);
        }
        // Create the AI document processor
        AIDocumentProcessor processor = doc.AI(options);

        // Execute the AI instruction
        return processor.ExecuteInstruction(doc, instruction, savePath, attachmentPaths);
    }
}

Reconstructed table of contents Reconstructed table of contents

After reconstruction, the TOC entries and levels correspond one-to-one with the body headings, and clicking an entry jumps to the correct position, with no more entries pointing to old chapters or missing. For documents with major structural changes, regenerating the TOC directly is more convenient than manually adding and deleting TOC entries, and far less likely to miss a change.


Page Number, Header and Footer Refresh

After the TOC is reconstructed, the page numbers, headers and footers also need to be realigned. Page number misalignment usually comes from the section settings, and the chapter name or total page count in the header shows the old value of the field. The core idea of this step is: let AI refresh the page numbers of the whole document and set the start value and continuation method according to sections, and at the same time update the dynamic fields in the headers and footers (such as chapter name, total page count, and date) so that the values of these fields match the current content of the body.

using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;

// The document whose page numbers are to be refreshed (can follow the reconstructed document from the previous section)
string inputPath = "E:\\Input\\XX_Project_Implementation_Plan-Reconstructed.docx";
// Save path
string savePath = "E:\\Output\\XX_Project_Implementation_Plan-Final.docx";
// SpireToken Key
string key = "**********************";
// Natural language instruction
string instruction =
    "Please refresh the page numbers of the current document and update the dynamic fields in the headers and footers: " +
    "1. Recalculate and refresh the page numbers of the whole document, with the body page numbers numbered consecutively starting from page 1; " +
    "2. Set page numbers by section, do not number the cover page and the table of contents, and start the body on a separate page with restarted numbering; " +
    "3. Update the dynamic fields such as the chapter name in the header (taken from the heading at the corresponding level) and the total page count, so that they match the current content of the body; " +
    "4. Unify the footer page number format as \"Page X of Y\". " +
    "Only update the above structural information, do not modify the body content, and finally save and output in DOCX format";

// Call the Word document processing function
AIResult result = ExecuteDemoWord(instruction, inputPath, savePath, key, null);

// Execute Word document AI processing
static AIResult ExecuteDemoWord(string instruction, string inputPath, string savePath, string key, string[] attachmentPaths)
{
    // Create an AIOptions configuration object
    AIOptions options = new AIOptions();
    // Set the SpireToken Key
    options.SpireToken = key;

    // Use the Document object to process the Word document
    using (Document doc = new Document())
    {
        // Load the document whose page numbers are to be refreshed
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            doc.LoadFromFile(inputPath);
        }
        // Create the AI document processor
        AIDocumentProcessor processor = doc.AI(options);

        // Execute the AI instruction
        return processor.ExecuteInstruction(doc, instruction, savePath, attachmentPaths);
    }
}

Document after refreshing the page numbers, headers and footers Refreshed page numbers, headers and footers

After refreshing, the body page numbers are consecutive, the section start values are correct, and the chapter name and total page count in the header stay consistent with the body. For documents processed in batches, the same set of instructions can be used to unify the page number rules and the header and footer formats, saving the time of opening each document and checking each item.


FAQ

The reconstructed table of contents still shows old entries or gains extra items

Reason: The original document's table of contents is static text rather than a TOC field, or the heading styles are not unified, causing deviations in the recognized levels.

Solution: In the instruction, explicitly require "delete the original table of contents and regenerate a table of contents field according to the body headings", and state the basis for recognizing headings (by style name or outline level) to reduce misjudgment.

Page numbers do not match starting from a certain section or repeat

Reason: The page number start value and continuation method of the section breaks do not meet the requirements, and it is easy to miss a section when setting them manually.

Solution: Write out the page number requirements of each section one by one in the instruction, such as "do not number the cover page and the table of contents, start the body from page 1, and number each section consecutively", and let AI set them uniformly by section.

The chapter name or total page count in the header does not change

Reason: The chapter name and total page count are mostly dynamic fields such as STYLEREF and NUMPAGES, and still show cached values when not refreshed.

Solution: Require "update the values of all dynamic fields in the headers and footers", and explain the source of the fields, such as the chapter name taken from the heading at the corresponding level and the total page count taken from the whole document.

The body formatting is changed along with the structure reconstruction

Reason: The operation scope was not limited, and AI adjusted the fonts and paragraph formats of the body while unifying the heading styles.

Solution: In the instruction, clearly state "only adjust structural information such as heading styles, the table of contents, page numbers, headers and footers, and keep the body fonts and paragraph formats unchanged".


Get the SpireToken Key

Configure it in your code:

AIOptions options = new AIOptions();
options.SpireToken = key;

When you need to compare several metrics across multiple dimensions at the same time, a radar chart is a very intuitive way to present them — each dimension is placed on an axis radiating out from the center, and the values on those axes are joined into a polygon, so the shape immediately shows where the strengths and weaknesses are. Spire.XLS for JavaScript provides a complete charting API that supports creating radar charts directly in the browser via WebAssembly, without requiring a backend service.

This article covers two core features:

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


Create a Radar Chart

You can add a radar chart to a worksheet with the sheet.Charts.Add() method. The steps are as follows:

  1. Load the Excel file that contains the data and get the first worksheet.
  2. Add a radar chart with Charts.Add({ chartType: ExcelChartType.Radar }).
  3. Set the DataRange property to specify the chart data range: the first row holds the series names, the first column holds the category names for each axis, and the remaining cells hold the values.
  4. Set SeriesDataFromRange = false so that the data is not taken from a row/column layout.
  5. Set the chart title, position, and legend position.
  6. Save the workbook with the Workbook.SaveToFile() method.

Below is a complete code example that shows how to create a radar chart in React:

function App() {
  const createRadarChart = async () => {
    // Get the Spire.XLS WASM module
    const xlsModule = window.wasmModule?.spirexls;

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

    // Load the Excel file into VFS
    const inputFileName = 'RadarChartData.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

    // Load the workbook
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile(inputFileName);

    // Get the first worksheet
    let sheet = workbook.Worksheets.get(0);

    // Add a radar chart
    let chart = sheet.Charts.Add({ chartType: xlsModule.ExcelChartType.Radar });

    // Set the chart data range
    chart.DataRange = sheet.Range.get("A1:C5");
    chart.SeriesDataFromRange = false;

    // Set the chart position
    chart.LeftColumn = 7;
    chart.TopRow = 6;
    chart.RightColumn = 16;
    chart.BottomRow = 29;

    // Set the chart title
    chart.ChartTitle = "Product Sales by Region";
    chart.ChartTitleArea.IsBold = true;
    chart.ChartTitleArea.Size = 12;

    // Set the legend position
    chart.Legend.Position = xlsModule.LegendPositionType.Corner;

    // Save the workbook
    const outputFileName = 'CreateRadarChart.xlsx';
    workbook.SaveToFile(outputFileName);

    // Release resources
    workbook.Dispose();

    // Read the converted file from VFS and trigger download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
    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>Create a Radar Chart</h1>
      <button onClick={createRadarChart}>Start</button>
    </div>
  );
}

export default App;

After running, the effect of creating a radar chart:

Create Radar Chart


Style the Radar Chart

After creating the radar chart, you can further improve its appearance by setting the chart area, plot area, and series line colors. The steps are as follows:

  1. Get the radar chart object that has been created.
  2. Set the ChartArea.Fill.ForeColor property to set the chart background color.
  3. Set the PlotArea.Fill.ForeColor property to set the plot area background color.
  4. Set the Series[i].Format.LineProperties.Color property to specify the line color of each series; Series.get(0) and Series.get(1) correspond to the two series in the data range.
  5. Save the workbook.

Below is a complete code example that shows how to style the radar chart:

function App() {
  const styleRadarChart = async () => {
    // Get the Spire.XLS WASM module
    const xlsModule = window.wasmModule?.spirexls;

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

    // Load the Excel file into VFS
    const inputFileName = 'RadarChartData.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

    // Load the workbook
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile(inputFileName);

    // Get the first worksheet
    let sheet = workbook.Worksheets.get(0);

    // Add a radar chart
    let chart = sheet.Charts.Add({ chartType: xlsModule.ExcelChartType.Radar });

    // Set the chart data range
    chart.DataRange = sheet.Range.get("A1:C5");
    chart.SeriesDataFromRange = false;

    // Set the chart position
    chart.LeftColumn = 7;
    chart.TopRow = 6;
    chart.RightColumn = 16;
    chart.BottomRow = 29;

    // Set the chart title
    chart.ChartTitle = "Product Sales by Region";
    chart.ChartTitleArea.IsBold = true;
    chart.ChartTitleArea.Size = 12;

    // Style the radar chart
    // Set the chart area background color
    chart.ChartArea.Fill.ForeColor = xlsModule.Color.get_LightCyan();
    // Set the plot area background color
    chart.PlotArea.Fill.ForeColor = xlsModule.Color.get_LightYellow();
    // Set the color of the first series line
    chart.Series.get(0).Format.LineProperties.Color = xlsModule.Color.get_Orange();
    // Set the color of the second series line
    chart.Series.get(1).Format.LineProperties.Color = xlsModule.Color.get_CornflowerBlue();

    // Save the workbook
    const outputFileName = 'StyledRadarChart.xlsx';
    workbook.SaveToFile(outputFileName);

    // Release resources
    workbook.Dispose();

    // Read the converted file from VFS and trigger download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
    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>Style the Radar Chart</h1>
      <button onClick={styleRadarChart}>Start</button>
    </div>
  );
}

export default App;

After running, the effect of styling the radar chart:

Style Radar Chart


Frequently Asked Questions

The radar chart does not change after setting a series color

Reason: A radar chart draws each series as a line, so setting a fill color with Series.get(i).Format.Fill has no effect.

Solution: Set the line color through Series.get(i).Format.LineProperties.Color, for example:

chart.Series.get(0).Format.LineProperties.Color = xlsModule.Color.get_Orange();

How do I adjust the legend position of a radar chart?

Reason: The legend is docked to the right of the chart by default, where it competes with the plot area for width — especially noticeable on a radar chart, which already takes up a lot of horizontal space.

Solution: Set the legend position with the chart.Legend.Position property. The available values are LegendPositionType.Bottom, Corner, Top, Right, Left, and NotDocked. For example, to move the legend to the top-right corner:

chart.Legend.Position = xlsModule.LegendPositionType.Corner;

Get a Free License

Spire.XLS for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.

page 25