Styles are the way Word reuses formatting: give a set of formatting — the font, the size, the colour, the paragraph spacing — a name and save it, and from then on every paragraph that carries that style name picks the whole set up automatically. Change the style definition and every paragraph that references it updates along with it, which is exactly what setting the formatting on a paragraph directly cannot do. Spire.Doc for JavaScript performs all of this in the browser via WebAssembly, using a virtual file system (VFS) to manage input and output files — no backend server required.

Structurally a paragraph style holds two parts at once: character formatting and paragraph formatting. Once a built-in style has been taken, ParagraphStyle.CharacterFormat changes the text-level properties and ParagraphStyle.ParagraphFormat changes the paragraph-level ones. The library maps this model onto the Document.Styles collection in full.

This article covers two core features:

For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.


Modify the Built-in Styles

Word ships with a batch of built-in styles — Title, Normal, Heading 1 to Heading 9 and so on. document.AddStyle({ builtinStyle }) takes the named built-in style (creating it if it does not exist yet) and returns its object, and once you have it, it can be rewritten.

Note that the return type of AddStyle is the generic Style, so before changing the paragraph format you have to confirm that the object really is a paragraph style — instanceof wasmModule.ParagraphStyle is the check. Normal is the base style of the body text, and changing it also affects every style that inherits from it, so it is usually used only to unify the body font and the font size.

function App() {
  const Styles = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    await window.spire.FetchFileToVFS("ARIALUNI.TTF", "/Library/Fonts/", `${process.env.PUBLIC_URL}static/font/`);

    // Create the document and the section
    let doc = new docModule.Document();
    let sec = doc.AddSection();

    // Take the built-in Title style and rewrite it with a custom colour scheme:
    // a bottom border and left alignment
    let titleStyle = doc.AddStyle({ builtinStyle: docModule.BuiltinStyle.Title });

    // Check whether it is a paragraph style; if it is, set the paragraph format too
    if (titleStyle instanceof docModule.ParagraphStyle) {
      let ps = titleStyle;
      ps.CharacterFormat.FontName = "Arial Unicode MS";
      ps.CharacterFormat.FontSize = 28;
      ps.CharacterFormat.TextColor = docModule.Color.FromArgb(42, 123, 136);
      ps.ParagraphFormat.Borders.Bottom.BorderType = docModule.BorderStyle.Single;
      ps.ParagraphFormat.Borders.Bottom.Color = docModule.Color.FromArgb(42, 123, 136);
      ps.ParagraphFormat.Borders.Bottom.LineWidth = 1.5;
      ps.ParagraphFormat.HorizontalAlignment = docModule.HorizontalAlignment.Left;
    }

    // Body style: one font and one size for the body text
    let normalStyle = doc.AddStyle({ builtinStyle: docModule.BuiltinStyle.Normal });
    normalStyle.CharacterFormat.FontName = "Arial Unicode MS";
    normalStyle.CharacterFormat.FontSize = 11;

    // Heading 1 style
    let heading1Style = doc.AddStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
    heading1Style.CharacterFormat.FontName = "Arial Unicode MS";
    heading1Style.CharacterFormat.FontSize = 14;
    heading1Style.CharacterFormat.Bold = true;
    heading1Style.CharacterFormat.TextColor = docModule.Color.FromArgb(42, 123, 136);

    // Heading 2 style
    let heading2Style = doc.AddStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    heading2Style.CharacterFormat.FontName = "Arial Unicode MS";
    heading2Style.CharacterFormat.FontSize = 12;
    heading2Style.CharacterFormat.Bold = true;

    // Custom bulleted list style
    let bulletList = doc.Styles.Add({ listType: docModule.ListType.Bulleted, name: "bulletList" });
    doc.Styles.Add({ style: bulletList });

    // Apply the styles: built-in styles by builtinStyle, custom styles by name
    let paragraph = sec.AddParagraph();
    paragraph.AppendText("Quarterly Operations Report");
    paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Title });

    paragraph = sec.AddParagraph();
    paragraph.AppendText("Prepared by: Operations Management Department | Date: September 2026");
    paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Normal });

    paragraph = sec.AddParagraph();
    paragraph.AppendText("Overall Progress");
    paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });

    paragraph = sec.AddParagraph();
    paragraph.AppendText("All three product lines stayed on plan this quarter, and the overall delivery pace was steady.");
    paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Normal });

    paragraph = sec.AddParagraph();
    paragraph.AppendText("Key Items");
    paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });

    paragraph = sec.AddParagraph();
    paragraph.AppendText("Key Milestones");
    paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });

    paragraph = sec.AddParagraph();
    paragraph.AppendText("Core module integration testing completed");
    paragraph.ListFormat.ApplyStyle("bulletList");

    paragraph = sec.AddParagraph();
    paragraph.AppendText("Trial run phase started");
    paragraph.ListFormat.ApplyStyle("bulletList");

    paragraph = sec.AddParagraph();
    paragraph.AppendText("Pre-release review scheduled");
    paragraph.ListFormat.ApplyStyle("bulletList");

    paragraph = sec.AddParagraph();
    paragraph.AppendText("Resource Input");
    paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });

    paragraph = sec.AddParagraph();
    paragraph.AppendText("Team capacity is tight at the moment; confirm the schedule early in the quarter.");
    paragraph.ListFormat.ApplyStyle("bulletList");

    // Define the output file name
    const outputFileName = "Styles-result.docx";

    // Save the document to the given path
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    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 Styles and Apply Them to Paragraphs</h1>
      <button onClick={Styles}>Generate</button>
    </div>
  );
}
export default App;

