Adding text to a PDF usually sits at the end of a generation pipeline: document numbers, review comments, annotations, or a line of pale text laid over a chart. Typing it in by hand is fine for a few pages, but once the text has to follow the data — a number that changes per copy, a note angled into the page corner, pale text sitting on top of a chart — manual layout stops keeping up.

This article shows how to use Spire.PDF for JavaScript to draw text on PDF pages, including text filled with a gradient, text laid out inside a rectangle, and text that is rotated, transformed, or semi-transparent. It runs on WebAssembly to create and save PDF documents directly in the browser, doing all the work locally and reading and writing files through a virtual file system (VFS) with no backend involved.

This article covers four core features:

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


Draw Text with a Color Gradient

The color of text comes from the brush passed to DrawString. Swap in a gradient brush — PdfLinearGradientBrush — and the glyphs transition from one color to another along a given direction: mode sets the direction, and the rect on the brush bounds where the gradient starts and ends.

function App() {
  const drawGradientText = 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 font into the VFS
    await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Create a PDF document and add a blank page
    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 24 });
    const text = 'Gradient Text';

    // Measure this line of text so the gradient spans exactly its width
    const textWidth = font.MeasureString({ text: text }).Width;

    // Horizontal gradient: from red to blue
    const gradient = new pdfModule.PdfLinearGradientBrush({
      rect: new pdfModule.RectangleF({ x: 40, y: 90, width: textWidth, height: 40 }),
      color1: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Red() }),
      color2: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }),
      mode: pdfModule.PdfLinearGradientMode.Horizontal,
    });

    // Align the text anchor with the left edge of the gradient rectangle so both colors sweep the whole line
    page.Canvas.DrawString({ s: text, font: font, brush: gradient, x: 40, y: 110 });

    // Define the output file name and save the document
    const outputFileName = 'GradientText.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>Draw Text with a Color Gradient</h1>
      <button onClick={drawGradientText}>
        Draw
      </button>
    </div>
  );
}

export default App;

When the gradient rectangle matches the text width, red to blue sweeps across the entire line:

With the gradient rectangle matching the text width, red to blue sweeps across the entire line


Draw Text Laid Out Inside a Rectangle

DrawString takes either a pair of coordinates or a layout rectangle, layoutRectangle. With a rectangle, the text wraps to the width of the box on its own, so you don't have to work out where each line breaks. Combined with alignment and lineAlignment from PdfStringFormat, you get more control over how the text is aligned.

function App() {
  const drawTextInRectangle = 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 font into the VFS for the page text
    await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Create a PDF document and add a blank page
    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 14 });
    const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Black() }) });
    const borderPen = new pdfModule.PdfPen({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_LightGray() }), width: 1 });

    const text = 'This is a longer paragraph of explanatory text. Handed to the rectangle, it wraps to the box width on its own.';

    // Left box: left-aligned wrapping by default, text starts at the top-left corner of the box
    const leftBox = new pdfModule.RectangleF({ x: 40, y: 80, width: 200, height: 100 });
    page.Canvas.DrawRectangle({ pen: borderPen, rectangle: leftBox });
    page.Canvas.DrawString({ s: text, font: font, brush: brush, layoutRectangle: leftBox });

    // Right box: the same text, centered horizontally and vertically inside the box
    const rightBox = new pdfModule.RectangleF({ x: 300, y: 80, width: 200, height: 100 });
    page.Canvas.DrawRectangle({ pen: borderPen, rectangle: rightBox });
    const center = new pdfModule.PdfStringFormat({
      alignment: pdfModule.PdfTextAlignment.Center,
      lineAlignment: pdfModule.PdfVerticalAlignment.Middle,
    });
    page.Canvas.DrawString({ s: text, font: font, brush: brush, layoutRectangle: rightBox, format: center });

    // Define the output file name and save the document
    const outputFileName = 'TextInRectangle.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>Draw Text Laid Out Inside a Rectangle</h1>
      <button onClick={drawTextInRectangle}>
        Draw
      </button>
    </div>
  );
}

export default App;

The same paragraph wraps inside a 200-point-wide box, while the right box adds horizontal and vertical centering:

The same paragraph wrapping inside a 200-point-wide box on the left, and centered horizontally and vertically on the right


Draw Rotated and Transformed Text

Rotation and deformation of text come from the canvas rather than from font parameters: you turn the canvas first, then draw on it. Four methods cover the common cases:

API Effect Parameters and units
TranslateTransform(dx, dy) Moves the canvas origin to the target position Offset in points
RotateTransform({ angle }) Rotates around the canvas origin Angle; a positive value is clockwise on this canvas
SkewTransform(angleX, angleY) Skews the axes so text runs along a slanted line Skew angle; with (-20, 0) the right end of the line lifts
ScaleTransform(scaleX, scaleY) Scales the canvas by a factor Scale factors for both axes; (1, 0.6) compresses vertically to 0.6

The four transforms applied to the same sample line of text (gray is before the transform, red is after, and the dot marks the anchor point):

Translation, rotation, skew, and scaling applied to the same sample line of text

function App() {
  const drawTransformedText = 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 font into the VFS for the page text
    await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Create a PDF document and add a blank page
    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 16 });
    const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_SteelBlue() }) });

    // Translation: move only the origin to the anchor point, shifting the text as a whole
    let state = page.Canvas.Save();
    page.Canvas.TranslateTransform(60, 110);
    page.Canvas.DrawString({ s: 'Translated text', font: font, brush: brush, x: 0, y: 0 });
    page.Canvas.Restore({ state: state });

    // Rotation: move the origin to the anchor point, then rotate 30°
    state = page.Canvas.Save();
    page.Canvas.TranslateTransform(120, 210);
    page.Canvas.RotateTransform({ angle: 30 });
    page.Canvas.DrawString({ s: 'Rotated 30° text', font: font, brush: brush, x: 0, y: 0 });
    page.Canvas.Restore({ state: state });

    // Skew: horizontal shear of -20°, lifting the right end of the line
    state = page.Canvas.Save();
    page.Canvas.TranslateTransform(60, 430);
    page.Canvas.SkewTransform(-20, 0);
    page.Canvas.DrawString({ s: 'Horizontally skewed text', font: font, brush: brush, x: 0, y: 0 });
    page.Canvas.Restore({ state: state });

    // Transform: compress vertically to 0.6, squashing the glyphs
    state = page.Canvas.Save();
    page.Canvas.TranslateTransform(60, 560);
    page.Canvas.ScaleTransform(1, 0.6);
    page.Canvas.DrawString({ s: 'Vertically compressed text', font: font, brush: brush, x: 0, y: 0 });
    page.Canvas.Restore({ state: state });

    // Define the output file name and save the document
    const outputFileName = 'TransformText.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>Draw Rotated and Transformed Text</h1>
      <button onClick={drawTransformedText}>
        Transform
      </button>
    </div>
  );
}

export default App;

Text drawn with four canvas transforms: translation, rotation, horizontal skew, and vertical compression:

Text drawn with four canvas transforms: translation, rotation, horizontal skew, and vertical compression


Draw Semi-Transparent Text

SetTransparency is set on the canvas: alphaBrush and alphaPen control how transparent the fill and the stroke are, taking a decimal between 0 and 1 (0 fully transparent, 1 opaque), and blendMode decides how the text composites with what lies underneath. It takes effect for everything drawn from the moment it is set, so wrap it in Save and Restore — otherwise the content that follows fades as well.

function App() {
  const drawTransparentText = 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 font into the VFS for the page text
    await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Create a PDF document and add a blank page
    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 20 });
    const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_SeaGreen() }) });
    const text = 'Spire.PDF Semi-Transparent Text';

    // First line: opaque, as a reference
    page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 90 });

    // Turn on transparency: both the fill and the stroke alpha are set to 0.3
    const state = page.Canvas.Save();
    page.Canvas.SetTransparency({ alphaPen: 0.3, alphaBrush: 0.3, blendMode: pdfModule.PdfBlendMode.Normal });
    page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 140 });

    // Restore the canvas state so drawing outside this block goes back to opaque
    page.Canvas.Restore({ state: state });
    page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 190 });

    // Define the output file name and save the document
    const outputFileName = 'TransparentText.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>Draw Semi-Transparent Text</h1>
      <button onClick={drawTransparentText}>
        Draw
      </button>
    </div>
  );
}

export default App;

The same line drawn three times: opaque, at alpha 0.3, and opaque again after Restore:

The same line drawn three times: opaque, at alpha 0.3, and opaque again after Restore


FAQ

Text lands outside the page, or its vertical position is reversed

Cause: The canvas origin sits at the top-left corner of the page (inset by the page margins), the y axis points down, and the unit is the point. The anchor marks the top-left corner of the line, not the text baseline. Working from the assumption that the origin is at the bottom-left with the y axis pointing up puts the text on the opposite side, or even outside the drawable area — the sample page is A4 with 40-point top and bottom margins, leaving a usable height of 762 points, and a line drawn below roughly y = 740 points is clipped.

