Once a contract, quote, or trial document goes out as a PDF, the content is essentially in the recipient's hands, and when it stops being valid is left to their discretion. To make a document announce its expiry automatically after a set date, desktop software means opening each file and setting it by hand, which is hard to fit into a batch Web workflow; doing it server-side means uploading the files first, taking them off the user's device.

This article shows how to set an expiry date for a PDF document with Spire.PDF for JavaScript. Built on WebAssembly, it loads, modifies, and saves PDF documents directly in the browser. Everything runs locally, reading and writing files through a virtual file system (VFS), with no backend involved.

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


Set an Expiry Date for a PDF Document

Spire.PDF for JavaScript provides the PdfJavaScriptAction class, which wraps a JavaScript snippet as a PDF action. Assign it to PdfDocument.AfterOpenAction and the reader runs the snippet every time the document is opened — the snippet compares the current time with the configured date and, once past it, shows an alert and closes the document.

Step 1: Load the PDF to Process

First get the Spire.PDF WASM module and confirm it is ready, then read the PDF into the virtual file system (VFS) and hand it to PdfDocument to load.

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

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

Step 2: Write the Expiry Check Script

The expiry date lives in the script itself: new Date() captures the system time at the moment of opening and compares it with the configured deadline; if it is later, the two actions — alert and close — run. Both the alert text and the deadline are strings in this snippet, so changing the date or the wording happens here.

// Expiry check: alert and close the document once the current time is past the set date
const script =
  "var rightNow = new Date();" +
  "var endDate = new Date('January 1, 2026 23:59:59');" +
  "if (rightNow.getTime() > endDate.getTime()) {" +
  "app.alert('This document has expired, please contact us for a new one.', 1);" +
  "this.closeDoc();" +
  "}";

Step 3: Attach It to the Document's After-Open Action

The script does not run on its own; wrap it in a PdfJavaScriptAction and assign it to PdfDocument.AfterOpenAction. This is a document-level action that the reader runs when the document is opened, and it is written to disk along with the document on save.

// Wrap the script as a JavaScript action and attach it to the document's after-open action
doc.AfterOpenAction = new pdfModule.PdfJavaScriptAction(script);

// Save the document
const outputFileName = 'SetExpiryDate.pdf';
doc.SaveToFile(outputFileName);
doc.Close();

Step 4: Read the Result and Trigger the Download

The saved file is still in the VFS; read its bytes out, wrap them in a Blob, and hand a temporary link to the browser to download.

// Read the generated file from the VFS and trigger a 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);

Complete Code

Put the four steps back into a single App component and you have a complete, runnable example.

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

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

    // Expiry check: alert and close the document once the current time is past the set date
    const script =
      "var rightNow = new Date();" +
      "var endDate = new Date('January 1, 2026 23:59:59');" +
      "if (rightNow.getTime() > endDate.getTime()) {" +
      "app.alert('This document has expired, please contact us for a new one.', 1);" +
      "this.closeDoc();" +
      "}";

    // Wrap the script as a JavaScript action and attach it to the document's after-open action
    doc.AfterOpenAction = new pdfModule.PdfJavaScriptAction(script);

    // Save the document
    const outputFileName = 'SetExpiryDate.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger a 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 an Expiry Date for a PDF Document</h1>
      <button onClick={setExpiryDate}>
        Set Expiry Date
      </button>
    </div>
  );
}

export default App;

After setting an expiry date on the contract template, opening it in a JavaScript-capable reader shows an alert and then closes the expired document:

After setting an expiry date on the contract template, opening it in a JavaScript-capable reader shows an alert and then closes the expired document


FAQ

Why doesn't the alert appear when I open the document after setting the date

Cause: The expiry check depends on the reader running the JavaScript action embedded in the document. Built-in browser PDF viewers and most system preview tools do not execute embedded scripts; only JavaScript-capable readers such as Adobe Acrobat or Acrobat Reader run it when the document opens.

Fix: Open the generated PDF in a JavaScript-capable reader. The script is already written into the document, so switching readers is enough to see it — there is no need to regenerate the file.

Does the content become completely inaccessible once the document expires

Cause: The script shows an alert and closes the current reading session; it does not alter the document's content or encrypt it. The file is still an ordinary PDF and can be opened and text-extracted by any tool that does not run scripts.

Fix: To actually block access, encrypt the document and set an open password or permission restrictions. The expiry script is a reminder and a deterrent, not access control.

How do I change the expiry date or the alert text

Cause: The date and the alert are string literals in the script — the date inside new Date('…') and the text inside app.alert('…'). They are written into the document's open action together with the script, so changing document properties afterward does not affect them.

Fix: Edit these two strings in the script and generate the document again:

const script =
  "var rightNow = new Date();" +
  "var endDate = new Date('December 31, 2026 23:59:59');" +   // new expiry moment
  "if (rightNow.getTime() > endDate.getTime()) {" +
  "app.alert('Your trial has expired. Please purchase the full version.', 1);" +   // new alert text
  "this.closeDoc();" +
  "}";

How do I make the document expire 30 days after it is opened

Cause: The script runs every time the document is opened, and new Date() returns the system time at that moment. If the date is also derived from the opening time — for example new Date() plus 30 days — the computed deadline keeps moving forward on every open and the document never actually expires.

Fix: Write the expiry date as a fixed value: work out the moment 30 days out at generation time and put that in, leaving the script to only compare the current time against it:

// Compute a fixed moment at generation time and put it in the script; do not leave it to be calculated at open time
"var endDate = new Date('October 15, 2026 23:59:59');" +

Get a Free License

If you want to remove the evaluation message from the result document or lift the feature limits, contact sales for a temporary license valid for 30 days.

Formatting a cell range cell by cell means repeating the same group of properties down every column, and with enough columns one of them is easily missed; these formats also tend to be spread across fonts, borders and alignment, so each has to be set and then checked again on its own. Spire.XLS stores that group of formats as a named style, uses a flag object to state which of those formats should land this time, and then applies it to the target range: properties that were not ticked are not applied, and every other format on the range is left unchanged. Spire.XLS for JavaScript performs these operations in the browser through WebAssembly; the output file is managed by a virtual file system (VFS), with no backend service required.

This article covers two feature points:

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


Apply a named style to a cell range

Setting formats cell by cell means repeating the same block of code in every column, and with enough columns a cell is easily missed; fonts, borders and centring set separately would also mean working over the same block three times over. A named style stores a group of formats as an object that can be taken up repeatedly, and the flag object states which of those formats should land this time; together they apply the wanted parts to the whole range in a single call, leaving the range's other formatting unchanged. The steps are:

  1. Load the font file into the VFS, create a workbook, take the first worksheet and write the header and two data rows.
  2. Create a named style with workbook.Styles.Add and give it the font colour, size and italic the header will use.
  3. Set the top and bottom borders on that same style.
  4. Set horizontal and vertical centring on that same style.
  5. Create the flag object with new xlsModule.CellStyleFlag() and tick the properties to land this time; the items left unticked are not written into the target cells.
  6. Apply the style to the header range with sheet.Range.get('A1:E1').ApplyStyle(tableStyle, flag); set the column widths so the header text displays in full, then save the workbook and dispose of it to free resources.

The full code example below shows how to apply cell styles to an Excel cell range in React:

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

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

    // load the font file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // create a new workbook and take the first worksheet
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);

    // write the header and two data rows
    const columns = ['A', 'B', 'C', 'D', 'E'];
    const headers = ['Product', 'Spec', 'Quantity', 'Unit price', 'Amount'];
    headers.forEach((text, index) => {
      sheet.Range.get(`${columns[index]}1`).Value = text;
    });

    const rows = [
      ['Blu-ray mini projector', '1080P / 8G', '6', '1299', '7794'],
      ['Portable Bluetooth speaker', '20W / IPX7', '12', '399', '4788'],
    ];
    rows.forEach((row, rowIndex) => {
      columns.forEach((column, columnIndex) => {
        sheet.Range.get(`${column}${rowIndex + 2}`).Value = row[columnIndex];
      });
    });

    // create a named style
    const tableStyle = workbook.Styles.Add('tableStyle');

    // give the header its font colour, size and italic
    tableStyle.Font.Color = xlsModule.Color.get_CornflowerBlue();
    tableStyle.Font.Size = 16;
    tableStyle.Font.IsItalic = true;

    // set the top and bottom borders on the same style
    tableStyle.Borders.get(xlsModule.BordersLineType.EdgeTop).LineStyle = xlsModule.LineStyleType.Thin;
    tableStyle.Borders.get(xlsModule.BordersLineType.EdgeBottom).LineStyle = xlsModule.LineStyleType.Thin;

    // and set horizontal and vertical centring on the same style
    tableStyle.HorizontalAlignment = xlsModule.HorizontalAlignType.Center;
    tableStyle.VerticalAlignment = xlsModule.VerticalAlignType.Center;

    // create the flag object and tick the properties to land this time; unticked ones are not written to the target cells
    const flag = new xlsModule.CellStyleFlag();
    flag.FontColor = true;
    flag.FontSize = true;
    flag.FontItalic = true;
    flag.Borders = true;
    flag.HorizontalAlignment = true;
    flag.VerticalAlignment = true;

    // apply the style to the header range
    sheet.Range.get('A1:E1').ApplyStyle(tableStyle, flag);

    // set the column widths
    sheet.Columns.get(0).ColumnWidth = 22;
    sheet.Columns.get(1).ColumnWidth = 14;

    // save the workbook as an Excel file
    const outputFileName = 'ApplyStyleToRange.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2016 });

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start the 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>Apply cell styles to a cell range</h1>
      <button id="apply-style-to-range" onClick={applyStyleToRange}>Apply cell styles to the cell range</button>
    </div>
  );
}

