Once 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:

  1. Load the font and the test data file into the VFS.
  2. Load the workbook with workbook.LoadFromFile.
  3. Walk every worksheet using workbook.Worksheets.Count.
  4. Walk the cells of that worksheet with sheet.Range.Cells, which covers the used range of the sheet.
  5. Test whether the cell holds a formula with cell.HasFormula.
  6. If it does, take the result it has calculated with cell.FormulaValue.
  7. Clear the formula with cell.Clear(xlsModule.ExcelClearOptions.ClearContent) — contents only, the formatting stays where it is.
  8. Write the result taken in the previous step back into the cell with cell.Value2.
  9. 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 the whole workbook


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:

  1. Load the font and the test data file into the VFS.
  2. Load the workbook with workbook.LoadFromFile.
  3. Take the first worksheet with workbook.Worksheets.get(0).
  4. Mark out the stretch to process with sheet.Range.get('D3:D10').
  5. Walk the cells inside the range with range.Cells; cells outside it never enter the loop.
  6. Test whether the cell holds a formula with cell.HasFormula.
  7. If it does, take the result it has calculated with cell.FormulaValue.
  8. Clear the formula with cell.Clear(xlsModule.ExcelClearOptions.ClearContent).
  9. Write the result back into the cell with cell.Value2.
  10. 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:

Delete 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.

Drawing 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:

  1. Load the font and the test data file into the VFS.
  2. Load the workbook with workbook.LoadFromFile and take the first worksheet.
  3. Add a chart with sheet.Charts.Add() and point chart.DataRange at A1:C7: the first row supplies the series names, the first column the category labels, and the next two columns become one series each.
  4. Set chart.SeriesDataFromRange to false so that each series keeps the values bound to it.
  5. Place the chart on the sheet with chart.LeftColumn, chart.TopRow, chart.RightColumn and chart.BottomRow.
  6. Give chart.Series.get(0).SerieType and chart.Series.get(1).SerieType the values ExcelChartType.ColumnClustered and ExcelChartType.Line. Every series names its own type this way, so which types go together is yours to choose.
  7. Set the title with chart.ChartTitle, then save the workbook with workbook.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:
Combine 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:

  1. Load the font and the test data file into the VFS, load the workbook and take the first worksheet.
  2. Add a chart with sheet.Charts.Add() and point chart.DataRange at the main block alone, A1:C7.
  3. Give the two series of that block ExcelChartType.ColumnClustered and ExcelChartType.Line.
  4. Add a series with chart.Series.Add({ name, serieType }), which takes the series name and its chart type in one call.
  5. Bind the category labels and the values with series.CategoryLabels and series.Values, each pointing at the range the data actually occupies.
  6. Set the title with chart.ChartTitle, then save the workbook with workbook.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:
Add 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:

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:

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:

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.

Page 10 of 371
page 10