The built-in Title, Heading 1 and Heading 2 have been rewritten into one teal colour scheme, and the custom bulletList supplies the bullets

The result after the built-in styles have been modified and applied


Copy Styles Between Documents

Companies often keep a "style master" document whose styles new documents have to follow. Rebuilding those styles one at a time by hand is slow and easy to miss something, so walk the Styles collection of the source document and add every style object to the target document instead.

document.Styles supports Count and get_Item(index), so the collection can be walked completely by index. Once the styles of the source document have been added to the target document, the paragraphs in the target that reference those style names immediately show the formatting of the source document.

This example uses two sample documents: CopyDocumentStyles1.docx is the source document with custom styles, and CopyDocumentStyles2.docx is the target document — some of its paragraphs reference style names that only the source document defines, but it does not define them itself, so before the copy those paragraphs are displayed with the default formatting.

function App() {
  const CopyDocumentStyles = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the two sample files into the virtual file system (VFS)
    let inputFileName_1 = "CopyDocumentStyles1.docx";
    await window.spire.FetchFileToVFS(inputFileName_1, "", `${process.env.PUBLIC_URL}static/data/`);

    let inputFileName_2 = "CopyDocumentStyles2.docx";
    await window.spire.FetchFileToVFS(inputFileName_2, "", `${process.env.PUBLIC_URL}static/data/`);

    // Load the source document (with the custom styles)
    let srcDoc = new docModule.Document();
    srcDoc.LoadFromFile(inputFileName_1);

    // Load the target document (built-in styles only)
    let destDoc = new docModule.Document();
    destDoc.LoadFromFile(inputFileName_2);

    // Take the style collection of the source document
    let styles = srcDoc.Styles;

    // Add them to the target document one by one
    for (let i = 0; i < styles.Count; i++) {
      let style = styles.get_Item(i);
      destDoc.Styles.Add(style);
    }

    // Define the output file name
    const outputFileName = "CopyDocumentStyles_result.docx";

    // Save the document to the given path
    destDoc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
    destDoc.Dispose();
    srcDoc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    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>Copy the Styles of the Source Document into the Target Document</h1>
      <button onClick={CopyDocumentStyles}>Generate</button>
    </div>
  );
}
export default App;

After the copy the target document has the custom styles of the source document, and the paragraphs that "referenced a style without carrying its formatting" are displayed correctly again

The result before and after the styles have been copied


FAQ

The Normal style was changed, but not every body paragraph followed

Cause: Only styles that inherit from Normal are affected by it. If a paragraph carries direct formatting of its own (a per-paragraph CharacterFormat.FontName, for example), the direct formatting has a higher priority than the style and covers up what the style sets.

Solution: Control the appearance through styles consistently and remove the direct formatting from the paragraphs and the runs. Normal is commonly used to unify the body font and the font size:

let normalStyle = document.AddStyle({ builtinStyle: wasmModule.BuiltinStyle.Normal });
normalStyle.CharacterFormat.FontName = "Arial Unicode MS";
normalStyle.CharacterFormat.FontSize = 11;

The number of styles in the target document doubled after the copy