export default App;

After running, the effect of applying the named style to the cell range:

Apply cell styles to a cell range


Read and adjust the named style

Once a style has been saved, later changes concern the values inside it rather than the whole set of formats, and no workbook has to be rebuilt: load the result file produced by feature point 1, read the named style by name, take the size already set in it, increase it, and apply the style to another range. The flag decides the scope here too: this time only the size, borders and centring are ticked, so the style's colour and italic are not written into the data range. Note that applying a style writes its properties into the cells as they stand at that moment, and the cells keep no reference to the style, so the range has to be applied again for the change to take effect. The steps are:

  1. Run the code of feature point 1 first, so that its result file ApplyStyleToRange.xlsx stays in the VFS as this run's input file.
  2. Load that input file with workbook.LoadFromFile and take the first worksheet.
  3. Read the named style feature point 1 created with workbook.Styles.get('tableStyle') and take its Font.Size as the baseline.
  4. Raise the baseline size and write it back to the style, as the size the data range will use.
  5. Create the flag object and tick only the size, borders and centring; apply the adjusted style to the data range with sheet.Range.get('A2:E3').ApplyStyle(readBack, flag), set the column widths so the larger font size still fits, then save the workbook and dispose of it to free resources.

The full code example below shows how to load a result file, read the named style and apply it after adjusting the size in React:

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

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

    // load the font file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // load the result file produced by feature point 1 as this run's input
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: 'ApplyStyleToRange.xlsx' });
    const sheet = workbook.Worksheets.get(0);

    // read the named style feature point 1 created and take its current font size as the baseline
    const readBack = workbook.Styles.get('tableStyle');
    const baseSize = readBack.Font.Size;

    // raise the baseline size to get the size the data range will use
    readBack.Font.Size = baseSize + 4;

    // create the flag object: this time tick only the font size, borders and centring, so the style's colour and italic stay off the data range
    const flag = new xlsModule.CellStyleFlag();
    flag.FontSize = true;
    flag.Borders = true;
    flag.HorizontalAlignment = true;
    flag.VerticalAlignment = true;

    // apply the adjusted style to the data range
    sheet.Range.get('A2:E3').ApplyStyle(readBack, flag);

    // set the column widths so the larger font size still fits
    sheet.Columns.get(0).ColumnWidth = 30;
    sheet.Columns.get(1).ColumnWidth = 20;

    // save the workbook as an Excel file
    const outputFileName = 'ReadAndAdjustFontStyle.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2016 });

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start the 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>Read the style and adjust its size</h1>
      <button id="read-and-adjust-font-style" onClick={readAndAdjustFontStyle}>Read the named style, adjust it, then apply it</button>
    </div>
  );
}

export default App;

After running, the effect of reading the named style and adjusting its font size:

Read the named style and adjust its size


FAQ

Do the number formats already on the range change when I apply a style

Reason: No. Only the items ticked in the flag object are written into the target cells when the style is applied; the items left unticked take no part, so the number formats, borders and alignment already on the cells remain as they were.

Solution: Conversely, to apply number formats along with it, set the number format on the style as well and tick the matching item in the flag:

// give the style the number format first
tableStyle.NumberFormat = '0.00';

// tick the matching item in the flag
flag.NumberFormat = true;
sheet.Range.get('A1:E1').ApplyStyle(tableStyle, flag);

How do I apply a style to the whole worksheet

Reason: The examples above apply the style to one range at a time; a whole worksheet treated the same way would mean listing every range to cover. The worksheet object carries a method of the same name that covers the whole sheet in a single call.

Solution: Call ApplyStyle on the worksheet object to apply the whole style to the whole sheet:

sheet.ApplyStyle(tableStyle);

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.

PDF hyperlinks are supposed to be the easiest part of a document — one click and you are there, with no copying and no typing. But once they have to be maintained in bulk, the situation flips: a product document changes domain and every URL has to be fixed one by one; an old manual is about to be published and its external links have to be cleared first. In desktop software these jobs mean clicking through by hand, or cannot be done in bulk at all.

This article shows how to modify and delete hyperlinks with Spire.PDF for JavaScript. It is built on WebAssembly and loads, modifies and saves PDF documents directly in the browser; the whole process runs locally through a virtual file system (VFS), with no backend required.

This article covers two key operations:

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


Modify Hyperlinks

When a document's URLs change domain, resetting them one by one is tedious. An existing link annotation can be read and written through PdfUriAnnotationWidget, whose Uri property is the link's target address. Walking the annotation collection page by page and rewriting the matches changes links in bulk.

function App() {
  const updateHyperlinks = 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 be processed into the VFS
    const inputFileName = 'ProductManual.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);

    // The new link address
    const newUrl = 'https://www.e-iceblue.com/Introduce/pdf-for-javascript.html';

    // Walk the annotations page by page and rewrite the URI links
    for (let i = 0; i < doc.Pages.Count; i++) {
      const page = doc.Pages.get_Item(i);
      for (let j = 0; j < page.Annotations.Count; j++) {
        const annotation = page.Annotations.get_Item(j);
        // Handle only link annotations; leave other annotation types as they are
        if (annotation instanceof pdfModule.PdfUriAnnotationWidget) {
          annotation.Uri = newUrl;
        }
      }
    }

    // Save the resulting document
    const outputFileName = 'HyperlinksUpdated.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger a download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const downloadUrl = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = downloadUrl;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(downloadUrl);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Modify Hyperlinks</h1>
      <button onClick={updateHyperlinks}>
        Update Hyperlinks
      </button>
    </div>
  );
}

export default App;

The three hyperlinks already in the document now point to the new address:

The three hyperlinks already in the document now point to the new address


Delete Hyperlinks

Deleting hyperlinks is likewise a walk over the annotation collection, calling Remove on the matches. The difference is collecting first and deleting after: removing during the walk shifts the collection forward, and the loop then skips the entry right after the removed one.

function App() {
  const deleteHyperlinks = 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 be processed into the VFS
    const inputFileName = 'ProductManual.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);

    // Collect all link annotations first
    const links = [];
    for (let i = 0; i < doc.Pages.Count; i++) {
      const page = doc.Pages.get_Item(i);
      for (let j = 0; j < page.Annotations.Count; j++) {
        const annotation = page.Annotations.get_Item(j);
        if (annotation instanceof pdfModule.PdfUriAnnotationWidget) {
          links.push({ page, annotation });
        }
      }
    }

    // Remove them all after the collection is complete
    for (const link of links) {
      link.page.Annotations.Remove(link.annotation);
    }

    // Save the resulting document
    const outputFileName = 'HyperlinksRemoved.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger a download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const downloadUrl = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = downloadUrl;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(downloadUrl);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Delete Hyperlinks</h1>
      <button onClick={deleteHyperlinks}>
        Delete Hyperlinks
      </button>
    </div>
  );
}

export default App;

All hyperlinks in the document have been removed:

All hyperlinks in the document have been removed


FAQ

I changed Uri, but the address is unchanged after saving

Cause: link annotations are not a single class. The Uri property exists only on PdfUriAnnotationWidget. A hyperlink exported from Word, or produced by Spire's PdfTextWebLink, is another class, PdfTextWebLinkAnnotationWidget, whose address property is called Url. Code that tests only one class skips the other entirely.

Solution: test both classes and set the matching property:

if (annotation instanceof pdfModule.PdfUriAnnotationWidget) {
  annotation.Uri = newUrl;
} else if (annotation instanceof pdfModule.PdfTextWebLinkAnnotationWidget) {
  annotation.Url = newUrl;
}

Why does deleting while iterating remove only some of the links?

Cause: page.Annotations is a mutable annotation collection. After item i is removed, the annotations behind it shift up by one, while the loop's i still increments as usual, so the entry immediately after the removed one is skipped. With several links on a page, several are missed.

Solution: collect the annotations to delete into an array first, then remove them all after the loop:

const links = [];
// ...collect while iterating...

// remove after the collection is complete
for (const link of links) {
  link.page.Annotations.Remove(link.annotation);
}

Does deleting hyperlinks also delete comments?

Cause: page.Annotations holds more than links — text box annotations, popups, form fields and so on all live in the same collection. Calling Clear(), or deleting everything without checking, wipes those out too.

Solution: delete only link annotations and leave the rest untouched:

if (annotation instanceof pdfModule.PdfUriAnnotationWidget) {
  page.Annotations.Remove(annotation);
}

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.