Solution: Lay the text out from the top-left corner with y increasing downwards. To measure from the bottom of the page instead, subtract from the usable height:

// Height of the drawable area (762 points for A4 with 40-point margins)
const height = page.Canvas.ClientSize.Height;

// Draw 100 points above the bottom of the drawable area
page.Canvas.DrawString({ s: 'Text near the bottom of the page', font: font, brush: brush, x: 40, y: height - 100 });

Gradient text shows only a single color, or the transition is incomplete

Cause: The rect on a PdfLinearGradientBrush bounds where the gradient starts and ends, in absolute canvas coordinates, independent of the text anchor. When the rectangle does not cover the whole line, the text only lands on one segment of the gradient, which looks like a flat color. In a test with the rectangle starting at x = 0 and the text anchored at x = 40, the left edge of the text was already a third of the way through the gradient, and the red-to-blue transition was no longer complete.

Solution: Use MeasureString to get the text width, then make the rectangle the same width as the text and align its start with the anchor point, so the gradient sweeps across the whole line:

// Use the text width as the width of the gradient rectangle
const textWidth = font.MeasureString({ text: text }).Width;

const gradient = new pdfModule.PdfLinearGradientBrush({
  rect: new pdfModule.RectangleF({ x: 40, y: 90, width: textWidth, height: 40 }),
  color1: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Red() }),
  color2: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }),
  mode: pdfModule.PdfLinearGradientMode.Horizontal,
});

// Align the anchor with the left edge of the rectangle
page.Canvas.DrawString({ s: text, font: font, brush: gradient, x: 40, y: 110 });

Get a Free License

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

A contract or a report of a few dozen pages lands on your desk, and you need to confirm where a certain clause or amount appears and how many times. Going through it page by page by eye is easy to get wrong. Marking the hits is the least effort, but find-and-highlight in a desktop application is hard to fit into a web workflow, and screenshotting every page to annotate it is not realistic either.

Spire.PDF for JavaScript loads, processes and saves PDF documents in the browser on WebAssembly, so finding and highlighting happen entirely locally, reading and writing files through a virtual file system (VFS) with no backend involved. This article uses PdfTextFinder to implement three ways of finding and highlighting: the whole document, a given area, and a regular expression.

This article covers three core features:

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


Find and Highlight All Matches

PdfTextFinder locates given text in the text layer of a page. It works page by page: build one finder for each page of the document and you can find every match across the whole file at once. Each hit is highlighted by calling HighLight(), which is yellow by default; pass a color when different keywords have to be told apart.

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

    // Search page by page and highlight every hit
    for (let i = 0; i < doc.Pages.Count; i++) {
      const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(i));
      finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
      const finds = finder.Find('Ornamental');
      for (let j = 0; j < finds.length; j++) {
        finds.get(j).HighLight();
      }
    }

    // Save the document
    const outputFileName = 'FindAndHighlight.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>Find and Highlight All Matches</h1>
      <button onClick={findAndHighlightAll}>
        Find and Highlight
      </button>
    </div>
  );
}

export default App;

Every Ornamental in the document is highlighted:

Every Ornamental in the document is highlighted


Find and Highlight Within an Area

Text that reads the same on a page is often only partly worth marking. PdfTextFinder also provides Options.Area, which narrows the search to a rectangle; matches that fall outside it are not returned, and therefore not highlighted. The rectangle is described in page coordinates, with the origin at the top-left corner of the page and units in points.

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

    // Set the search range: page coordinates, origin at the top-left, units in points
    const area = new pdfModule.RectangleF({ x: 60, y: 488, width: 420, height: 160 });

    const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(0));
    finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
    finder.Options.Area = area;

    // Only matches that fall inside the rectangle are returned
    const finds = finder.Find('Ornamental');
    for (let j = 0; j < finds.length; j++) {
      finds.get(j).HighLight();
    }

    // Save the document
    const outputFileName = 'HighlightInArea.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>Find and Highlight Within an Area</h1>
      <button onClick={findAndHighlightInArea}>
        Find and Highlight
      </button>
    </div>
  );
}

export default App;

Only the Ornamental inside the comparison table is highlighted; the body text and the list stay as they are:

Only the Ornamental inside the comparison table is highlighted


Find and Highlight by Regular Expression