Cause: The walk copies every style of the source document across, and that includes a large number of built-in styles. Those style names usually already exist in the target document, and adding them one at a time creates duplicate entries.

Solution: This is the actual behaviour of the sample (the style collection grows noticeably). If a real project only needs the custom styles, filter by name first and copy only the styles the target document does not have yet:

for (let i = 0; i < srcDoc.Styles.Count; i++) {
  let style = srcDoc.Styles.get_Item(i);

  // Check whether the target document already has a style with the same name
  let exists = false;
  for (let j = 0; j < destDoc.Styles.Count; j++) {
    if (destDoc.Styles.get_Item(j).Name === style.Name) {
      exists = true;
      break;
    }
  }

  // Only add the styles that the target document is missing
  if (!exists) {
    destDoc.Styles.Add(style);
  }
}

A custom style does not take effect when it is applied to a paragraph

Cause: ApplyStyle is called differently for built-in and custom styles — a built-in style takes a { builtinStyle } object, a custom style takes the style name as a string. Passing the wrong form is ignored silently.

Solution: Choose the form that matches where the style comes from:

// Built-in style
paragraph.ApplyStyle({ builtinStyle: wasmModule.BuiltinStyle.Heading1 });

// Custom style (the string name, which has to match the name used in Add)
paragraph.ListFormat.ApplyStyle("bulletList");

Get a Free License

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

A PDF's properties panel records the title, author, subject, and keywords, and knowledge bases, archival systems, and full-text search all use them as the basis for classification. The files you actually receive tend to be the opposite: the title still carries the name left over from a previous template, the author field is empty, and keywords are missing altogether. Filling them in means typing into each field by hand in a reader, and checking the author or subject of a batch of documents means opening the properties dialog one file at a time — desktop software cannot do it in bulk, and uploading the files to a server means the content leaves the user's device.

This article uses Spire.PDF for JavaScript to set and get PDF document properties. It runs on WebAssembly to load, modify, and save PDFs directly in the browser, working through a virtual file system (VFS) with no backend required.

This article covers two core features:

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


Set PDF Document Properties

Spire.PDF for JavaScript provides doc.DocumentInformation for writing the standard document properties — title, author, subject, and keywords each take one field, while Creator and Producer record who generated the file, all of them plain strings.

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

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

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

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

    // Write the standard document properties
    doc.DocumentInformation.Title = '2026 Product Overview';
    doc.DocumentInformation.Author = 'Marketing Department';
    doc.DocumentInformation.Subject = 'Product Line and Pricing';
    doc.DocumentInformation.Keywords = 'product overview, pricing, 2026';
    doc.DocumentInformation.Creator = 'Content Center';
    doc.DocumentInformation.Producer = 'Spire.PDF for JavaScript';

    const outputFileName = 'Properties-Set.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    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>Set PDF Document Properties</h1>
      <button onClick={setPdfProperties}>
        Set Properties
      </button>
    </div>
  );
}

export default App;

The standard fields shown in the reader's document properties panel after setting:

The standard fields shown in the reader's document properties panel after setting


Get PDF Document Properties

Reading goes through the same DocumentInformation: the standard fields come back as strings. Joining the values you get into text and writing it out lets a batch pipeline compare or store them directly, without going through a reader's properties panel.

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

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

    // Load the PDF to be read into the VFS
    const inputFileName = 'Properties-Set.pdf';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

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

    // Read the standard properties and the creation/modification dates one by one
    const info = doc.DocumentInformation;

    const lines = [
      `Title: ${info.Title}`,
      `Author: ${info.Author}`,
      `Subject: ${info.Subject}`,
      `Keywords: ${info.Keywords}`,
      `Creator: ${info.Creator}`,
      `Producer: ${info.Producer}`,
      `CreationDate: ${info.CreationDate.toString()}`,
      `ModificationDate: ${info.ModificationDate.toString()}`,
    ];

    // Write the result to a text file
    const outputFileName = 'Document-Properties.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, lines.join('\n'));
    doc.Close();

    // Read the generated file from the VFS and trigger the download
    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 PDF Document Properties</h1>
      <button onClick={getPdfProperties}>
        Get Properties
      </button>
    </div>
  );
}

export default App;

The exported text file lists the standard properties that were read, one per line:

The exported text file lists the standard properties that were read, one per line


FAQ

Why do my property changes disappear after I reopen the file