A table has only a few seconds to be understood, and the work is done not only by the numbers but by the fonts: a title big enough to stand out, a figure that matters found at a glance, a cancelled line spotted immediately. In Excel every one of those differences is carried by a font object belonging to a single cell -- font name, size, colour, bold, italic, underline, strikethrough; changing a cell touches only that cell. One cell can also hold several fonts at once, and switching a single word to another colour or size lifts it out of the sentence around it. Spire.XLS for JavaScript performs these operations in the browser through WebAssembly; the output file is managed by a virtual file system (VFS), with no backend service required.

This article covers two feature points:

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


Set the font formatting of a cell

Every cell carries a font object of its own, and all seven properties -- font name, size, colour, bold, italic, underline and strikethrough -- hang off that object. To see what each one does, start a worksheet of its own, write the property's name and a sample of text row by row, and set the matching property on the cell that holds the sample: a change lands on that cell alone, and the description in column A keeps its own look. The steps are:

  1. Load the font file into the VFS with window.spire.FetchFileToVFS.
  2. Create a workbook with new Workbook() and take the first worksheet with workbook.Worksheets.get(0).
  3. Write the properties out one per row with sheet.Range.get: the name into column A and the sample text into column B, rows starting at 1 and going up by 2.
  4. Set the matching format on each row's sample through FontName, Size, Color, IsBold, IsItalic, Underline and IsStrikethrough on Range.Style.Font, taking the font colour from xlsModule.Color.get_Red() and the underline from xlsModule.FontUnderlineType.Single.
  5. Set both columns to a width of 25 with sheet.Columns.get(0).ColumnWidth and sheet.Columns.get(1).ColumnWidth.
  6. Save the workbook with workbook.SaveToFile and dispose of it with workbook.Dispose to free resources.

The full code example below shows how to set font formatting for cells in React:

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

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

    // load the font file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // create a new workbook and take the first worksheet
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);

    // set the font name
    let row = 1;
    sheet.Range.get(`A${row}`).Value = 'Font name';
    sheet.Range.get(`B${row}`).Value = 'Arial';
    sheet.Range.get(`B${row}`).Style.Font.FontName = 'Arial';

    // set the font size
    row += 2;
    sheet.Range.get(`A${row}`).Value = 'Font size';
    sheet.Range.get(`B${row}`).Value = '15';
    sheet.Range.get(`B${row}`).Style.Font.Size = 15;

    // set the font colour
    row += 2;
    sheet.Range.get(`A${row}`).Value = 'Font colour';
    sheet.Range.get(`B${row}`).Value = 'Red';
    sheet.Range.get(`B${row}`).Style.Font.Color = xlsModule.Color.get_Red();

    // bold the text
    row += 2;
    sheet.Range.get(`A${row}`).Value = 'Bold';
    sheet.Range.get(`B${row}`).Value = 'Bold';
    sheet.Range.get(`B${row}`).Style.Font.IsBold = true;

    // italicise the text
    row += 2;
    sheet.Range.get(`A${row}`).Value = 'Italic';
    sheet.Range.get(`B${row}`).Value = 'Italic';
    sheet.Range.get(`B${row}`).Style.Font.IsItalic = true;

    // underline the text
    row += 2;
    sheet.Range.get(`A${row}`).Value = 'Underline';
    sheet.Range.get(`B${row}`).Value = 'Underline';
    sheet.Range.get(`B${row}`).Style.Font.Underline = xlsModule.FontUnderlineType.Single;

    // strike through the text
    row += 2;
    sheet.Range.get(`A${row}`).Value = 'Strikethrough';
    sheet.Range.get(`B${row}`).Value = 'Strikethrough';
    sheet.Range.get(`B${row}`).Style.Font.IsStrikethrough = true;

    // set the column widths
    sheet.Columns.get(0).ColumnWidth = 25;
    sheet.Columns.get(1).ColumnWidth = 25;

    // save the workbook as an Excel file
    const outputFileName = 'CellFont.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2016 });

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start the 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>Set Cell Font Formatting</h1>
      <button id="cell-font" onClick={setCellFont}>Set the font formatting of a cell</button>
    </div>
  );
}

export default App;

After running, the effect of setting font formatting for individual cells:

Font formatting applied to cells


Mix several fonts inside one cell

A cell's rich text object lets one run of text be cut at character positions and each piece given a different font, while the sentence still reads as a whole. The way to do it is to create the font objects first and then assign them range by range with SetFont -- each call overwrites one range, so every part of the sentence has to be covered. Ranges count from 0 and include both ends. The steps are:

  1. Load the font file into the VFS with window.spire.FetchFileToVFS.
  2. Create a workbook with new Workbook() and take the first worksheet with workbook.Worksheets.get(0).
  3. Create two fonts with workbook.CreateFont(): one for the whole sentence, and one that emphasises the key phrase through IsBold and KnownColor.
  4. Take the cell that holds the sentence with sheet.Range.get('A1'), and write the whole sentence into its rich text object through RichText.Text.
  5. Work out the start and end of the text that needs the new font within the sentence with note.indexOf(key) and key.length, then assign the fonts to the three parts -- before the key phrase, the key phrase itself and after it -- with RichText.SetFont.
  6. Save the workbook with workbook.SaveToFile and dispose of it with workbook.Dispose to free resources.

The full code example below shows how to give part of the text in one cell a different font in React:

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

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

    // load the font file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // create a new workbook and take the first worksheet
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);

    // create the first font, for the whole sentence
    const font1 = workbook.CreateFont();
    font1.FontName = 'Arial';
    font1.Size = 12;

    // create the second font, for the key phrase
    const font2 = workbook.CreateFont();
    font2.FontName = 'Arial';
    font2.Size = 14;
    font2.IsBold = true;
    font2.KnownColor = xlsModule.ExcelColors.Red;

    // take the cell's rich text object and write the whole sentence
    const note = 'This quotation is valid for 30 days; contact your sales representative to extend it.';
    const key = '30 days';
    const richText = sheet.Range.get('A1').RichText;
    richText.Text = note;

    // locate the character range the key phrase occupies
    const start = note.indexOf(key);
    const end = start + key.length - 1;

    // set the part before the key phrase to the first font
    richText.SetFont(0, start - 1, font1);

    // set the key phrase itself to the second font
    richText.SetFont(start, end, font2);

    // keep the part after the key phrase in the first font
    richText.SetFont(end + 1, note.length - 1, font1);

    // save the workbook as an Excel file
    const outputFileName = 'RichTextFont.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2016 });

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start the 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>Mix Several Fonts Inside One Cell</h1>
      <button id="rich-text-fonts" onClick={setRichTextFonts}>Give part of the text a different font</button>
    </div>
  );
}

export default App;

After running, the effect of mixing several fonts inside one cell:

Two fonts mixed inside one cell


FAQ

What value should SetFont's end position take

Reason: Both position arguments of SetFont count from 0 and include the end position itself, so passing a string's length directly lands one character past the end.

Solution: Pass "length minus one" as the end position. To cover a whole run of text, use the sentence's length minus one:

richText.SetFont(0, note.length - 1, font1);

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.

An Excel comment usually carries a paragraph of text, explaining a number or adding a note. When the thing to be explained is itself a picture -- a supplier mark, a product photo, a sketch of a part -- text alone will not do. Spire.XLS for JavaScript can fill a comment's background with a picture, so the image appears beside the cell without a separate floating picture object being placed on the sheet. A comment's background is controlled by its fill object, and switching to a solid colour likewise changes only that one object. Spire.XLS for JavaScript performs these operations in the browser through WebAssembly; input and output files are managed by a virtual file system (VFS), with no backend service required.

This article covers two feature points:

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


Insert a picture comment into a cell

A comment is attached to a cell; giving one a picture means first obtaining the comment object on the target cell and then filling it. The picture becomes the comment box's background, replacing the default pale-yellow shading, and it is this picture that appears when the mouse moves over the cell or the file is opened. The picture is scaled to the size of the comment box, and the default box is a flat rectangle, so a square picture is stretched out of shape; after filling, the width and height must therefore be set so that the box matches the picture's proportions. Where the box sits on the sheet is decided by a separate pair of position properties, so it does not have to follow the cell border. The steps are:

  1. Load the font, the picture and the test data file into the VFS.
  2. Load the workbook with workbook.LoadFromFile.
  3. Take the cell the comment will be attached to with sheet.Range.get('A4').
  4. Create a comment object on the cell with range.AddComment().
  5. Fill the comment background with the picture using comment.Fill.CustomPicture, passing the picture as a stream built by new xlsModule.Stream('logo.png') and 'None' for the name argument.
  6. Set comment.Width and comment.Height so the comment box is square, matching the square picture, which keeps the picture from being stretched.
  7. Place the comment box relative to the top-left corner of the worksheet with comment.Top and comment.Left, so that it covers the content below without obscuring its own row.
  8. Set comment.Visible to show the comment as soon as the file is opened.
  9. Save the workbook with workbook.SaveToFile.
  10. Dispose of the workbook with workbook.Dispose to free resources.