The target is not necessarily a fixed set of characters. Options.Parameter decides the matching rule; set it to Regex and the argument to Find() becomes a regular expression, so targets that share a shape but differ in content can be circled in one pattern. The default value matches by substring, which is what the previous two sections do, and the same enumeration also offers IgnoreCase and WholeWord.

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

    // Match page by page with a regular expression and highlight every hit
    for (let i = 0; i < doc.Pages.Count; i++) {
      const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(i));
      finder.Options.Parameter = pdfModule.TextFindParameter.Regex;
      const finds = finder.Find('Figure\\s*\\d');
      for (let j = 0; j < finds.length; j++) {
        finds.get(j).HighLight({ color: pdfModule.Color.get_Orange() });
      }
    }

    // Save the document
    const outputFileName = 'HighlightByRegex.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>Find and Highlight by Regular Expression</h1>
      <button onClick={findAndHighlightByRegex}>
        Find and Highlight
      </button>
    </div>
  );
}

export default App;

The three figure captions are matched by the pattern and highlighted in orange:

Three figure captions matched by the regular expression and highlighted


FAQ

The highlight works, but it is missing from the reader's comments panel

Cause: HighLight() writes into the page content, not into PDF annotations. The highlight block is written to the page's content stream when the document is saved, so the file grows by roughly 2 KB, and no annotation object is added to the output — reading it back with PyMuPDF gives an empty set from page.annots().

Fix: Treat the highlight as page graphics. It displays the same way as an annotation; it simply has no annotation identity, so it cannot be selected, deleted or recolored one by one in the reader. When highlights have to be managed as annotations, record the hit positions before saving and keep that list on the application side.

The search area is set, but nothing is highlighted

Cause: Options.Area uses page coordinates (origin at the top-left corner of the page, units in points). If the rectangle is too small or mispositioned, every match falls outside it. It also takes effect on the current page only — in a multi-page document, apply the same rectangle to the finder of the page you want.

Fix: Measure the target region in whole-page coordinates first, then narrow it down. In the sample below the comparison table falls within x≈60–480 and y≈488–648, so RectangleF({ x: 60, y: 488, width: 420, height: 160 }) frames it exactly, and the body text and list outside the rectangle are not matched:

// Search the current page only, and match inside this rectangle only
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(0));
finder.Options.Area = new pdfModule.RectangleF({ x: 60, y: 488, width: 420, height: 160 });

When the coordinates are uncertain, run the search once without Area and read the actual position from each hit's finds.get(i).Bounds[0] to work back to the rectangle.

The same regex matches a Chinese document but fails on a Japanese one

Cause: A regular expression matches the actual characters in the PDF text layer, not meanings. The dash that separates the ranges is not the same across the three samples: Chinese and English use – (U+2013), while Japanese uses the full-width tilde ~ (U+FF5E), so a pattern with only one of them hits only one kind of document.

Fix: Put the dashes in a character class so that both spellings work at once:

// Number ranges: 6–9 / 1–3 m / 6~9月 all match
finder.Options.Parameter = pdfModule.TextFindParameter.Regex;
const finds = finder.Find('[0-9]+\\s*[–-~]\\s*[0-9]+');

Get a Free License

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

A product list or an order detail table rarely keeps one format per cell. A description note has to be split over several lines, only a few words of a promotion line deserve to stand out, and a stock warning needs to be picked out in red. Doing that by hand means double-clicking every cell, selecting the fragment and adjusting the font one by one, which does not scale past a handful of rows. An HTML string describes exactly this kind of content — one piece of text whose parts are styled differently — and Spire.XLS for JavaScript exposes the HtmlString property, which renders a piece of HTML straight into a cell: line breaks, bold, italic, underline, color and font size all follow the tags, so a batch is one assignment inside a loop. It runs entirely in the browser on WebAssembly, managing input and output files with a virtual file system (VFS) and requiring no backend service.

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.


Write HTML text with line breaks into a cell

Breaking a line inside a cell normally means pressing Alt+Enter while editing it. When the data comes from a database or a form, that manual step cannot be batched, and the <br> tag in HTML means precisely "break the line here". HtmlString parses <br>, <div> and <p> into line breaks inside the cell, so a single string written into one cell becomes several lines. The steps are as follows:

  1. Load the font into the VFS.
  2. Create a workbook, take its first worksheet and widen column A.
  3. Build an HTML string for each entry, splitting the parts with <br>.
  4. Write them into the cells of column A with HtmlString.
  5. Save the workbook.

The complete code example below shows how to write HTML text with line breaks into an Excel cell in React:

function App() {
  const setMultilineText = 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 into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Create a workbook, take the first worksheet and widen column A so a single line stays on one line
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);
    sheet.Range.get('A1:A5').ColumnWidth = 30;

    // Each description has three parts, separated by <br> inside the same cell
    const notes = [
      'Bluetooth 5.3 dual pairing<br>30-hour battery<br>USB-C fast charge',
      'Silent micro switches<br>Built-in 800 mAh battery<br>2.4G wireless',
      'Dual-mic noise cancelling<br>6 hours per charge<br>Magnetic charging case',
      '1080P resolution<br>780 g lightweight<br>Single USB-C cable',
      'Wooden enclosure<br>Bluetooth 5.0<br>Remote control included',
    ];

    // Write the HTML strings into column A, three lines inside one cell
    notes.forEach((html, index) => {
      sheet.Range.get(`A${index + 1}`).HtmlString = html;
    });

    // Save the workbook
    const outputFileName = 'MultilineHtmlText.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // Release the resources
    workbook.Dispose();

    // Read the result file out of the VFS and trigger 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>Excel HTML rich text</h1>
      <button onClick={setMultilineText}>Multiline HTML text</button>
    </div>
  );
}

export default App;

After running, the effect of writing HTML text with line breaks into a cell:

Write HTML text with line breaks into a cell


Format part of the text in a cell

Text inside one cell does not have to share one format. Within a single run of text, only part of it usually needs to stand out, while the rest can stay at the regular weight. HtmlString supports the <b>, <i> and <u> tags, and takes color and font size in a style attribute on a <span>. Every fragment wrapped in a tag becomes its own run of formatting without affecting the others, and the tags themselves never appear in the cell. The steps are as follows:

  1. As in the previous section, load the font into the VFS, create a workbook, take the first worksheet and widen column A.
  2. Mark the text to emphasize with <b>, <i> and <u>, and wrap the text to highlight in <span style="color:#C00000;font-size:14pt">, giving the color as a hexadecimal value and the size in points.
  3. Write the strings into column A; the rest of the text in the same cell keeps its original format.
  4. Save the workbook.

The complete code example below shows how to format part of the text in an Excel cell in React:

function App() {
  const setPartialFormatting = 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 into the VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Create a workbook, take the first worksheet and widen column A so a single line stays on one line
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);
    sheet.Range.get('A1:A5').ColumnWidth = 30;

    // Each part of a promotion line gets its own <b>, <i>, <u> or styled <span>;
    // bold, italic, underline, color and font size apply only to the wrapped text
    const promotions = [
      '<b>Save 50 over 300</b>, <i>three days only</i>, <u>limit 2 per customer</u>, <span style="color:#C00000;font-size:14pt">only 3 left</span>',
      '<b>Second one half price</b>, <i>this week only</i>, <u>not combinable</u>',
      '<b>100 off instantly</b>, <i>ends at midnight</i>, <u>free carry case</u>, <span style="color:#C00000;font-size:14pt">low stock</span>',
      '<b>Trade-in bonus 200</b>, <i>until month end</i>, <u>old device required</u>',
      '<b>Buy one get one</b>, <i>500 sets only</i>, <u>gift chosen at random</u>, <span style="color:#E36C0A;font-size:14pt">restocking</span>',
    ];

    // Write the HTML strings into column A
    promotions.forEach((html, index) => {
      sheet.Range.get(`A${index + 1}`).HtmlString = html;
    });

    // Save the workbook
    const outputFileName = 'PartialFormattingHtml.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // Release the resources
    workbook.Dispose();

    // Read the result file out of the VFS and trigger 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>Excel HTML rich text</h1>
      <button onClick={setPartialFormatting}>Partial formatting</button>
    </div>
  );
}

export default App;

After running, the effect of formatting part of the text in a cell as bold, italic, underlined, colored or larger:

Format part of the text in a cell


FAQ

Does the HTML parser swallow runs of spaces?

Cause: A browser collapses consecutive spaces into one when it renders HTML, which makes it easy to assume Spire does the same and to reach for &nbsp; to build any padding.

Solution: It does not. A B is still five spaces when read back out of the cell, and leading spaces survive as well. &nbsp; works too, but ordinary alignment does not need it.

Does writing rich text wipe out the alignment the cell already had?

Cause: HtmlString does change the cell's font properties, so it is fair to wonder whether it resets the alignment set earlier along with them.

Solution: The alignment is untouched. Set Style.HorizontalAlignment first and write the HtmlString afterwards, and the alignment reads back unchanged — setting the format before the content is safe.

const cell = sheet.Range.get('A1');
cell.Style.HorizontalAlignment = xlsModule.HorizontalAlignType.Center;
cell.HtmlString = '<b>Centered</b>';

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 17