Reason: the fields on DocumentInformation change the document object in memory, and only calling SaveToFile writes them into the file. Assign the values and close the document right away, or open the original input file again, and you will of course still see the old values.

Solution: after assigning the values, save the document to a new output file, then open that result to check it:

doc.DocumentInformation.Title = '2026 Product Overview';
doc.DocumentInformation.Author = 'Marketing Department';

// Only after saving do the properties land in the file
doc.SaveToFile('Properties-Set.pdf');

Get a Free License

If you want to remove the evaluation message from the result documents or get rid of feature limitations, please contact sales to obtain a free 30-day temporary license.

Send out a manual or report of a few hundred pages and the complaint is rarely about the content — it's that readers can't find the chapter they want. They want a page-numbered list of chapters up front, and one click to jump there. Many PDFs are generated without one, so readers are left to the scrollbar or in-document search.

This article shows how to create a table of contents page and add navigation to its entries with Spire.PDF for JavaScript. It loads, edits and saves PDF documents directly in the browser through WebAssembly, reading and writing files through a virtual file system (VFS), so everything runs locally with no backend.

Two core features are covered:

For installation and project setup, see Integrate Spire.PDF for JavaScript in a React Project. The examples below assume Spire.PDF is installed and the WebAssembly module is initialized.


Create a Table of Contents Page

The contents page has to land at a specific position in the document. Pages.Insert({ index }) inserts a page and returns it, and the title, chapter entries, leader dots and page numbers are all drawn on that page with Canvas.DrawString. Each entry advances horizontally by its text width, and leader dots fill the gap from the end of the title to the start of the page number.

function App() {
  const createTocPage = 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 to process into the VFS
    const inputFileName = 'Chapter_Document.pdf';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

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

    // Insert the contents page after the cover; the body pages shift down by one
    const tocPage = doc.Pages.Insert({ index: 1 });

    // Fonts for the title and the entries, using the built-in Helvetica (no font file to load)
    const titleFont = new pdfModule.PdfFont({ fontFamily: pdfModule.PdfFontFamily.Helvetica, size: 20, style: pdfModule.PdfFontStyle.Bold });
    const entryFont = new pdfModule.PdfFont({ fontFamily: pdfModule.PdfFontFamily.Helvetica, size: 14 });
    const centerFormat = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Center });

    // Draw the centered contents title
    const title = 'Contents';
    tocPage.Canvas.DrawString({
      s: title,
      font: titleFont,
      brush: pdfModule.PdfBrushes.get_Black(),
      point: new pdfModule.PointF(tocPage.Canvas.ClientSize.Width / 2, 50),
      format: centerFormat
    });

    // Chapter titles and their page numbers after the contents page is inserted
    const chapters = [
      { title: 'Chapter 1 Overview', page: 3 },
      { title: 'Chapter 2 Architecture', page: 4 },
      { title: 'Chapter 3 Deployment', page: 5 },
      { title: 'Chapter 4 Maintenance', page: 6 }
    ];

    const width = tocPage.Canvas.ClientSize.Width;
    let y = 110;
    for (const chapter of chapters) {
      // Entry text
      const titleSize = entryFont.MeasureString({ text: chapter.title });
      tocPage.Canvas.DrawString({ s: chapter.title, font: entryFont, brush: pdfModule.PdfBrushes.get_Black(), x: 40, y: y });

      // Right-aligned page number
      const pageText = chapter.page.toString();
      const pageSize = entryFont.MeasureString({ text: pageText });
      tocPage.Canvas.DrawString({ s: pageText, font: entryFont, brush: pdfModule.PdfBrushes.get_Black(), x: width - 40 - pageSize.Width, y: y });

      // Leader dots: fill from the end of the entry to the start of the page number
      const dotStart = 40 + titleSize.Width + 6;
      const dotEnd = width - 40 - pageSize.Width - 6;
      for (let x = dotStart; x < dotEnd; x += 6) {
        tocPage.Canvas.DrawString({ s: '.', font: entryFont, brush: pdfModule.PdfBrushes.get_Gray(), x: x, y: y });
      }

      y += 24;
    }

    // Define the output file name and save
    const outputFileName = 'Document-with-TOC.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    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 Table of Contents Page</h1>
      <button id="btn-1" onClick={createTocPage}>
        Create TOC
      </button>
    </div>
  );
}

export default App;