The complete code example below shows how to insert a picture comment into an Excel cell in React:

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

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

    // load the font, the picture and the test data file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    await window.spire.FetchFileToVFS('logo.png', '', `${process.env.PUBLIC_URL}data/`);
    const inputFileName = 'ProductList.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // load the workbook
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });

    // take the first worksheet and add a comment to cell A4, filled with the picture
    const sheet = workbook.Worksheets.get(0);
    const range = sheet.Range.get('A4');
    const comment = range.AddComment();
    comment.Fill.CustomPicture({ im: new xlsModule.Stream('logo.png'), name: 'None' });

    // set the width and height of the comment box
    comment.Width = 150;
    comment.Height = 150;

    // place the comment box relative to the top-left corner of the worksheet
    comment.Top = 100;
    comment.Left = 400;

    // make the comment show as soon as the file is opened
    comment.Visible = true;

    // save the workbook
    const outputFileName = 'PictureComment.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start the 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>Insert Comments with Pictures</h1>
      <button id="picture-comment" onClick={addPictureComment}>Insert a picture comment into a cell</button>
    </div>
  );
}

export default App;

After running, the effect of inserting a picture comment into a cell:

Picture comment inserted into a cell


Fill a comment with a solid colour

A comment background need not be a picture. When all that is needed is for one comment to stand out from the pale-yellow shading around it, specifying a solid colour is more straightforward than preparing an image. Filling with a picture and filling with a solid colour use the same comment.Fill object; only the fill type changes from picture to solid. The steps are:

  1. Load the font and the test data file into the VFS.
  2. Load the workbook with workbook.LoadFromFile.
  3. Take the cell the comment will be attached to with sheet.Range.get('A4') and create the comment with range.AddComment().
  4. Write the comment text with comment.Text.
  5. Create a font with workbook.CreateFont() and set the whole text to it with comment.RichText.SetFont.
  6. Set comment.Fill.FillType to the solid fill type, xlsModule.ShapeFillType.SolidColor.
  7. Pick the colour with comment.Fill.ForeColor.
  8. Set comment.Visible to show the comment as soon as the file is opened.
  9. Save the workbook with workbook.SaveToFile.
  10. Dispose of the workbook with workbook.Dispose to free resources.

The complete code example below shows how to fill an Excel comment with a solid colour in React:

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

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

    // load the font and the test data file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'ProductList.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // load the workbook
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });

    // take the first worksheet and add a comment to cell A4
    const sheet = workbook.Worksheets.get(0);
    const range = sheet.Range.get('A4');

    // create the font the comment text will use
    const font = workbook.CreateFont();
    font.FontName = 'Arial';
    font.Size = 11;
    font.KnownColor = xlsModule.ExcelColors.Orange;

    // write the comment text and set the whole text to that font
    const commentText = 'This product leads the season';
    range.Comment.Text = commentText;
    range.Comment.RichText.SetFont(0, commentText.length - 1, font);

    // set the comment fill to a solid colour and pick the colour
    range.Comment.Fill.FillType = xlsModule.ShapeFillType.SolidColor;
    range.Comment.Fill.ForeColor = xlsModule.Color.get_SkyBlue();
    range.Comment.Visible = true;

    // save the workbook
    const outputFileName = 'CommentFillColor.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start the 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>Insert Comments with Pictures</h1>
      <button id="comment-fill-color" onClick={fillCommentColor}>Fill a comment with a solid colour</button>
    </div>
  );
}

export default App;

After running, the effect of filling a comment with a solid colour:

Comment filled with a solid colour


FAQ

Why does the picture comment not appear when the result file is opened

Reason: Comments are hidden by default and appear only when the mouse moves over the cell. If the comment was not set to visible before saving, reopening the file shows an ordinary worksheet, and the picture comment does not appear on its own.

Solution: Set comment.Visible to true, and the comment appears as soon as the file is opened:

comment.Visible = true;

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.

Hyperlinks in a PDF are meant to save work: one click and you are there, no copying an address, no retyping it. The catch is that they are rarely there when you need them. A brand-new document should come with its download page, a contact address and a product link already in place. An older, already laid-out document should send the product name recurring in its body to the same introduction page. Doing it by hand means dragging a selection and pasting an address at every spot — and on a long document that never ends.

Moving the job into the browser is what Spire.PDF for JavaScript is for: it loads, modifies and saves PDF documents directly on WebAssembly, with PdfTextWebLink drawing visible link text in one step and PdfFileLinkAnnotation attaching a document link. Everything runs locally, reading and writing files through a virtual file system (VFS) with no backend involved.

This article covers two core tasks:

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


Add Hyperlinks to a PDF

When you build a document from scratch, everything on the page is decided by code, and links fall into a few kinds:

  • Simple text link: the URL drawn as ordinary text, with no annotation attached; whether it is clickable is up to the viewer — Chrome and others auto-detect the URL and make it clickable — so it is here only for contrast with the explicitly linked ones.
  • Hypertext link: PdfTextWebLink with Text, Url, Font and Brush set; DrawTextWebLink() draws the underlined link text in one step.
  • Document link: PdfFileLinkAnnotation(hotspot rectangle, target file), with the rectangle defined by you.
function App() {
  const addLinksToNewPdf = 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;
    }

    // Drawing text needs a font; load it into the VFS first
    const fontVfs = '/Library/Fonts/ARIAL UNICODE MS.TTF';
    await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Create a blank document
    const doc = new pdfModule.PdfDocument();

    // Add a section and set its margins to 0: coordinates then equal page coordinates
    const section = doc.Sections.Add();
    section.PageSettings.Size = pdfModule.PdfPageSize.A4();
    const margins = new pdfModule.PdfMargins();
    margins.Top = 0;
    margins.Bottom = 0;
    margins.Left = 0;
    margins.Right = 0;
    section.PageSettings.Margins = margins;

    const page = section.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFile: fontVfs, size: 13 });
    const linkFont = new pdfModule.PdfTrueTypeFont({ fontFile: fontVfs, size: 13, style: pdfModule.PdfFontStyle.Underline });
    const format = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Left });

    // Draw a plain line of text as a label
    const drawLabel = (text, y) => {
      page.Canvas.DrawString({
        s: text, font: font, brush: pdfModule.PdfBrushes.get_Black(), x: 60, y: y, format: format,
      });
    };

    // Draw a line of underlined text and hand back its rectangle as the hotspot
    const drawLinkText = (text, y) => {
      const size = linkFont.MeasureString({ text: text });
      page.Canvas.DrawString({
        s: text, font: linkFont, brush: pdfModule.PdfBrushes.get_Blue(), x: 160, y: y, format: format,
      });
      return new pdfModule.RectangleF({ x: 160, y: y, width: size.Width, height: size.Height });
    };

    const url = 'https://www.e-iceblue.com/Introduce/pdf-for-javascript.html';

    // 1) Simple text link: just the URL drawn as ordinary text, with no annotation attached (clickability depends on the viewer)
    let y = 80;
    drawLabel('Simple text link:', y);
    drawLinkText(url, y);

    // 2) Hypertext link: DrawTextWebLink draws the text and attaches the link in one step
    y += 34;
    drawLabel('Hypertext link:', y);
    const webLink = new pdfModule.PdfTextWebLink();
    webLink.Text = 'Visit our site';
    webLink.Url = url;
    webLink.Font = linkFont;
    webLink.Brush = pdfModule.PdfBrushes.get_Blue();
    webLink.DrawTextWebLink({ graphics: page.Canvas, location: new pdfModule.PointF(160, y) });

    // 3) Email link: same class, with a mailto: URL
    y += 34;
    drawLabel('Email link:', y);
    const mailLink = new pdfModule.PdfTextWebLink();
    mailLink.Text = 'Contact support';
    mailLink.Url = 'mailto:support@e-iceblue.com';
    mailLink.Font = linkFont;
    mailLink.Brush = pdfModule.PdfBrushes.get_Blue();
    mailLink.DrawTextWebLink({ graphics: page.Canvas, location: new pdfModule.PointF(160, y) });

    // 4) Document link: hotspot rectangle + target file
    y += 34;
    drawLabel('Document link:', y);
    const fileName = 'Report.xlsx';
    const fileBounds = drawLinkText('Open file', y);
    page.Annotations.Add(new pdfModule.PdfFileLinkAnnotation(fileBounds, fileName));

    // Save the result document
    const outputFileName = 'HyperlinkSample.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 downloadUrl = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = downloadUrl;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(downloadUrl);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Add Hyperlinks</h1>
      <button onClick={addLinksToNewPdf}>
        Start Creating
      </button>
    </div>
  );
}

export default App;

All four links — simple text, hypertext, email and document — are in place in the new document:

All four links — simple text, hypertext, email and document — are in place in the new document


Add Hyperlinks to Text

To add links to an existing document, work the other way round: find the position in the document first, then lay the link over it. PdfTextFinder searches the text of a given page by keyword, and each match carries a bounding rectangle. Build a PdfUriAnnotation from it and add it to the page annotation collection, and the link lands exactly on the original text instead of you doing the measuring.

