Knowledgebase (2597)
Children categories
Delete Excel Formulas but Keep Values in React with JavaScript
2026-10-10 01:21:45 Written by liu taliaOnce a report is final, the formulas inside it turn from helper into liability: a reference to another sheet, to another file, or to a row that has since been deleted, and any one of those changes can turn a whole column of numbers into #REF!. When the file is archived or sent outside the team, the safe move is to replace each formula with the result it has just calculated — the same numbers, no longer depending on anything else. The detail sheet, on the other hand, may still need its formulas in order to be maintained, so it cannot be stripped along with the rest. What really has to be decided is the scope: the whole workbook, or one range inside it. ClearContent in Spire.XLS for JavaScript clears the cell's contents and leaves its formatting alone, so writing the result back keeps the thousands separator in an amount and the display style of a date exactly as they were. Spire.XLS for JavaScript does all of 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.
Delete the formulas of the whole workbook
When a report is archived, sent out, or handed to someone who has no business seeing the logic behind it, the formulas are the liability: cross-sheet references, external links, rows deleted afterwards — any one of those changes can make the numbers go dead. Replacing each formula with the result it has just calculated turns the file into a snapshot that depends on nothing else and no longer changes: numbers, formatting and totals all still there, only each cell has been fixed in place. A whole-book delivery is the most common case, so the first approach walks every worksheet in the workbook. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFile. - Walk every worksheet using
workbook.Worksheets.Count. - Walk the cells of that worksheet with
sheet.Range.Cells, which covers the used range of the sheet. - Test whether the cell holds a formula with
cell.HasFormula. - If it does, take the result it has calculated with
cell.FormulaValue. - Clear the formula with
cell.Clear(xlsModule.ExcelClearOptions.ClearContent)— contents only, the formatting stays where it is. - Write the result taken in the previous step back into the cell with
cell.Value2. - Save the workbook with
workbook.SaveToFile.
The complete code example below shows how to delete the formulas of a whole workbook in React:
function App() {
const removeAllFormulas = 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 = 'RemoveFormulas.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// load the workbook
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// walk every worksheet and every cell looking for formulas
for (let i = 0; i < workbook.Worksheets.Count; i++) {
const sheet = workbook.Worksheets.get(i);
for (const cell of sheet.Range.Cells) {
// only a formula cell needs work: take the result it has calculated, clear the cell, then write the result back
if (cell.HasFormula) {
const value = cell.FormulaValue;
cell.Clear(xlsModule.ExcelClearOptions.ClearContent);
cell.Value2 = value;
}
}
}
// save the workbook
const outputFileName = 'RemoveAllFormulas.xlsx';
workbook.SaveToFile({ fileName: outputFileName });
// 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>Delete Formulas but Keep the Values in Excel</h1>
<button id="remove-all-formulas" onClick={removeAllFormulas}>Remove the formulas of the whole workbook</button>
</div>
);
}
export default App;
After running, deleting the formulas of a whole workbook and keeping the values:

Delete the formulas of one range
Within a single worksheet, some cells will never change again and are best frozen into values, while others have to follow whatever else moves and must keep their formulas. Narrowing the scope down to a range lets both needs live in the same sheet, and the formulas outside the range are untouched. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFile. - Take the first worksheet with
workbook.Worksheets.get(0). - Mark out the stretch to process with
sheet.Range.get('D3:D10'). - Walk the cells inside the range with
range.Cells; cells outside it never enter the loop. - Test whether the cell holds a formula with
cell.HasFormula. - If it does, take the result it has calculated with
cell.FormulaValue. - Clear the formula with
cell.Clear(xlsModule.ExcelClearOptions.ClearContent). - Write the result back into the cell with
cell.Value2. - Save the workbook with
workbook.SaveToFile.
The complete code example below shows how to delete the formulas of one range in React:
function App() {
const removeFormulasInRange = 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 = 'RemoveFormulas.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 narrow the range down to the amount column
const sheet = workbook.Worksheets.get(0);
const range = sheet.Range.get('D3:D10');
for (const cell of range.Cells) {
if (cell.HasFormula) {
const value = cell.FormulaValue;
cell.Clear(xlsModule.ExcelClearOptions.ClearContent);
cell.Value2 = value;
}
}
// save the workbook
const outputFileName = 'RemoveFormulasInRange.xlsx';
workbook.SaveToFile({ fileName: outputFileName });
// 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>Delete Formulas but Keep the Values in Excel</h1>
<button id="remove-formulas-in-range" onClick={removeFormulasInRange}>Remove the formulas of one range</button>
</div>
);
}
export default App;
After running, deleting the formulas of one range:

FAQ
Does clearing a formula also wipe the cell's number format and fill?
Cause: Clearing a cell goes through Clear, and what it clears is decided by the ExcelClearOptions passed in. ClearAll or ClearFormat wipes the formatting along with everything else; ClearContent clears the contents only.
Solution: Pass ClearContent. The number format, the font and the fill all stay where they were, and the value written back is still shown the way it was — an amount keeps its thousands separator and two decimals, and a date still reads 2026-03-31 rather than the serial number 46112.
Why does cell.Value not give me the result a formula calculated?
Cause: While a cell still carries a formula, cell.Value returns the formula itself rather than its result.
Solution: Read the result with FormulaValue, or with FormulaNumberValue when only the number is wanted. Once the formula has been cleared and replaced by a value, Value returns that number.
// a cell that still has its formula: Value is the formula text, the result is in
// FormulaValue / FormulaNumberValue
const formula = cell.Value; // "=B5*C5"
const result = cell.FormulaNumberValue; // 97280
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.
Create a Custom (Combination) Chart in React with JavaScript
2026-10-10 01:21:27 Written by liu taliaDrawing a single chart type per figure is Excel's default behaviour, not a rule anyone has to keep. A report usually has more than one thing to say, and each reading has a shape that suits it: height suits columns, movement suits a line, a running total suits an area. A custom (combination) chart exists for exactly this: within one chart, each series can be given a chart type of its own, and which types are put together is decided by the questions the figure has to answer side by side -- there is no fixed pairing. Columns with a line is the pairing most widely used in practice, because it accommodates both readings at once. The example below takes that pair, drawing sales as columns and the target as a line, so that what the reader compares is the line's position against the column tops. Two separate charts would need their category axes aligned first, before any comparison could be made; one chart, with a category axis the series share, keeps the months strictly aligned. A report also often holds a series that cannot be brought into the main data range, and such a series has to be added by hand. Spire.XLS for JavaScript performs all of this in the browser on top of WebAssembly, managing input and output files through a virtual file system (VFS), with no backend service required.
This article covers two key features:
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.
Combine a column chart with a line chart
A pairing is not fixed: which types share one figure depends on which questions have to be answered side by side, and columns with a line is only the most widely used of them. In this example sales are best expressed as columns, where heights are compared at a glance, and the target is best expressed as a line, where what the reader compares is its position against the column tops. Once the two series have a type each, one figure carries both readings, and the category axis they share keeps the months strictly aligned, so no comparison across two figures is needed. Substituting clustered bars with a line, or columns with an area, works the same way. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFileand take the first worksheet. - Add a chart with
sheet.Charts.Add()and pointchart.DataRangeatA1:C7: the first row supplies the series names, the first column the category labels, and the next two columns become one series each. - Set
chart.SeriesDataFromRangetofalseso that each series keeps the values bound to it. - Place the chart on the sheet with
chart.LeftColumn,chart.TopRow,chart.RightColumnandchart.BottomRow. - Give
chart.Series.get(0).SerieTypeandchart.Series.get(1).SerieTypethe valuesExcelChartType.ColumnClusteredandExcelChartType.Line. Every series names its own type this way, so which types go together is yours to choose. - Set the title with
chart.ChartTitle, then save the workbook withworkbook.SaveToFile.
The complete code example below shows how to combine a column chart with a line chart in React:
function App() {
const combineColumnAndLine = 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 = 'SalesReport.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
const sheet = workbook.Worksheets.get(0);
// add a chart and bind its data range to A1:C7: the first column becomes the category labels, the next two become one series each
const chart = sheet.Charts.Add();
chart.DataRange = sheet.Range.get('A1:C7');
chart.SeriesDataFromRange = false;
// place the chart
chart.LeftColumn = 1;
chart.TopRow = 10;
chart.RightColumn = 9;
chart.BottomRow = 26;
// give the two series a chart type each: sales drawn as columns, target sales drawn as a line
chart.Series.get(0).SerieType = xlsModule.ExcelChartType.ColumnClustered;
chart.Series.get(1).SerieType = xlsModule.ExcelChartType.Line;
chart.ChartTitle = 'Sales and Target Sales';
// save the workbook
const outputFileName = 'CombineColumnAndLine.xlsx';
workbook.SaveToFile({ fileName: outputFileName });
// 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>Create a Custom (Combination) Chart</h1>
<button id="combine-column-and-line" onClick={combineColumnAndLine}>Combine a column chart with a line chart</button>
</div>
);
}
export default App;
After running, the effect of combining a column chart with a line chart:
Add a series by hand
DataRange can only ever be one continuous rectangle, and that is a hard limit on how data gets bound. Reports commonly present two such cases: the main data occupies columns A to C while the reference values start a column further along, with an empty column in between; or the values to be plotted are derived by calculation and occupy no cell at all. Neither case can be bound in a single call, so the chart has to be built series by series instead. A hand-added series does not depend on DataRange: its range can point anywhere, and it still takes a chart type of its own, so the rules of a combination chart apply to it in full. Here last year's sales are added as a second column series, paired alongside the current year, with the target line running across both. The steps are:
- Load the font and the test data file into the VFS, load the workbook and take the first worksheet.
- Add a chart with
sheet.Charts.Add()and pointchart.DataRangeat the main block alone,A1:C7. - Give the two series of that block
ExcelChartType.ColumnClusteredandExcelChartType.Line. - Add a series with
chart.Series.Add({ name, serieType }), which takes the series name and its chart type in one call. - Bind the category labels and the values with
series.CategoryLabelsandseries.Values, each pointing at the range the data actually occupies. - Set the title with
chart.ChartTitle, then save the workbook withworkbook.SaveToFile.
The complete code example below shows how to add a series by hand in React:
function App() {
const addSeriesManually = 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 = 'SalesReport.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
const sheet = workbook.Worksheets.get(0);
// add a chart and bind its data range to the main block, A1:C7
const chart = sheet.Charts.Add();
chart.DataRange = sheet.Range.get('A1:C7');
chart.SeriesDataFromRange = false;
// place the chart
chart.LeftColumn = 1;
chart.TopRow = 10;
chart.RightColumn = 9;
chart.BottomRow = 26;
// the two series of the main block: sales as columns, target sales as a line
chart.Series.get(0).SerieType = xlsModule.ExcelChartType.ColumnClustered;
chart.Series.get(1).SerieType = xlsModule.ExcelChartType.Line;
// last year's figures sit in column E, one empty column away from the main block, so they cannot come along with the data range; add a series by hand instead.
// a hand-added series takes a chart type of its own too; here it is drawn as columns alongside sales
const lastYear = chart.Series.Add({ name: 'Last Year Sales (K)', serieType: xlsModule.ExcelChartType.ColumnClustered });
lastYear.CategoryLabels = sheet.Range.get('A2:A7');
lastYear.Values = sheet.Range.get('E2:E7');
chart.ChartTitle = 'Sales, Target Sales and Last Year Sales';
// save the workbook
const outputFileName = 'AddSeriesManually.xlsx';
workbook.SaveToFile({ fileName: outputFileName });
// 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>Create a Custom (Combination) Chart</h1>
<button id="add-series-manually" onClick={addSeriesManually}>Add a series by hand</button>
</div>
);
}
export default App;
After running, the effect of adding a series by hand:
FAQ
Can a hand-added series be used alongside DataRange?
Cause: Yes, and the two do not interfere. DataRange produces only the series that correspond to the columns of the main block; a hand-added series carries its own Values and CategoryLabels, which are unrelated to the range DataRange covers and need not lie within the same rectangle. In the second example above the chart holds both series of the main block and the hand-added last-year series at once.
Solution: Point DataRange at the main block, then add each extra series with chart.Series.Add({ name, serieType }). Note that a hand-added series does not inherit the category labels of the main block: CategoryLabels has to be assigned explicitly, and if it is pointed at the same column the series share one category axis.
// the two series of the main block come from DataRange; the hand-added one is bound separately
chart.DataRange = sheet.Range.get('A1:C7');
const lastYear = chart.Series.Add({ name: 'Last Year Sales (K)', serieType: xlsModule.ExcelChartType.ColumnClustered });
lastYear.CategoryLabels = sheet.Range.get('A2:A7');
lastYear.Values = sheet.Range.get('E2:E7');
What type does a series added without serieType get?
Cause: It takes the chart's own type. sheet.Charts.Add() is called with no arguments in the example, and the chart it builds defaults to ExcelChartType.ColumnClustered, which is the type a new series keeps as well.
Solution: For a new series to join a combination chart, name its type at the call, or change SerieType after adding it:
// named at the call
const serie = chart.Series.Add({ name: 'Last Year Sales (K)', serieType: xlsModule.ExcelChartType.ColumnClustered });
// or changed after adding
serie.SerieType = xlsModule.ExcelChartType.Line;
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.
Once a contract or approval form is finalized, it often needs an "Approved" stamp or a timestamp in the corner to record who confirmed it and when. Doing this in desktop software means stamping each file by hand, which gets slow as the pile grows; sending the document to a server to process means the file leaves the local machine.
Spire.PDF for JavaScript loads, modifies, and saves PDF documents in the browser with WebAssembly. Stamps are generated locally and files are read and written through a virtual file system (VFS), with no backend involved. The library treats a stamp as a type of annotation: PdfRubberStampAnnotation sets the stamp's position and size on the page, while its appearance comes from a PdfTemplate assigned through PdfAppearance — draw an image on the template for an image stamp, or text for a text stamp.
This article covers three core features:
For installation and project setup, see How to Integrate Spire.PDF for JavaScript in a React Project. The examples below assume Spire.PDF is installed and the WebAssembly module is initialized.
Add an Image Stamp
Spire.PDF for JavaScript provides the PdfRubberStampAnnotation class for placing a stamp at a given position on the page. It only defines the stamp's position and size; the appearance is set through PdfAppearance.
function App() {
const addImageStamp = 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 and the stamp image into the VFS
const inputFileName = 'SampleDocument.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
const stampImageName = 'Seal.png';
await window.spire.FetchFileToVFS(stampImageName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Get the first page
let page = doc.Pages.get_Item(0);
// Load the stamp image
let image = pdfModule.PdfImage.FromFile(stampImageName);
// Scale the image to fit within 120 points
const maxSize = 120;
const scale = Math.min(maxSize / image.Width, maxSize / image.Height, 1);
const stampWidth = image.Width * scale;
const stampHeight = image.Height * scale;
// Create a template the same size as the stamp and fill it with the image
let template = new pdfModule.PdfTemplate({ width: stampWidth, height: stampHeight });
template.Graphics.DrawImage({ image: image, x: 0, y: 0, width: stampWidth, height: stampHeight });
// Frame the stamp in the bottom-right corner; the rectangle matches the template
let rect = new pdfModule.RectangleF({
location: new pdfModule.PointF(page.ActualSize.Width - stampWidth - 40, page.ActualSize.Height - stampHeight - 40),
size: new pdfModule.SizeF({ width: stampWidth, height: stampHeight })
});
// Create the stamp annotation and set the template as its appearance
let stamp = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect });
let appearance = new pdfModule.PdfAppearance(stamp);
appearance.Normal = template;
stamp.Appearance = appearance;
// Add the stamp annotation to the page's annotation collection
page.AnnotationsWidget.Add(stamp);
// Save the document
const outputFileName = 'ImageStamp.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 an Image Stamp</h1>
<button onClick={addImageStamp}>
Add Stamp
</button>
</div>
);
}
export default App;
A PDF document with an image stamp in the bottom-right corner:

Add a Text Stamp
Spire.PDF for JavaScript also provides PdfPath, PdfPen, and PdfTrueTypeFont for drawing text as a stamp. PdfPath builds a rounded rectangle from four arcs, DrawPath strokes it into a border, and DrawString writes the text inside — a text stamp with a border.
function App() {
const addTextStamp = 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 a font into the VFS font directory
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Load the PDF into the VFS
const inputFileName = 'SampleDocument.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Get the first page
let page = doc.Pages.get_Item(0);
// Create the template that holds the stamp
let template = new pdfModule.PdfTemplate({ width: 125, height: 55 });
// Build the font from the file in the VFS and prepare the brush
let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 12, style: pdfModule.PdfFontStyle.Bold });
let brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_DarkRed() }) });
let pen = new pdfModule.PdfPen({ brush: brush });
// Build a rounded-rectangle path from four arcs and a line
const radius = 20;
let path = new pdfModule.PdfPath();
path.AddArc({ x: 0, y: 0, width: radius, height: radius, startAngle: 180, sweepAngle: 90 });
path.AddArc({ x: template.Width - radius, y: 0, width: radius, height: radius, startAngle: 270, sweepAngle: 90 });
path.AddArc({ x: template.Width - radius, y: template.Height - radius, width: radius, height: radius, startAngle: 0, sweepAngle: 90 });
path.AddArc({ x: 0, y: template.Height - radius, width: radius, height: radius, startAngle: 90, sweepAngle: 90 });
path.AddLine({ x1: 0, y1: template.Height - radius, x2: 0, y2: radius / 2 });
// Stroke the path into the stamp border
template.Graphics.DrawPath({ pen: pen, path: path });
// Write two lines of text inside the border
template.Graphics.DrawString({ s: 'APPROVED', font: font, brush: brush, point: new pdfModule.PointF(28, 10) });
template.Graphics.DrawString({ s: 'Marketing Dept.', font: font, brush: brush, point: new pdfModule.PointF(10, 32) });
// Frame the stamp in the bottom-right corner; the rectangle matches the template
let rect = new pdfModule.RectangleF({
location: new pdfModule.PointF(page.ActualSize.Width - template.Width - 40, page.ActualSize.Height - template.Height - 40),
size: template.Size
});
// Create the stamp annotation and set the template as its appearance
let stamp = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect });
let appearance = new pdfModule.PdfAppearance(stamp);
appearance.Normal = template;
stamp.Appearance = appearance;
// Add the stamp annotation to the page's annotation collection
page.AnnotationsWidget.Add(stamp);
// Save the document
const outputFileName = 'TextStamp.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 Stamp</h1>
<button onClick={addTextStamp}>
Add Stamp
</button>
</div>
);
}
export default App;
A text stamp with a rounded border and the word APPROVED:

Add a Date and Time Stamp
Spire.PDF for JavaScript also provides PdfTemplate for rendering arbitrary content as a stamp appearance. The code below draws a single line of the current time on the template: the string is generated with the browser's native Date, and since DrawString draws any plain string, the stamp content changes with the moment it is generated.
function App() {
const addDateTimeStamp = 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 a font into the VFS font directory
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Load the PDF into the VFS
const inputFileName = 'SampleDocument.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Get the first page
let page = doc.Pages.get_Item(0);
// Get the current date and time
const now = new Date();
const pad = (value) => String(value).padStart(2, '0');
let timeString = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
// Create a template sized for the text
let template = new pdfModule.PdfTemplate({ width: 150, height: 16 });
// Prepare the font and brush
let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 12 });
let brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }) });
// Draw the time text onto the template
template.Graphics.DrawString({ s: timeString, font: font, brush: brush, point: new pdfModule.PointF(0, 0) });
// Frame the stamp in the bottom-right corner; the rectangle matches the template
let rect = new pdfModule.RectangleF({
location: new pdfModule.PointF(page.ActualSize.Width - template.Width - 20, page.ActualSize.Height - template.Height - 20),
size: template.Size
});
// Create the stamp annotation and set the template as its appearance
let stamp = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect });
let appearance = new pdfModule.PdfAppearance(stamp);
appearance.Normal = template;
stamp.Appearance = appearance;
// Add the stamp annotation to the page's annotation collection
page.AnnotationsWidget.Add(stamp);
// Save the document
const outputFileName = 'DateTimeStamp.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 Date and Time Stamp</h1>
<button onClick={addDateTimeStamp}>
Add Stamp
</button>
</div>
);
}
export default App;
A stamp in the page corner recording the current date and time:

FAQ
The stamp is too large and its position is off
Cause: A stamp's position and size on the page come from the rectangle of PdfRubberStampAnnotation, and the appearance fills that rectangle. When the template size and the rectangle size differ, the appearance is stretched; when the rectangle uses the image's pixel dimensions without scaling, the stamp can cover the body text or run off the page.
Solution: Work out the target display size first, give the template and the rectangle the same width and height, then position them relative to the page size:
// Give the template and the rectangle the same width and height
let template = new pdfModule.PdfTemplate({ width: stampWidth, height: stampHeight });
let rect = new pdfModule.RectangleF({
location: new pdfModule.PointF(page.ActualSize.Width - stampWidth - 40, page.ActualSize.Height - stampHeight - 40),
size: template.Size
});
Text in the stamp renders as hollow boxes
Cause: PdfTrueTypeFont loads a font file through fontFile. If that file does not contain the glyphs for the text (for example, a Western font used with non-Latin characters), those characters have no glyph to draw and render as a row of boxes.
Solution: Use a font that contains the glyphs, and load the file into the VFS font directory /Library/Fonts/ first:
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 12 });
How to customize the date and time format
Cause: DrawString draws exactly the string you pass in, so the format is decided entirely by the code that builds the string. Methods such as toLocaleString depend on the browser's locale, so the output can differ from one environment to another.
Solution: For a fixed format, build the string from the Date getters:
const now = new Date();
const pad = (value) => String(value).padStart(2, '0');
const timeString = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
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.