The document with a contents page: the page after the cover lists each chapter with its page number

The document with a contents page: the page after the cover lists each chapter with its page number


Add Navigation to Table of Contents Entries

Once the contents page is drawn, each entry is still just a line of text. To make an entry clickable, cover it with a PdfActionAnnotation hit area and attach a PdfGoToAction carrying a PdfDestination that names the target page. There is no need to derive the hit area's position from line spacing — search the entry's text on the contents page with PdfTextFinder, and the rectangle it returns is where that line actually sits on the page, ready to use as the hit area.

function App() {
  const addTocNavigation = 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 document generated in the previous step
    const inputFileName = 'Document-with-TOC.pdf';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // The contents page is page 2 of the document (index 1)
    const tocPage = doc.Pages.get_Item(1);

    // The entry text and the page each one should jump to
    const chapters = [
      { title: 'Chapter 1 Overview', page: 3 },
      { title: 'Chapter 2 Architecture', page: 4 },
      { title: 'Chapter 3 Deployment', page: 5 },
      { title: 'Chapter 4 Maintenance', page: 6 }
    ];

    // Search the contents page by keyword
    const finder = new pdfModule.PdfTextFinder(tocPage);

    for (const chapter of chapters) {
      const found = finder.Find(chapter.title);
      if (found.length === 0) {
        continue;
      }

      // Define the hit area based on the keyword position
      const lineBounds = found.get(0).Bounds[0];
      const bounds = new pdfModule.RectangleF({
        location: new pdfModule.PointF(0, lineBounds.Y),
        size: new pdfModule.SizeF({ width: tocPage.Canvas.ClientSize.Width, height: lineBounds.Height })
      });

      // The jump target is the chapter's page, aligned to the top-left corner of the body
      const targetPage = doc.Pages.get_Item(chapter.page - 1);
      const destination = new pdfModule.PdfDestination({
        page: targetPage,
        location: new pdfModule.PointF(0, 0)
      });

      // Attach the jump action and set the border width to 0
      const action = new pdfModule.PdfActionAnnotation(bounds, new pdfModule.PdfGoToAction({ destination }));
      action.Border = new pdfModule.PdfAnnotationBorder({ borderWidth: 0 });
      tocPage.Annotations.Add(action);
    }

    // Define the output file name and save
    const outputFileName = 'Clickable-TOC.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    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>Add Navigation to Table of Contents Entries</h1>
      <button id="btn-2" onClick={addTocNavigation}>
        Add Navigation
      </button>
    </div>
  );
}

export default App;

Clicking a chapter title in the contents page jumps to that page

Clicking a chapter title in the contents page jumps to that page


FAQ

Table of contents page numbers don't match the actual pages

Cause: the contents page is inserted into the original document, so every page after the insertion point shifts down by one. If the page numbers keep the order from before the insertion, they will be off by one across the board.

Solution: write the page numbers as they appear after the insertion. For example, if the cover was page 1 and chapter 1 was page 2, then after inserting the contents page after the cover, chapter 1 falls on page 3, and that is what the contents should list.

Clicking a table of contents entry jumps to the wrong chapter, or does nothing

Cause: the entry is drawn with Canvas.DrawString, but the hit area has to be given in page coordinates. Deriving it from the drawing y plus the line spacing means any mismatch in font metrics, line spacing or page margins accumulates row by row, so the click lands on a different entry — or on nothing at all.

Solution: don't derive it — search the contents page for the entry's text (the keyword) and use the rectangle that comes back. PdfTextFinder already returns page coordinates, so no top margin has to be added; get page with doc.Pages.get_Item(...) so it is a real page object:

const finder = new pdfModule.PdfTextFinder(tocPage);
const found = finder.Find(chapter.title);
const lineBounds = found.get(0).Bounds[0];
const bounds = new pdfModule.RectangleF({
  location: new pdfModule.PointF(0, lineBounds.Y),
  size: new pdfModule.SizeF({ width: tocPage.Canvas.ClientSize.Width, height: lineBounds.Height })
});
const targetPage = doc.Pages.get_Item(chapter.page - 1);
const action = new pdfModule.PdfActionAnnotation(bounds, new pdfModule.PdfGoToAction({ destination }));

Get a Free License

To remove the evaluation message from the generated documents, or to get rid of the function limitations, please contact sales for a temporary license valid for 30 days.

Page 2 of 361
page 2