function App() {
  const addHyperlinksToText = 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 = 'ProductManual.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);

    // Take the first page
    const page = doc.Pages.get_Item(0);

    // Search the page for the keyword; Parameter is case-sensitive by default, so switch to IgnoreCase
    const finder = new pdfModule.PdfTextFinder(page);
    finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
    const finds = finder.Find('Aurora');

    // Create one link annotation for each match
    const linkUrl = 'https://www.e-iceblue.com/Introduce/pdf-for-javascript.html';
    for (let i = 0; i < finds.length; i++) {
      // The bounding rectangle of a match is the link hotspot
      const annotation = new pdfModule.PdfUriAnnotation({ rectangle: finds.get(i).Bounds[0] });
      annotation.Uri = linkUrl;

      // Attach the annotation to the page annotation collection
      page.Annotations.Add(annotation);
    }

    // Save the result document
    const outputFileName = 'HyperlinksAdded.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 downloadUrl = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = downloadUrl;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(downloadUrl);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Add Hyperlinks to Text</h1>
      <button onClick={addHyperlinksToText}>
        Start Adding
      </button>
    </div>
  );
}

export default App;

All three occurrences of Aurora now carry a hyperlink:

All three occurrences of Aurora now carry a hyperlink


FAQ

Find() returns nothing even though the word is right there in the text

Cause: Options.Parameter defaults to TextFindParameter.None, which is case-sensitive, so searching for aurora will not match the capitalised Aurora; and the finder searches only the single page it was constructed with.

Fix: set it to IgnoreCase; to search the whole document, build one finder per page:

finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;

Measured: Aurora appears three times on page 1, and a case-sensitive search returns 0.

The link is there, but nothing shows up when the file is opened

Cause: this kind of link annotation is only a click hotspot — Spire generates no appearance stream (/AP) for it, and neither Chrome (PDFium) nor PyMuPDF (mupdf) draws a border for it; setting Border/Color only writes into the annotation dictionary (/Border [0 0 1], /C [0 0 1]) and stays invisible on the page.

Fix: the link is still clickable. In task 1 the hypertext, email, simple-text and document links all have their text drawn by the code, so they are visible to begin with; for a hotspot laid over existing text in task 2, do not rely on the annotation border — draw an underline at the link position with page.Canvas.DrawString().


Get a Free License

If you want to remove the evaluation message from the result document, or get past the feature limits, contact our sales team for a temporary 30-day license.

A shape in Excel is usually there to point at something: the arrow on a diagram, the triangle that flags a number, the cloud that circles an area. Give it a hyperlink and the drawing becomes an entry point a reader can click. A shape, like a picture, is not a cell, and its hyperlink lives on the shape object itself, so both setting and reading one go through the worksheet's shape collection rather than its cell hyperlink collection. Spire.XLS for JavaScript does this in the browser on WebAssembly, handling input and output files through a virtual file system (VFS) with no backend service.

This article covers two core feature points:

For installation and project setup, see Integrating Spire.XLS for JavaScript in a React Project. The examples below assume Spire.XLS is installed and the WebAssembly module has been initialised.


Set a hyperlink on a shape

Setting a hyperlink on a shape is what turns a drawing into a clickable entry point: a reader who clicks the shape on the worksheet opens the address it points at. The link belongs to the shape itself rather than to a cell, so the shape has to be taken from the worksheet's shape collection first, and the address handed to its own link object. The steps are:

  1. Load the font and the test data file into the VFS.
  2. Load the workbook with workbook.LoadFromFile.
  3. Add the shapes into the space left for them with sheet.PrstGeomShapes.AddPrstGeomShape.
  4. Take the shape count from sheet.PrstGeomShapes.Count and walk every shape on the worksheet with it.
  5. Take each shape's link object from its HyLink property and write the address into its Address.
  6. Save the workbook with workbook.SaveToFile.
  7. Dispose of the workbook with workbook.Dispose to free resources.

The complete code example below shows how to set a hyperlink on a shape in Excel in React:

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

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

    // load the font and the test data file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'ShapeLink.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // load the workbook
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });

    // take the first worksheet and add the three shapes into the space left for them
    const sheet = workbook.Worksheets.get(0);
    const shapes = sheet.PrstGeomShapes;
    shapes.AddPrstGeomShape(10, 1, 90, 60, xlsModule.PrstGeomShapeType.Triangle);
    shapes.AddPrstGeomShape(10, 3, 90, 60, xlsModule.PrstGeomShapeType.CurvedRightArrow);
    shapes.AddPrstGeomShape(10, 5, 90, 60, xlsModule.PrstGeomShapeType.Cloud);

    // set a hyperlink on each shape in turn
    const addresses = [
      'https://www.e-iceblue.com/Introduce/xls-for-javascript.html',
      'https://www.e-iceblue.com/Buy/Spire.XLS-JavaScript.html',
      'https://www.e-iceblue.com/Download/Spire-Office-JavaScript.html',
    ];
    for (let i = 0; i < shapes.Count; i++) {
      shapes.get(i).HyLink.Address = addresses[i];
    }

    // save the workbook
    const outputFileName = 'ShapeHyperlink.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, fileFormat: xlsModule.FileFormat.Version2010 });

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start the 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>Add Hyperlinks to Shapes in Excel</h1>
      <button id="shape-hyperlink" onClick={shapeHyperlink}>Set a hyperlink on the shape</button>
    </div>
  );
}

export default App;

After running, the effect of setting a hyperlink on a shape:

Set a hyperlink on a shape in Excel


Read the hyperlink from a shape

Reading the hyperlink off a shape is how a finished sheet is checked over: whether every shape carries a link, and whether they point at the right addresses. The addresses that come back are written out as a text file, which makes them easy to line up against a list of links. Shape hyperlinks are not kept in the cell hyperlink collection, so the shapes have to be taken from the worksheet one by one, and each one asked for its own link object. The steps are:

  1. Load the font and the test data file into the VFS.
  2. Load the workbook with workbook.LoadFromFile.
  3. Take the shape count from sheet.PrstGeomShapes.Count and walk every shape on the worksheet with it.
  4. Take this shape's link object from its HyLink property.
  5. Read the target address from the link object's Address, collecting the addresses as you go.
  6. Write the collected addresses into a text file with FS.writeFile, one address per line.
  7. Dispose of the workbook with workbook.Dispose to free resources.

The complete code example below shows how to read the hyperlink from a shape in Excel in React:

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

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

    // load the font and the test data file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'ShapeHyperlinks.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // load the workbook
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });

    // walk the shapes of the worksheet and read the hyperlink off each one
    const sheet = workbook.Worksheets.get(0);
    const shapes = sheet.PrstGeomShapes;
    const addresses = [];
    for (let i = 0; i < shapes.Count; i++) {
      addresses.push(shapes.get(i).HyLink.Address);
    }

    // write the addresses that were read into a text file
    const outputFileName = 'ReadShapeHyperlink.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, addresses.join('\n'));

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start 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>Add Hyperlinks to Shapes in Excel</h1>
      <button id="read-shape-hyperlink" onClick={readShapeHyperlink}>Read the hyperlink from the shape</button>
    </div>
  );
}

export default App;

After running, the effect of reading the hyperlink from a shape:

Read the hyperlink from a shape in Excel


FAQ

Why does a shape's hyperlink not show up in sheet.HyperLinks?

Cause: sheet.HyperLinks collects cell hyperlinks only. A shape is not a cell; it belongs to the worksheet's shape collection, and the link hangs on the shape object's HyLink.

Solution: Go through the shape collection. In this example, after a hyperlink was set on the shapes, sheet.HyperLinks.Count was still 0, and it was still 0 after the file was saved and opened again. Read a shape's link with sheet.PrstGeomShapes.get(i).HyLink.Address.

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.

Excel workbooks often need to tie scattered content together: one click on a summary page jumps to a detail worksheet, one click on a quote opens the source file kept beside it. That "jump elsewhere" ability belongs to cell hyperlinks, and the link type decides where the jump goes -- another worksheet inside the same workbook, or a file outside it. Spire.XLS for JavaScript does this in the browser through WebAssembly; input and output files are managed by a virtual file system (VFS), with no backend service required.

This article covers two feature points:

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


Add a link to another worksheet

A cross-sheet link solves the "jump from here to there" problem inside one workbook: it strings the content scattered across worksheets into a clickable path, so that the reader does not have to hunt for the right sheet. A link sits on a cell, so the first move is to create a hyperlink object on the target cell and then say, in turn, where it jumps and what text it shows. The steps are:

  1. Load the font and the test data file into the VFS.
  2. Load the workbook with workbook.LoadFromFile.
  3. Create a hyperlink object on the target cell with sheet.HyperLinks.Add.
  4. Mark the type as a workbook-internal link with the hyperlink's Type property, set to xlsModule.HyperLinkType.Workbook.
  5. Set the text shown on the cell with the TextToDisplay property.
  6. Write the jump target into the Address property, in the form "sheet name!cell", for example Data!B7.
  7. Style the link text blue and underlined so that it reads differently from ordinary text.
  8. Save the workbook with workbook.SaveToFile.
  9. Dispose of the workbook with workbook.Dispose to free resources.

