JavaScript (261)
Set an Expiry Date for a PDF Document Using JavaScript in React
2026-10-16 01:12:53 Written by Nina TangOnce 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:

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.
Apply Cell Styles to an Excel Cell Range with JavaScript in React
2026-10-14 08:52:48 Written by liu taliaFormatting 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:
- Load the font file into the VFS, create a workbook, take the first worksheet and write the header and two data rows.
- Create a named style with
workbook.Styles.Addand give it the font colour, size and italic the header will use. - Set the top and bottom borders on that same style.
- Set horizontal and vertical centring on that same style.
- 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. - 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:

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:
- Run the code of feature point 1 first, so that its result file
ApplyStyleToRange.xlsxstays in the VFS as this run's input file. - Load that input file with
workbook.LoadFromFileand take the first worksheet. - Read the named style feature point 1 created with
workbook.Styles.get('tableStyle')and take itsFont.Sizeas the baseline. - Raise the baseline size and write it back to the style, as the size the data range will use.
- 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:

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.
Modify and Delete Hyperlinks in PDF with JavaScript in React
2026-10-14 06:20:44 Written by Nina TangPDF 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:

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:

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:
- Load the font file into the VFS with
window.spire.FetchFileToVFS. - Create a workbook with
new Workbook()and take the first worksheet withworkbook.Worksheets.get(0). - 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. - Set the matching format on each row's sample through
FontName,Size,Color,IsBold,IsItalic,UnderlineandIsStrikethroughonRange.Style.Font, taking the font colour fromxlsModule.Color.get_Red()and the underline fromxlsModule.FontUnderlineType.Single. - Set both columns to a width of 25 with
sheet.Columns.get(0).ColumnWidthandsheet.Columns.get(1).ColumnWidth. - Save the workbook with
workbook.SaveToFileand dispose of it withworkbook.Disposeto 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:

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:
- Load the font file into the VFS with
window.spire.FetchFileToVFS. - Create a workbook with
new Workbook()and take the first worksheet withworkbook.Worksheets.get(0). - Create two fonts with
workbook.CreateFont(): one for the whole sentence, and one that emphasises the key phrase throughIsBoldandKnownColor. - Take the cell that holds the sentence with
sheet.Range.get('A1'), and write the whole sentence into its rich text object throughRichText.Text. - Work out the start and end of the text that needs the new font within the sentence with
note.indexOf(key)andkey.length, then assign the fonts to the three parts -- before the key phrase, the key phrase itself and after it -- withRichText.SetFont. - Save the workbook with
workbook.SaveToFileand dispose of it withworkbook.Disposeto 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:

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:
- Load the font, the picture and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFile. - Take the cell the comment will be attached to with
sheet.Range.get('A4'). - Create a comment object on the cell with
range.AddComment(). - Fill the comment background with the picture using
comment.Fill.CustomPicture, passing the picture as a stream built bynew xlsModule.Stream('logo.png')and'None'for thenameargument. - Set
comment.Widthandcomment.Heightso the comment box is square, matching the square picture, which keeps the picture from being stretched. - Place the comment box relative to the top-left corner of the worksheet with
comment.Topandcomment.Left, so that it covers the content below without obscuring its own row. - Set
comment.Visibleto show the comment as soon as the file is opened. - Save the workbook with
workbook.SaveToFile. - Dispose of the workbook with
workbook.Disposeto 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:

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:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFile. - Take the cell the comment will be attached to with
sheet.Range.get('A4')and create the comment withrange.AddComment(). - Write the comment text with
comment.Text. - Create a font with
workbook.CreateFont()and set the whole text to it withcomment.RichText.SetFont. - Set
comment.Fill.FillTypeto the solid fill type,xlsModule.ShapeFillType.SolidColor. - Pick the colour with
comment.Fill.ForeColor. - Set
comment.Visibleto show the comment as soon as the file is opened. - Save the workbook with
workbook.SaveToFile. - Dispose of the workbook with
workbook.Disposeto 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:

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:
PdfTextWebLinkwithText,Url,FontandBrushset;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:

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:

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:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFile. - Add the shapes into the space left for them with
sheet.PrstGeomShapes.AddPrstGeomShape. - Take the shape count from
sheet.PrstGeomShapes.Countand walk every shape on the worksheet with it. - Take each shape's link object from its
HyLinkproperty and write the address into itsAddress. - Save the workbook with
workbook.SaveToFile. - Dispose of the workbook with
workbook.Disposeto 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:

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:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFile. - Take the shape count from
sheet.PrstGeomShapes.Countand walk every shape on the worksheet with it. - Take this shape's link object from its
HyLinkproperty. - Read the target address from the link object's
Address, collecting the addresses as you go. - Write the collected addresses into a text file with
FS.writeFile, one address per line. - Dispose of the workbook with
workbook.Disposeto 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:

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.
Add Cross-Sheet and External File Links with JavaScript in React
2026-10-13 01:09:11 Written by liu taliaExcel 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:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFile. - Create a hyperlink object on the target cell with
sheet.HyperLinks.Add. - Mark the type as a workbook-internal link with the hyperlink's
Typeproperty, set toxlsModule.HyperLinkType.Workbook. - Set the text shown on the cell with the
TextToDisplayproperty. - Write the jump target into the
Addressproperty, in the form "sheet name!cell", for exampleData!B7. - Style the link text blue and underlined so that it reads differently from ordinary text.
- Save the workbook with
workbook.SaveToFile. - Dispose of the workbook with
workbook.Disposeto 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 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:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFile. - Create a hyperlink object on the target cell with
sheet.HyperLinks.Add. - Mark the type as a file link with the
Typeproperty, set toxlsModule.HyperLinkType.File. - Set the text shown on the cell with the
TextToDisplayproperty. - Write the path of the external file into the
Addressproperty, for example../files/Quote.xlsx. - Style the link text blue and underlined so that it reads differently from ordinary text.
- Save the workbook with
workbook.SaveToFile. - Dispose of the workbook with
workbook.Disposeto 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:

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:

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:

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:
- Add a Text Markup Annotation
- Add a Free Text Annotation
- Add a Popup Annotation
- Add a Stamp Annotation
- Add a Shape Annotation
- Add a Web Link Annotation
- Add a File Link Annotation
- Add a Document Link Annotation
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:

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:

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:

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:

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:

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:

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:

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:

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.