The complete code example below shows how to add a cross-sheet hyperlink to an Excel cell in React:

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

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

    // load the font and the test data file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'SheetLink.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // load the workbook
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });

    // take the first worksheet and add a hyperlink object to cell B5
    const sheet = workbook.Worksheets.get(0);
    const cell = sheet.Range.get('B5');
    const hyperlink = sheet.HyperLinks.Add({ range: cell });

    // mark it as a workbook-internal link that points at cell B7 of the second worksheet
    hyperlink.Type = xlsModule.HyperLinkType.Workbook;
    hyperlink.TextToDisplay = 'View the full-year total in the data sheet';
    hyperlink.Address = 'Data!B7';

    // colour the link text blue and underline it so that it reads as a link
    cell.Style.Font.Color = xlsModule.Color.get_Blue();
    cell.Style.Font.Underline = xlsModule.FontUnderlineType.Single;

    // save the workbook
    const outputFileName = 'LinkToOtherSheetCell.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, fileFormat: xlsModule.FileFormat.Version2010 });

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start the 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>Add Cross-Sheet and External File Links in Excel</h1>
      <button id="link-to-other-sheet" onClick={linkToOtherSheet}>Add a link to another worksheet</button>
      <button id="link-to-external-file" onClick={linkToExternalFile}>Add a link to an external file</button>
    </div>
  );
}

export default App;

After running, the effect of adding a cross-sheet link:

Add a link to another worksheet to an Excel cell


Add a link to an external file

A cross-sheet link can only jump within the same workbook, so once the target falls outside it, the link has to become a file link. It differs from a cross-sheet link in just two places: the type is marked as a file link, and the jump target becomes a file path -- either a full path or a path relative to the current workbook. The steps are:

  1. Load the font and the test data file into the VFS.
  2. Load the workbook with workbook.LoadFromFile.
  3. Create a hyperlink object on the target cell with sheet.HyperLinks.Add.
  4. Mark the type as a file link with the Type property, set to xlsModule.HyperLinkType.File.
  5. Set the text shown on the cell with the TextToDisplay property.
  6. Write the path of the external file into the Address property, for example ../files/Quote.xlsx.
  7. Style the link text blue and underlined so that it reads differently from ordinary text.
  8. Save the workbook with workbook.SaveToFile.
  9. Dispose of the workbook with workbook.Dispose to free resources.

The complete code example below shows how to add an external-file hyperlink to an Excel cell in React:

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

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

    // load the font and the test data file into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'SheetLink.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // load the workbook
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });

    // take the first worksheet and add a hyperlink object to cell B6
    const sheet = workbook.Worksheets.get(0);
    const cell = sheet.Range.get('B6');
    const hyperlink = sheet.HyperLinks.Add({ range: cell });

    // mark it as a file link that points at a file outside the workbook
    hyperlink.Type = xlsModule.HyperLinkType.File;
    hyperlink.TextToDisplay = 'Open the external quote';
    hyperlink.Address = '../files/Quote.xlsx';

    // colour the link text blue and underline it so that it reads as a link
    cell.Style.Font.Color = xlsModule.Color.get_Blue();
    cell.Style.Font.Underline = xlsModule.FontUnderlineType.Single;

    // save the workbook
    const outputFileName = 'LinkToExternalFile.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, fileFormat: xlsModule.FileFormat.Version2010 });

    // dispose of the workbook to free resources
    workbook.Dispose();

    // read the result file from the VFS and start the 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>Add Cross-Sheet and External File Links in Excel</h1>
      <button id="link-to-other-sheet" onClick={linkToOtherSheet}>Add a link to another worksheet</button>
      <button id="link-to-external-file" onClick={linkToExternalFile}>Add a link to an external file</button>
    </div>
  );
}

export default App;

After running, the effect of adding an external-file link:

Add a link to an external file to an Excel cell


FAQ

Why does adding a link to a cell that already has text replace that text with the address

Reason: The text a cell shows is decided by TextToDisplay. Create a link without setting it and the address moves up to serve as the display text; the replacement happens the moment the link is added, so reading Range.Text afterwards returns the address rather than the original text.

Solution: Read the text before adding the link, then hand it back to TextToDisplay once the link is in place:

const cell = sheet.Range.get('B6');
const text = cell.Text;                  // keep the original text before adding the link
const hyperlink = sheet.HyperLinks.Add({ range: cell });
hyperlink.Type = xlsModule.HyperLinkType.File;
hyperlink.Address = '../files/Quote.xlsx';
hyperlink.TextToDisplay = text;          // hand the text back, so the cell still shows it

How do I remove a link but keep the text in the cell

Reason: The link and the text are two different things -- the link hangs on the cell, while the text is the cell's content. Range.ClearAll() clears the content, so using it to get rid of a link takes the text with it.

Solution: Remove the link by index with sheet.HyperLinks.RemoveAt, and the cell text stays as it was. For example, sheet.HyperLinks.RemoveAt(0) removes the first link; the text is still in the cell afterwards, it simply is no longer clickable.

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 margins decide how much blank space sits between the body and the edge of the paper. Too little and the content crowds the edge and looks cramped; too much and a page holds very little. Existing PDFs usually have their layout fixed already — adding a ring of white space, or trimming the excess away, used to mean re-laying out page after page in desktop software; and once documents are processed in bulk by a program, the margins have to follow the layout.

This article shows how to increase or decrease PDF page margins with Spire.PDF for JavaScript. It is built on WebAssembly and loads, modifies and saves PDF documents directly in the browser, all locally, reading and writing files through a virtual file system (VFS) with no backend involved.

This article covers two core features:

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


Increase PDF Page Margins

Increasing the margins is like giving the document a larger sheet of paper: use CreateTemplate() to capture each page's content as a template and draw it onto a new page with a larger size, where PdfMargins reserves the blank space on all four sides — the content keeps its size, and a ring of white space appears around it.

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

    // Load the original document and take its first page to compute the new page size
    const originalPdf = new pdfModule.PdfDocument();
    originalPdf.LoadFromFile(inputFileName);
    const firstPage = originalPdf.Pages.get_Item(0);

    // Add 40 points of padding on every side
    const marginsToAdd = new pdfModule.PdfMargins();
    marginsToAdd.Top = 40;
    marginsToAdd.Bottom = 40;
    marginsToAdd.Left = 40;
    marginsToAdd.Right = 40;

    // New page size = original size + the four margins, so the content area keeps its original size
    const newSize = new pdfModule.SizeF({
      width: firstPage.Size.Width + marginsToAdd.Left + marginsToAdd.Right,
      height: firstPage.Size.Height + marginsToAdd.Top + marginsToAdd.Bottom,
    });

    // Capture each page's content as a template and draw it onto a new page with margins
    const newPdf = new pdfModule.PdfDocument();
    for (let i = 0; i < originalPdf.Pages.Count; i++) {
      const template = originalPdf.Pages.get_Item(i).CreateTemplate();
      const page = newPdf.Pages.Add({ size: newSize, margins: marginsToAdd });
      page.Canvas.DrawTemplate({ template: template, location: new pdfModule.PointF(0, 0) });
    }

    const outputFileName = 'IncreaseMargins.pdf';
    newPdf.SaveToFile(outputFileName);
    newPdf.Close();
    originalPdf.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>Increase PDF Page Margins</h1>
      <button onClick={increasePdfMargins}>
        Start
      </button>
    </div>
  );
}

export default App;

Each side is padded by 40 points, so the page grows and a ring of white space appears around the original content:

Each side padded by 40 points, so the page grows and white space appears around the original content


Decrease PDF Page Margins

Decreasing the margins is the reverse: the new page is smaller than the original, and the template is drawn shifted toward the top-left, trimming the excess white space on all four sides. Note that the amount trimmed per side must not exceed the blank space already present on that side of the original document, or the content will be cut off at the page boundary.

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

    // Load the original document and take its first page to compute the new page size
    const originalPdf = new pdfModule.PdfDocument();
    originalPdf.LoadFromFile(inputFileName);
    const firstPage = originalPdf.Pages.get_Item(0);

    // Trim 20 points of white space off every side; the value must not exceed the blank already on that side
    const trim = 20;

    // New page size = original size − the four trimmed values
    const newSize = new pdfModule.SizeF({
      width: firstPage.Size.Width - trim * 2,
      height: firstPage.Size.Height - trim * 2,
    });

    // The new page reserves no extra margins of its own
    const noMargins = new pdfModule.PdfMargins();
    noMargins.Top = 0;
    noMargins.Bottom = 0;
    noMargins.Left = 0;
    noMargins.Right = 0;

    // Capture each page as a template and draw it shifted toward the top-left, trimming the white space on all four sides
    const newPdf = new pdfModule.PdfDocument();
    for (let i = 0; i < originalPdf.Pages.Count; i++) {
      const template = originalPdf.Pages.get_Item(i).CreateTemplate();
      const page = newPdf.Pages.Add({ size: newSize, margins: noMargins });
      page.Canvas.DrawTemplate({ template: template, location: new pdfModule.PointF(-trim, -trim) });
    }

    const outputFileName = 'DecreaseMargins.pdf';
    newPdf.SaveToFile(outputFileName);
    newPdf.Close();
    originalPdf.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>Decrease PDF Page Margins</h1>
      <button onClick={decreasePdfMargins}>
        Start
      </button>
    </div>
  );
}

export default App;

Each side is trimmed by 20 points of white space, so the page shrinks and the content moves closer to the paper's edge:

Each side trimmed by 20 points of white space, so the page shrinks and the content moves closer to the edge


FAQ

Why not just change the page margins of the original document

Cause: Margins only decide where the content area begins, positioning content drawn afterward; content already drawn in the original document is fixed at absolute coordinates, and reassigning PageSettings.Margins does not move it.

Solution: Capture the original page's content as a template with CreateTemplate() and draw it onto a new page with margins — both examples above follow this approach.

The content gets cut off when decreasing the margins

Cause: The template is drawn starting at (-trim, -trim) while the page also shrinks by 2 × trim; once the amount trimmed exceeds the blank space already present on that side of the original document, the content crosses the page boundary and is clipped.

Solution: Set trim to no more than the blank space already present on that side. When the original document sits flush against the edge, widen it first with the increase approach, then consider narrowing it.

Why does the new page size have to include the margins

Cause: PdfMargins is the distance by which the canvas origin is inset; if the page size does not grow along with it, the content area ends up smaller than the original content and falls outside the page.

Solution: Compute the new page size as "original page size + the left and right (or top and bottom) margin sum" (when increasing), or "original page size − the left and right (or top and bottom) sum" (when decreasing), so the content area exactly fits the original content:

const newSize = new pdfModule.SizeF({
  width: firstPage.Size.Width + marginsToAdd.Left + marginsToAdd.Right,
  height: firstPage.Size.Height + marginsToAdd.Top + marginsToAdd.Bottom,
});

Get a Free License

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

Once a PDF reaches the review stage it becomes a file that gets passed back and forth: one page needs its key points marked, another needs a note on what to change, and the final copy needs an "Approved" stamp. Adding these marks by hand in a reader one at a time does not scale once there are many documents, and the next revision means doing it all over again. Build the marks into the generation flow and annotations come out together with the document.

Spire.PDF for JavaScript loads, modifies, and saves PDF documents in the browser with WebAssembly. Annotations are generated locally and files are read and written through a virtual file system (VFS), with no backend involved. This article uses PdfPage.Annotations to carry eight kinds of annotations: text markup, free text, popup, stamps, lines and polygons, plus web links, file links, and in-document jump links.

This article covers eight core features:

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


Add a Text Markup Annotation

PdfTextMarkupAnnotation overlays a mark on existing text without changing the text itself. Set TextMarkupAnnotationType to Highlight and pick a color with TextMarkupColor to get the most common effect, a highlight that leaves the body text color untouched. The marked area is bounded by four corner points (QuadPoints) in absolute page coordinates, so clear the page margins first to make the canvas and the corner points line up. title and text are written into the annotation as its author and note; they are not drawn on the page and show up in the reader's comment panel.

function App() {
  const addTextMarkupAnnotation = 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;
    }

    // The sample text on the page needs a font; load it into the VFS first
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    const doc = new pdfModule.PdfDocument();

    // Zero the page margins so the canvas coordinates line up with the annotation's corner points
    const margins = new pdfModule.PdfMargins();
    margins.Top = 0;
    margins.Bottom = 0;
    margins.Left = 0;
    margins.Right = 0;
    doc.PageSettings.Margins = margins;

    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({
      fontFamily: 'Arial Unicode MS',
      size: 14,
      style: pdfModule.PdfFontStyle.Regular,
      unicode: true,
    });
    const brush = pdfModule.PdfBrushes.get_Black();

    const text = 'Highlight markup';

    // Draw the target text on the page first
    page.Canvas.DrawString({ s: text, font, brush, x: 80, y: 120 });

    // Measure the text so the markup covers it exactly
    const size = font.MeasureString({ text });
    const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });

    const annotation = new pdfModule.PdfTextMarkupAnnotation({ title: 'Proofread', text: 'Needs review', rect, font });

    // Marked area: the four corner points of the text rectangle
    annotation.QuadPoints = [
      new pdfModule.PointF(rect.Left, rect.Top),
      new pdfModule.PointF(rect.Right, rect.Top),
      new pdfModule.PointF(rect.Left, rect.Bottom),
      new pdfModule.PointF(rect.Right, rect.Bottom),
    ];
    annotation.TextMarkupAnnotationType = pdfModule.PdfTextMarkupAnnotationType.Highlight;
    annotation.TextMarkupColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_LightSkyBlue() });

    page.Annotations.Add(annotation);

    const outputFileName = 'TextMarkupAnnotation.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 a Text Markup Annotation</h1>
      <button onClick={addTextMarkupAnnotation}>Add Annotation</button>
    </div>
  );
}

export default App;

A piece of text on the page covered by a highlight markup:

A piece of text on the page covered by a highlight markup


Add a Free Text Annotation

PdfFreeTextAnnotation draws a block of text directly on the page as a text box, which is common for notes, comments, or fill-in fields. The box position and size come from the rectangle passed to the constructor, and the font, border, background, and opacity can all be set afterward.

function App() {
  const addFreeTextAnnotation = 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;
    }

    // The text box needs a font; load it into the VFS first
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: 220, height: 60 });

    // The rectangle is passed to the constructor, so it decides the annotation's position and size
    const annotation = new pdfModule.PdfFreeTextAnnotation(rect);
    annotation.Text = 'This is a free text annotation';
    annotation.Font = new pdfModule.PdfTrueTypeFont({
      fontFamily: 'Arial Unicode MS',
      size: 14,
      style: pdfModule.PdfFontStyle.Regular,
      unicode: true,
    });
    annotation.Border = new pdfModule.PdfAnnotationBorder({ borderWidth: 1 });
    annotation.BorderColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Gray() });
    annotation.Color = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_LightGoldenrodYellow() });
    annotation.Opacity = 0.8;

    page.Annotations.Add(annotation);

    const outputFileName = 'FreeTextAnnotation.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 a Free Text Annotation</h1>
      <button onClick={addFreeTextAnnotation}>Add Annotation</button>
    </div>
  );
}

export default App;

A block of free text with a border and a background on the page:

A block of free text with a border and a background on the page


Add a Popup Annotation

PdfPopupAnnotation places an expandable popup window that leaves only an icon when collapsed, which suits a longer note that should not take up page space. The icon style is set with Icon, whose values include Note, Comment, Help, Insert, Key, NewParagraph, and Paragraph.

function App() {
  const addPopupAnnotation = 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;
    }

    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    // A collapsed popup annotation is just an icon, so a zero-size rectangle marks its spot
    const rect = new pdfModule.RectangleF({ x: 120, y: 160, width: 0, height: 0 });

    const annotation = new pdfModule.PdfPopupAnnotation({ rectangle: rect, text: 'This is the popup annotation content' });
    annotation.Icon = pdfModule.PdfPopupIcon.Note;
    annotation.Open = true;
    annotation.Color = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Yellow() });

    page.Annotations.Add(annotation);

    const outputFileName = 'PopupAnnotation.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 a Popup Annotation</h1>
      <button onClick={addPopupAnnotation}>Add Annotation</button>
    </div>
  );
}

export default App;

A popup annotation on the page, with its content expanded next to the icon:

A popup annotation on the page, with its content expanded next to the icon


Add a Stamp Annotation

PdfRubberStampAnnotation is used to place a stamp. It ships with a set of ready-made icons, so setting Icon produces a stamped frame with text and no need to draw the appearance yourself. The available icons include Approved, Confidential, Draft, Final, Expired, Sold, TopSecret, NotForPublicRelease, and a dozen or so more.

function App() {
  const addStampAnnotations = 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;
    }

    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    // Place three common stamps with built-in icons
    const stamps = [
      { icon: pdfModule.PdfRubberStampAnnotationIcon.Approved, y: 200, color: pdfModule.Color.get_LightGreen() },
      { icon: pdfModule.PdfRubberStampAnnotationIcon.Confidential, y: 300, color: pdfModule.Color.get_LightPink() },
      { icon: pdfModule.PdfRubberStampAnnotationIcon.Draft, y: 400, color: pdfModule.Color.get_LightBlue() },
    ];

    for (const stamp of stamps) {
      const rect = new pdfModule.RectangleF({ x: 100, y: stamp.y, width: 150, height: 60 });

      const annotation = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect, text: 'Stamp annotation' });
      annotation.Icon = stamp.icon;
      annotation.Color = new pdfModule.PdfRGBColor({ color: stamp.color });

      page.Annotations.Add(annotation);
    }

    const outputFileName = 'StampAnnotation.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 a Stamp Annotation</h1>
      <button onClick={addStampAnnotations}>Add Stamp</button>
    </div>
  );
}

export default App;

Three built-in icon stamps:

Three built-in icon stamps


Add a Shape Annotation

Shape annotations attach geometric figures to the page as independent objects, unlike drawing directly on the canvas: they carry their own properties and notes, and readers can recognize and select them individually. The three classes cover lines, polygons, and polylines:

Class Description
PdfLineAnnotation A straight line, with configurable line endings and arrows
PdfPolygonAnnotation A closed polygon bounded by several vertices
PdfPolyLineAnnotation An open polyline connected by several vertices

A polygon regenerates its appearance before it is added to the page, so set Author and Subject first; otherwise Add fails on a null reference.

function App() {
  const addShapeAnnotations = 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;
    }

    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    // Line: given a start and an end point, with arrows at both ends
    const line = new pdfModule.PdfLineAnnotation({ linePoints: [80, 120, 300, 200], text: 'This is a line annotation' });
    line.lineBorder.BorderStyle = pdfModule.PdfLineBorderStyle.Solid;
    line.lineBorder.BorderWidth = 2;
    line.BeginLineStyle = pdfModule.PdfLineEndingStyle.Circle;
    line.EndLineStyle = pdfModule.PdfLineEndingStyle.ClosedArrow;
    line.BackColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() });
    page.Annotations.Add(line);

    // Polygon: vertices joined head to tail into a closed region
    const polygonPoints = [
      new pdfModule.PointF(80, 300),
      new pdfModule.PointF(160, 260),
      new pdfModule.PointF(240, 300),
      new pdfModule.PointF(200, 380),
      new pdfModule.PointF(120, 380),
      new pdfModule.PointF(80, 300),
    ];
    const polygon = new pdfModule.PdfPolygonAnnotation(page, polygonPoints);
    polygon.Color = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_PaleVioletRed() });
    polygon.Text = 'This is a polygon annotation';
    polygon.Author = 'E-ICEBLUE';
    polygon.Subject = 'polygon annotation demo';
    polygon.BorderEffect = pdfModule.PdfBorderEffect.BigCloud;
    polygon.ModifiedDate = new Date();
    page.Annotations.Add(polygon);

    // Polyline: vertices connected in order, not closed
    const polylinePoints = [
      new pdfModule.PointF(320, 300),
      new pdfModule.PointF(400, 260),
      new pdfModule.PointF(480, 340),
      new pdfModule.PointF(540, 300),
    ];
    const polyline = new pdfModule.PdfPolyLineAnnotation(page, polylinePoints);
    polyline.Color = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Green() });
    polyline.Text = 'This is a polyline annotation';
    polyline.Border = new pdfModule.PdfAnnotationBorder({ borderWidth: 2 });
    page.Annotations.Add(polyline);

    const outputFileName = 'ShapeAnnotation.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 a Shape Annotation</h1>
      <button onClick={addShapeAnnotations}>Add Shape</button>
    </div>
  );
}

export default App;

Line, polygon, and polyline annotations on the same page:

Line, polygon, and polyline annotations on the same page


Add a Web Link Annotation

PdfUriAnnotation turns a rectangular area into a link to a URL; when clicked, the reader hands it to the browser to open.

function App() {
  const addWebLinkAnnotation = 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;
    }

    // The page needs a font; load it into the VFS first
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFamily: 'Arial Unicode MS', size: 14, style: pdfModule.PdfFontStyle.Regular, unicode: true });
    const text = 'Visit the Spire.PDF product page';

    // A link area is invisible, so draw some text first as the clickable entry
    page.Canvas.DrawString({ s: text, font, brush: pdfModule.PdfBrushes.get_DodgerBlue(), x: 80, y: 120 });
    const size = font.MeasureString({ text });

    const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
    const link = new pdfModule.PdfUriAnnotation({ rectangle: rect });
    link.Uri = 'https://www.e-iceblue.com/Introduce/Spire-PDF-Javascript.html';

    page.Annotations.Add(link);

    const outputFileName = 'WebLinkAnnotation.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 a Web Link Annotation</h1>
      <button onClick={addWebLinkAnnotation}>Add Link</button>
    </div>
  );
}

export default App;

A web link annotation pointing to the product page, with the link area covering the text:

A web link annotation pointing to the product page, with the link area covering the text


Add a File Link Annotation

PdfFileLinkAnnotation turns an area into a file link; when clicked, the reader opens the specified file through its system association.

function App() {
  const addFileLinkAnnotation = 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;
    }

    // The page needs a font; load it into the VFS first
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFamily: 'Arial Unicode MS', size: 14, style: pdfModule.PdfFontStyle.Regular, unicode: true });
    const text = 'Open Notepad';

    // Draw some text first as the clickable entry for the file link
    page.Canvas.DrawString({ s: text, font, brush: pdfModule.PdfBrushes.get_DodgerBlue(), x: 80, y: 120 });
    const size = font.MeasureString({ text });

    const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
    const link = new pdfModule.PdfFileLinkAnnotation(rect, 'C:\\Windows\\System32\\notepad.exe');

    page.Annotations.Add(link);

    const outputFileName = 'FileLinkAnnotation.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 a File Link Annotation</h1>
      <button onClick={addFileLinkAnnotation}>Add Link</button>
    </div>
  );
}

export default App;

A file link annotation that opens the specified file through its system association when clicked:

A file link annotation that opens the specified file through its system association when clicked


Add a Document Link Annotation

PdfDocumentLinkAnnotation handles in-document jumps: when clicked, it moves to the page and position given by a PdfDestination.

function App() {
  const addDocumentLinkAnnotation = 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;
    }

    // The page needs a font; load it into the VFS first
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    // Append a second page as the jump target
    const target = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFamily: 'Arial Unicode MS', size: 14, style: pdfModule.PdfFontStyle.Regular, unicode: true });
    target.Canvas.DrawString({ s: 'This is the destination page', font, brush: pdfModule.PdfBrushes.get_Black(), x: 80, y: 120 });

    const text = 'Go to page 2';
    page.Canvas.DrawString({ s: text, font, brush: pdfModule.PdfBrushes.get_DodgerBlue(), x: 80, y: 120 });
    const size = font.MeasureString({ text });

    // Destination: 80 points down from the top of page 2, at 100% zoom
    const destination = new pdfModule.PdfDestination({ page: target });
    destination.Mode = pdfModule.PdfDestinationMode.Location;
    destination.Location = new pdfModule.PointF(0, 80);
    destination.Zoom = 1;

    const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
    const link = new pdfModule.PdfDocumentLinkAnnotation({ rectangle: rect, destination });

    page.Annotations.Add(link);

    const outputFileName = 'DocumentLinkAnnotation.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 a Document Link Annotation</h1>
      <button onClick={addDocumentLinkAnnotation}>Add Link</button>
    </div>
  );
}

export default App;

A link on page 1 pointing to a position on page 2:

A link on page 1 pointing to a position on page 2


FAQ

Text on the page renders as boxes or blank

Cause: The body text and the annotation text both need a Unicode TrueType font. If the font is not loaded into the VFS, or PdfTrueTypeFont is used without unicode: true, the glyphs cannot be resolved and the saved file shows boxes or nothing at all.

Solution: Load the font file into /Library/Fonts/ in the VFS first, then reference the same font name with unicode enabled:

// Load the font into the VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

// Use the same font name and enable unicode
const font = new pdfModule.PdfTrueTypeFont({ fontFamily: 'Arial Unicode MS', size: 14, style: pdfModule.PdfFontStyle.Regular, unicode: true });

Link annotations leave no visible trace on the page

Cause: Link annotations only define a clickable area and an action; they draw no appearance of their own. Most readers do not draw a border around links, so the page looks unchanged after saving — which does not mean the annotation was not written.

Solution: To make it visible, draw text or a shape at the same rectangle and lay the link on top:

// Draw visible text first, then build the link from a rectangle of the same size
page.Canvas.DrawString({ s: text, font, brush: pdfModule.PdfBrushes.get_DodgerBlue(), x: 80, y: 120 });
const size = font.MeasureString({ text });
const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
const link = new pdfModule.PdfUriAnnotation({ rectangle: rect });
link.Uri = 'https://www.e-iceblue.com/';

The text markup annotation does not cover the text

Cause: The marked area is decided by QuadPoints, which uses absolute page coordinates and does not follow the text and align itself, while the canvas origin is pushed aside by the page margins. When the two are out of step, the mark lands beside the text or over empty space.

Solution: Zero the page margins first so the canvas and the corner points share one coordinate system, then measure the text with MeasureString and derive the four corner points from it:

const margins = new pdfModule.PdfMargins();
margins.Top = 0; margins.Bottom = 0; margins.Left = 0; margins.Right = 0;
doc.PageSettings.Margins = margins;

const size = font.MeasureString({ text: 'Highlight markup' });
const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
annotation.QuadPoints = [
  new pdfModule.PointF(rect.Left, rect.Top),
  new pdfModule.PointF(rect.Right, rect.Top),
  new pdfModule.PointF(rect.Left, rect.Bottom),
  new pdfModule.PointF(rect.Right, rect.Bottom),
];

Get a Free License

If you want to remove the evaluation message from the resulting documents, or to get rid of the functional limitations, please contact sales for a temporary license valid for 30 days.

Page 3 of 19
page 3