The same PDF can look different when opened on different machines: if the font a document references is not installed locally, the viewer substitutes another one. Conversely, when brand standards or legal requirements say an outgoing file may only use specified fonts, you first need to know which fonts the original uses. Common browser-side PDF libraries mostly draw pages; they do not expose font resources.

This article uses Spire.PDF for JavaScript to read and replace the fonts in a PDF: first list the document's fonts with PdfDocument.UsedFonts, then replace the standard and non-embedded TrueType fonts with a new TrueType font via PdfUsedFont.Replace(). Everything runs in the browser; files are read and written through a virtual file system (VFS) and never uploaded to a server.

This article covers two core features:

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


Get the Fonts Used in a PDF

Spire.PDF for JavaScript provides the PdfDocument.UsedFonts property for reading every font used in a document. Each item in the collection is a PdfUsedFont, whose Name, Size, Style, and Type give the font name, size, style, and font type.

The steps are:

  1. Get the Spire.PDF WASM module and confirm it is ready.
  2. Load the sample PDF into the VFS, create a PdfDocument object, and load the file.
  3. Read PdfDocument.UsedFonts and take each PdfUsedFont from the collection.
  4. Put together each font's name, size, style, and type, and write them to a text file.
  5. Read the result file from the VFS and trigger a browser download.

The code for getting the fonts in a PDF is as follows:

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

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

    // UsedFonts lists the fonts the document currently uses
    const fonts = doc.UsedFonts;

    const styleNames = new Map([
      [pdfModule.PdfFontStyle.Regular.value, 'Regular'],
      [pdfModule.PdfFontStyle.Bold.value, 'Bold'],
      [pdfModule.PdfFontStyle.Italic.value, 'Italic'],
      [pdfModule.PdfFontStyle.Underline.value, 'Underline'],
      [pdfModule.PdfFontStyle.Strikeout.value, 'Strikeout'],
    ]);
    const typeNames = new Map([
      [pdfModule.PdfFontType.Standard.value, 'Standard'],
      [pdfModule.PdfFontType.TrueType.value, 'TrueType'],
      [pdfModule.PdfFontType.TrueTypeEmbedded.value, 'Embedded TrueType'],
    ]);

    // Collect each font's name, size, style, and type
    let report = `Document: ${inputFileName}\r\nFont count: ${fonts.length}\r\n\r\n`;
    for (let i = 0; i < fonts.length; i++) {
      const font = fonts.get(i);
      report += `${i + 1}. ${font.Name}\r\n`;
      report += `   Size: ${font.Size}\r\n`;
      report += `   Style: ${styleNames.get(font.Style.value)}\r\n`;
      report += `   Type: ${typeNames.get(font.Type.value)}\r\n\r\n`;
    }

    // Write the font list into the VFS
    const outputFileName = 'FontList.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, report);
    doc.Close();

    // Read the result file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/plain;charset=utf-8' });
    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>Get the Fonts Used in a PDF</h1>
      <button onClick={getFonts}>
        Get fonts
      </button>
    </div>
  );
}

export default App;

The font list is written to a text file, listing each font's name, size, style, and type:

The font list is written to a text file, listing each font's name, size, style, and type


Replace the Fonts Used in a PDF

Spire.PDF for JavaScript also provides the PdfUsedFont.Replace() method, which swaps one font in a document for another font object. It works only with standard Type1 fonts and non-embedded TrueType fonts; embedded fonts (such as subset-embedded CJK fonts) cannot be replaced and must be skipped.

The steps are:

  1. Get the Spire.PDF WASM module and confirm it is ready.
  2. Load the sample PDF and the replacement font file into the VFS, with the font file placed under /Library/Fonts/.
  3. Create a PdfDocument object, load the PDF, and read PdfDocument.UsedFonts.
  4. Create a PdfTrueTypeFont object, iterate over UsedFonts, skip embedded fonts, and call PdfUsedFont.Replace() on standard and non-embedded TrueType fonts.
  5. Save the result with PdfDocument.SaveToFile() and trigger a browser download.

The code for replacing the fonts in a PDF is as follows:

function App() {
  const replaceFonts = 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 replacement font and the sample PDF
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'Font_Sample.pdf';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

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

    // UsedFonts returns the fonts used in the document
    const fonts = doc.UsedFonts;
    const newFont = new pdfModule.PdfTrueTypeFont({
      fontFamily: 'Arial Unicode MS',
      size: 12,
      style: pdfModule.PdfFontStyle.Regular,
      unicode: true
    });

    // Replace one by one: Replace() supports only standard Type1 and non-embedded TrueType fonts
    for (let i = 0; i < fonts.length; i++) {
      const font = fonts.get(i);
      // Embedded fonts cannot be replaced; skip them
      if (font.Type.value === pdfModule.PdfFontType.TrueTypeEmbedded.value) {
        continue;
      }
      font.Replace(newFont);
    }

    // Save the result file
    const outputFileName = 'ReplacedFonts.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>Replace the Fonts Used in a PDF</h1>
      <button onClick={replaceFonts}>
        Replace fonts
      </button>
    </div>
  );
}

export default App;

After replacement, the text in the standard font uses the new TrueType font while embedded fonts stay unchanged:

After replacement, the text in the standard font uses the new TrueType font while embedded fonts stay unchanged


FAQ

Why do the text size and style change after replacement

Reason: Replace() takes a complete font object, and replacement applies the new object's own size and style; the original sizes of individual text runs are not preserved.

Solution: Set the target font's size and style to the values you want in the result. For example, to make everything 12 pt regular, pass size: 12 and style: PdfFontStyle.Regular to PdfTrueTypeFont.

Why do Chinese or Japanese characters turn into boxes or question marks after replacement

Reason: The target font has no glyphs for those characters, or PdfTrueTypeFont was created without Unicode enabled.

Solution: Use a font that covers the target language (such as Arial Unicode MS) and pass unicode: true when creating it. The font file must first be loaded into /Library/Fonts/ in the VFS so Spire.PDF can embed it.

Can I replace only one of the fonts

Reason: UsedFonts lists each font in the document, and Replace() acts on a single PdfUsedFont.

Solution: Check Name while iterating and replace only the matching font:

for (let i = 0; i < fonts.length; i++) {
  const font = fonts.get(i);
  if (font.Name === 'ArialMT') {
    font.Replace(newFont);
  }
}

Font names of subset-embedded fonts may carry prefixes such as BCDEEE+, so write the comparison against the Name you actually read from the list.

Why does Replace() report that the font is not a standard Type 1 or non-embedded TrueType font

Reason: PdfUsedFont.Replace() only works on standard Type1 fonts and non-embedded TrueType fonts. CJK fonts in PDFs are often subset-embedded TrueType fonts (UsedFonts reports them as Embedded TrueType), and Spire.PDF cannot replace them directly.

Solution: Check Type while iterating UsedFonts and call Replace() only on standard and non-embedded TrueType fonts; skip the rest, or prepare a document that uses standard or non-embedded fonts first.


Get a Free License

To remove the evaluation message from result documents or lift the feature limits, contact sales for a 30-day temporary license.

Thursday, 22 October 2026 09:34

Create PDF Booklets in React with JavaScript

When a product manual or conference handout is printed as a booklet, the pages of a multi-page document have to be rearranged onto facing sheets in saddle-stitch order. In desktop software that means dragging pages by hand, which goes wrong easily once the page count grows, and every new document starts over. Hand it to a server, and the file has to leave the user's device first.

This article uses Spire.PDF for JavaScript to create PDF booklets. It loads, imposes, and saves PDFs in the browser via WebAssembly, reading and writing every file through a virtual file system (VFS) with no backend involved.

This article covers two core features:

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


Impose a PDF as a Two-Sided Booklet Automatically

Spire.PDF for JavaScript provides the PdfDocument.CreateBooklet() method, which imposes the pages of an existing PDF onto facing pages in saddle-stitch order. The width and height parameters set the sheet size of the finished product; when bothSides is true, the layout is arranged for double-sided printing.

The steps are:

  1. Get the Spire.PDF WASM module and confirm it is ready.
  2. Load the source PDF into the VFS.
  3. Create a PdfDocument object and call CreateBooklet() with a width equal to two A4 pages side by side and the height of one A4 page.
  4. Save the result with SaveToFile().
  5. Read the result file from the VFS and trigger a browser download.

The code for creating a booklet by automatic imposition is as follows:

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

    // Spread size: width is two A4 pages side by side, height is one A4 page
    const width = pdfModule.PdfPageSize.A4().Width * 2;
    const height = pdfModule.PdfPageSize.A4().Height;

    // Create a document and impose the pages in saddle-stitch order
    const doc = new pdfModule.PdfDocument();
    doc.CreateBooklet({ fileName: inputFileName, width: width, height: height, bothSides: true });

    // Save the result file
    const outputFileName = 'Booklet_Auto.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>Create a Two-Sided Booklet</h1>
      <button onClick={createBooklet}>
        Create Booklet
      </button>
    </div>
  );
}

export default App;

The source pages are imposed onto spreads two at a time in saddle-stitch order:

Source pages imposed onto spreads two at a time in saddle-stitch order


Build Facing Pages in a Custom Page Order

Spire.PDF for JavaScript also provides the PdfPageBase.CreateTemplate() method, which extracts the content of a page into a template that PdfTemplate.Draw() can draw at a given position on a new page. A page-order array decides which two pages land on each sheet; the example puts consecutive pages side by side in reading order.

The steps are:

  1. Get the Spire.PDF WASM module and confirm it is ready.
  2. Load the source PDF into the VFS.
  3. Create a PdfDocument object and load the source document; then create an output document whose width is two A4 pages side by side and whose height is one A4 page.
  4. For each pair in the page order, take a page template with CreateTemplate() and draw it onto the left or right half of the spread with Draw().
  5. Save the result with SaveToFile(), then read it from the VFS and trigger a download.

The code for building facing pages manually is as follows:

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

    // Create a PdfDocument object and load the source document
    const srcDoc = new pdfModule.PdfDocument();
    srcDoc.LoadFromFile(inputFileName);
    const pageCount = srcDoc.Pages.Count;
    const a4 = pdfModule.PdfPageSize.A4();

    // Output document: width is two A4 pages side by side, height is one A4 page
    const bookletDoc = new pdfModule.PdfDocument();
    bookletDoc.PageSettings.Margins.All = 0;
    bookletDoc.PageSettings.Width = a4.Width * 2;
    bookletDoc.PageSettings.Height = a4.Height;

    // Two consecutive pages per sheet: 1|2, 3|4, 5|6, ...
    for (let i = 0; i < pageCount; i += 2) {
      const sheet = bookletDoc.Pages.Add();

      // Left half
      const leftTemplate = srcDoc.Pages.get_Item(i).CreateTemplate();
      leftTemplate.Draw({ page: sheet, location: new pdfModule.PointF(0, 0) });

      // Right half; with an odd page count the last sheet leaves the right side blank
      if (i + 1 < pageCount) {
        const rightTemplate = srcDoc.Pages.get_Item(i + 1).CreateTemplate();
        rightTemplate.Draw({ page: sheet, location: new pdfModule.PointF(a4.Width, 0) });
      }
    }

    // Save the result file
    const outputFileName = 'Booklet_CustomOrder.pdf';
    bookletDoc.SaveToFile(outputFileName);
    bookletDoc.Close();
    srcDoc.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>Custom Page Order Imposition</h1>
      <button onClick={createCustomBooklet}>
        Build Facing Pages
      </button>
    </div>
  );
}

export default App;

Two consecutive pages sit side by side on one sheet; a 6-page document produces 3 spreads:

Two consecutive pages side by side on one sheet; a 6-page document produces 3 spreads


FAQ

What determines the paper size of the booklet

Reason: The width and height passed to CreateBooklet() describe the sheet size of the finished spread, not the size of a single page in the source document. This article uses "A4 width × 2" and "A4 height", which gives an A3-sized spread with one A4 page on each half.

Solution: To print on A4 paper, set the sheet to A4 landscape — width equal to the A4 height, height equal to the A4 width; each half then holds an A5 page, and the source pages are scaled down to half a sheet.

// A4 landscape: one A5 page on each half
const width = pdfModule.PdfPageSize.A4().Height;
const height = pdfModule.PdfPageSize.A4().Width;

What does the bothSides parameter do

Reason: bothSides tells CreateBooklet() whether the result will be printed double-sided. true groups pages into front and back pairs in saddle-stitch order and pads the page count to a multiple of 4; false lays pages out for single-sided printing and adds no blank pages.

Solution: Pass true when you print on a duplex printer and fold and staple the result; pass false for single-sided printing only. The print mode must match the imposed layout, or the page order will not line up.

How is the booklet arranged when the page count is not a multiple of 4

Reason: A saddle-stitch sheet holds four pages (two on each side); when the pages run out, the last sheet has blank positions.

Solution: With automatic imposition, CreateBooklet() pads the required spreads. With manual imposition, the loop checks i + 1 < pageCount before drawing the right half, so an unpaired page simply leaves the right side blank.

In manual imposition the content drifts to one side or runs off the sheet

Reason: PdfTemplate.Draw() draws at the template's original size by default. If a source page is wider than half a sheet, the right-hand page crosses the center line or extends past the edge.

Solution: Make the source page size match half the sheet. In this article the source pages are A4 and the spread is two A4 pages wide, so placing them at x = 0 and x = A4.Width aligns exactly. If the source size differs, use PdfCanvas.DrawTemplate() with size set to the half-sheet size; the template is scaled to fit.

sheet.Canvas.DrawTemplate({
  template: rightTemplate,
  location: new pdfModule.PointF(a4.Width, 0),
  size: new pdfModule.SizeF(a4.Width, a4.Height)
});

Get a Free License

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

When a contract or report needs a supporting image or a logo, a fully opaque image covers the body text, so it gets pushed into a corner. If an image has already been placed and needs to be faded, the usual fix is to go back to the source file and redo the layout, and without that file you are stuck with what is on the page. Both tasks are manual in desktop software and hard to fit into a batch Web workflow; handled on a server, the file has to leave the user's device first.

This article uses Spire.PDF for JavaScript to do the work in the browser. A new image is drawn at a given alpha with PdfCanvas.SetTransparency(); images already on the page are located with PdfImageHelper, deleted, and redrawn semi-transparent inside their original bounds. The library runs on WebAssembly and reads and writes PDF files through a virtual file system (VFS), so no backend is required.

This article covers two core features:

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


Add an Image with Transparency to a PDF Document

Spire.PDF for JavaScript provides the PdfCanvas.SetTransparency() method to set transparency on the canvas. When only alpha is passed, fill and stroke share that value. It ranges from 0 to 1: 0 is fully transparent and 1 is fully opaque. The setting applies to all drawing that follows the call, so wrap it in Save() and Restore() to keep other page content from fading as well.

Follow these steps:

  1. Get the Spire.PDF WebAssembly module and confirm it is ready.
  2. Load the PDF file and the image into the VFS.
  3. Create a PdfDocument object, load the PDF, take the first page, and load the image with PdfImage.FromFile().
  4. Save the canvas state with PdfCanvas.Save(), call SetTransparency(), and draw the image with DrawImage().
  5. Restore the canvas state with Restore(), save the result with SaveToFile(), and trigger the download.

The code for adding a semi-transparent image:

function App() {
  const addTransparentImage = async () => {
    // Get the Spire.PDF WebAssembly 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 image into the VFS
    const inputFileName = 'Business_Data_Overview.pdf';
    const imageFileName = 'scenery.jpg';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS(imageFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

    // Take the first page and load the image to draw
    let page = doc.Pages.get_Item(0);
    let image = pdfModule.PdfImage.FromFile(imageFileName);

    // Save the canvas state so the transparency applies only to this block
    const state = page.Canvas.Save();

    // An alpha of 0.35 makes the image semi-transparent while the content below stays visible
    page.Canvas.SetTransparency({ alpha: 0.35 });
    page.Canvas.DrawImage({ image: image, x: 145, y: 400, width: 300, height: 173 });

    // Restore the canvas state so later drawing is not affected
    page.Canvas.Restore({ state: state });

    // Save the result file
    const outputFileName = 'TransparentImage.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 Semi-Transparent Image</h1>
      <button onClick={addTransparentImage}>
        Add Image
      </button>
    </div>
  );
}

export default App;

With the image drawn at 35% opacity, the content underneath still shows through:

With the image drawn at 35% opacity, the content underneath still shows through


Adjust the Transparency of Existing Images on a PDF Page

Spire.PDF for JavaScript also provides the PdfImageHelper class to read, delete, and replace images on a page. GetImagesInfo() returns the bounds and image stream of each image. Recreate a PdfImage from each stream, delete the original with DeleteImage(), and redraw it at the specified transparency inside its Bounds.

Follow these steps:

  1. Get the Spire.PDF WebAssembly module and confirm it is ready.
  2. Load the PDF file into the VFS.
  3. Create a PdfDocument object, load the PDF, get the page, and read all images on it with PdfImageHelper.GetImagesInfo().
  4. Recreate each image from PdfImageInfo.Image with PdfImage.FromStream() and record its Bounds.
  5. Delete all original images with DeleteImage(), redraw them at the specified transparency inside their original bounds, then save with SaveToFile() and trigger the download.

The code for adjusting the transparency of existing images:

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

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

    // Take the first page and read the image information on it
    let page = doc.Pages.get_Item(0);
    let helper = new pdfModule.PdfImageHelper();
    let images = helper.GetImagesInfo(page);

    // Collect each image stream and bounds first, for redrawing after deletion
    let items = [];
    for (let i = 0; i < images.length; i++) {
      items.push({
        info: images[i],
        image: pdfModule.PdfImage.FromStream(images[i].Image),
        bounds: images[i].Bounds
      });
    }

    // Delete all original images first, then redraw them together; redrawing right after each delete gets overwritten by the next delete
    for (let i = 0; i < items.length; i++) {
      helper.DeleteImage({ imageInfo: items[i].info });
    }

    // Redraw each image at 40% opacity inside its original bounds
    for (let i = 0; i < items.length; i++) {
      const { image, bounds } = items[i];
      const state = page.Canvas.Save();
      page.Canvas.SetTransparency({ alpha: 0.4 });
      page.Canvas.DrawImage({
        image: image,
        x: bounds.X,
        y: bounds.Y,
        width: bounds.Width,
        height: bounds.Height
      });
      page.Canvas.Restore({ state: state });
    }

    // Save the result file
    const outputFileName = 'ExistingImageTransparency.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>Adjust Existing Image Transparency</h1>
      <button onClick={adjustImageTransparency}>
        Adjust Transparency
      </button>
    </div>
  );
}

export default App;

The three images on the page are redrawn at their original positions with 40% opacity, while the body text is unaffected:

The three images on the page are redrawn at their original positions with 40% opacity, while the body text is unaffected


FAQ

Why does content drawn after the image also become semi-transparent?

Cause: Transparency is part of the canvas state. Every drawing call after SetTransparency() keeps using that value until the state is restored or set again.

Solution: Wrap the setting in Save() and Restore() so the transparency applies only to the drawing in between:

const state = page.Canvas.Save();
page.Canvas.SetTransparency({ alpha: 0.35 });
page.Canvas.DrawImage({ image: image, x: 145, y: 400, width: 300, height: 173 });
page.Canvas.Restore({ state: state });

When adjusting existing images, why does only the last image become transparent?

Cause: DeleteImage() rewrites the page content stream. If you redraw right after each delete, the next delete overwrites the previous drawing, and often only the last image remains.

Solution: Split the work into three passes: read every image stream and bounds, delete all original images, then redraw them all.

// Delete all original images first
for (let i = 0; i < items.length; i++) {
  helper.DeleteImage({ imageInfo: items[i].info });
}

// Then redraw all of them
for (let i = 0; i < items.length; i++) {
  const { image, bounds } = items[i];
  const state = page.Canvas.Save();
  page.Canvas.SetTransparency({ alpha: 0.4 });
  page.Canvas.DrawImage({
    image: image,
    x: bounds.X,
    y: bounds.Y,
    width: bounds.Width,
    height: bounds.Height
  });
  page.Canvas.Restore({ state: state });
}

How do I adjust only one image on the page?

Cause: GetImagesInfo() returns all images on the page in order, each as a PdfImageInfo whose bounds are available from Bounds.

Solution: Skip the loop and process only the item you need. The following adjusts just the first image:

let images = helper.GetImagesInfo(page);
let info = images[0];
let image = pdfModule.PdfImage.FromStream(info.Image);
let bounds = info.Bounds;

helper.DeleteImage({ imageInfo: info });

const state = page.Canvas.Save();
page.Canvas.SetTransparency({ alpha: 0.4 });
page.Canvas.DrawImage({ image: image, x: bounds.X, y: bounds.Y, width: bounds.Width, height: bounds.Height });
page.Canvas.Restore({ state: state });

What do alpha values of 0 and 1 do?

Cause: alpha represents opacity and takes a linear value from 0 to 1.

Solution: At 0 the image is fully transparent and invisible on the page; at 1 it is fully opaque and looks exactly like a direct draw. For a faded effect, 0.2 to 0.5 is the usual range.


Get a Free License

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

Tuesday, 20 October 2026 02:45

Convert PowerPoint to Markdown in Python

Banner

PowerPoint locks content inside slides. Each page is a canvas, and titles and body text sit in separate shape frames. To move that content into documents, a knowledge base, or a large language model, you first need to turn it back into editable plain text. Markdown works well for that layer, and in a Git repository it also shows line-by-line diffs.

This article uses Spire.Presentation for Python to convert PowerPoint presentations to Markdown. You can also export only one slide, or export several slides in a loop. The process runs without Microsoft PowerPoint installed.

This article covers:

Install the Python PowerPoint to Markdown Library

Install Command

Before converting, install the library with pip:

pip install Spire.Presentation

For installation help, see How to Install Spire.Presentation for Python in VS Code.


Convert a Whole PowerPoint Presentation to Markdown

Presentation.SaveToFile with FileFormat.Markdown converts a whole PowerPoint presentation to Markdown in one call. Titles, paragraphs and tables keep their original hierarchy, and slides are separated by ==== Page N ==== page markers.

Steps:

  1. Create a Presentation object.
  2. Load the PPT/PPTX file with Presentation.LoadFromFile.
  3. Save the whole presentation as Markdown with Presentation.SaveToFile and FileFormat.Markdown.
  4. Release resources.
from spire.presentation import *

# Create a Presentation object
presentation = Presentation()

# Load the PowerPoint file
presentation.LoadFromFile("DataBriefing.pptx")

# Save the whole presentation as a Markdown file
presentation.SaveToFile("DataBriefing.md", FileFormat.Markdown)

# Release resources
presentation.Dispose()

The Markdown file produced by converting the whole PowerPoint presentation:

The Markdown file produced by converting the whole PowerPoint presentation


Export a Specific Slide to Markdown

When you need just one page, there is no need to export the whole deck and trim it afterward. ISlide.SaveToFile exports a specific slide to Markdown: get the target page with Slides[index], then save it as Markdown on its own.

Steps:

  1. Create a Presentation object.
  2. Load the PPT/PPTX file with Presentation.LoadFromFile.
  3. Get the target slide with Presentation.Slides[index].
  4. Save that slide as Markdown with ISlide.SaveToFile and FileFormat.Markdown.
  5. Release resources.
from spire.presentation import *

# Create a Presentation object
presentation = Presentation()

# Load the PowerPoint file
presentation.LoadFromFile("DataBriefing.pptx")

# Get the second slide (index starts at 0)
slide = presentation.Slides[1]

# Save the specified slide as a Markdown file
slide.SaveToFile("Slide-2.md", FileFormat.Markdown)

# Release resources
presentation.Dispose()

The Markdown file produced by exporting a specific slide:

The Markdown file produced by exporting a specific slide

Export Multiple Slides to Markdown at Once

When you need more than one page, put the slide indexes in a loop and call Slides[index].SaveToFile page by page. Each slide is saved as its own Markdown file:

from spire.presentation import *

# Create a Presentation object
presentation = Presentation()

# Load the PowerPoint file
presentation.LoadFromFile("DataBriefing.pptx")

# Export the first and second slides in order
for index in (0, 1):
    slide = presentation.Slides[index]
    slide.SaveToFile(f"Slide-{index + 1}.md", FileFormat.Markdown)

# Release resources
presentation.Dispose()

Each slide is exported separately, so page numbers in the Markdown restart from Page 1.


Summary

Whole-deck conversion and single-slide export both work on the same Presentation object. LoadFromFile reads the file, FileFormat.Markdown sets the output format, and SaveToFile writes it out. Export the whole deck directly when that is what you need. To take only a few pages, pull out the target slides and save them individually, or put them in a loop.


FAQ

Do I need Microsoft PowerPoint installed to convert

Reason: Spire.Presentation for Python parses the PPT/PPTX file structure itself and does not call Office components.

Solution: No. Install the Python library and the conversion runs.

Can I convert multiple PowerPoint files at once

Reason: One Presentation object maps to one presentation, so it handles one file at a time.

Solution: Put the conversion logic in a loop and process each PPT/PPTX in a directory:

import os
from spire.presentation import *

for name in os.listdir("presentations"):
    if name.endswith(".pptx"):
        presentation = Presentation()
        presentation.LoadFromFile(os.path.join("presentations", name))
        base = os.path.splitext(name)[0]
        presentation.SaveToFile(os.path.join("markdown", base + ".md"), FileFormat.Markdown)
        presentation.Dispose()

Why does each page in the exported Markdown start with a warning line

Reason: Without a license applied, Spire.Presentation for Python inserts an Evaluation Warning line into the exported content.

Solution: Apply a temporary license and the line no longer appears. See "Get a Free License" below.


Get a Free License

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

The same deck often needs more or fewer slides for different occasions: add two case-study pages for a client meeting, then drop the outdated ones after the previous version has been used. Doing this by hand in desktop software means someone babysitting the file, and it is hard to fold into a batch Web workflow; doing it server-side means uploading the file first, which takes it off the user's device.

This article shows how to add and delete PowerPoint slides directly in the browser with Spire.Presentation for JavaScript. Everything runs in the browser on WebAssembly — no Office install, no backend, and the file stays on the user's device.

The article covers three features:

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


Add PowerPoint Slides at the End

Spire.Presentation for JavaScript adds a new slide at the end of a presentation with the Slides.Append() method. What gets added depends on the arguments:

Call What it appends
Slides.Append() A new slide with the default (blank) layout
Slides.Append({ template: <layout object> }) A new slide built from the given layout
Slides.Append({ slide }) A copy of an existing slide

To build a slide from a specific layout, get the layout object first: ppt.Masters.get_Item(0).Layouts.get_Item(i) — the i-th layout under the first slide master (i is 0-based).

The steps are as follows:

  1. Get the Spire.Presentation WASM module and confirm it is ready.
  2. Read the sample presentation and the font into the VFS, create a Presentation object, and load the file.
  3. Add slides at the end with Presentation.Slides.Append() (pass template or slide to choose the content).
  4. Save the result with Presentation.SaveToFile().
  5. Read the result back from the VFS and trigger a browser download.

The code for adding PowerPoint slides:

function App() {
  const addSlides = async () => {
    // Get the Spire.Presentation WASM module
    const pptModule = window.wasmModule?.spirepresentation;

    // Check whether the module is ready
    if (!pptModule) {
      alert('Spire.Presentation is not ready yet');
      return;
    }

    // Load the sample presentation and the font
    await window.spire.FetchFileToVFS('SamplePresentation.pptx', "", `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', "/Library/Fonts/", `${process.env.PUBLIC_URL}/font/`);

    // Create a Presentation instance and load the presentation
    const ppt = new pptModule.Presentation();
    ppt.LoadFromFile('SamplePresentation.pptx');

    // Append in turn: default layout, a chosen layout, a clone of the first slide
    ppt.Slides.Append();
    ppt.Slides.Append({ template: ppt.Masters.get_Item(0).Layouts.get_Item(1) });
    ppt.Slides.Append({ slide: ppt.Slides.get_Item(0) });

    // Save the result
    const outputFileName = 'SlidesAdded.pptx';
    ppt.SaveToFile({ file: outputFileName, fileFormat: pptModule.FileFormat.Pptx2013 });
    ppt.Dispose();

    // 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/vnd.openxmlformats-officedocument.presentationml.presentation' });
    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 PowerPoint Slides</h1>
      <button onClick={addSlides}>
        Add Slides
      </button>
    </div>
  );
}

export default App;

After appending three slides to the end:

After appending three slides to the end


Insert PowerPoint Slides at a Specific Position

Slides.Insert() takes an index that sets where the slide goes: it is inserted at that index and the slides after it shift back. The content is again decided by the arguments:

Call What it inserts
Slides.Insert({ index, template: <layout object> }) A new slide built from the given layout, inserted at index
Slides.Insert({ index, slide }) A copy of an existing slide inserted at index

The layout object is obtained the same way as for Slides.Append().

The steps are as follows:

  1. Get the Spire.Presentation WASM module and confirm it is ready.
  2. Read the sample presentation and the font into the VFS, create a Presentation object, and load the file.
  3. Insert slides at the given positions with Presentation.Slides.Insert().
  4. Save the result with Presentation.SaveToFile().
  5. Read the result back from the VFS and trigger a browser download.

The code for inserting PowerPoint slides:

function App() {
  const insertSlides = async () => {
    // Get the Spire.Presentation WASM module
    const pptModule = window.wasmModule?.spirepresentation;

    // Check whether the module is ready
    if (!pptModule) {
      alert('Spire.Presentation is not ready yet');
      return;
    }

    // Load the sample presentation and the font
    await window.spire.FetchFileToVFS('SamplePresentation.pptx', "", `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', "/Library/Fonts/", `${process.env.PUBLIC_URL}/font/`);

    // Create a Presentation instance and load the presentation
    const ppt = new pptModule.Presentation();
    ppt.LoadFromFile('SamplePresentation.pptx');

    // Insert in turn: a chosen layout, a clone of the first slide
    ppt.Slides.Insert({ index: 1, template: ppt.Masters.get_Item(0).Layouts.get_Item(1) });
    ppt.Slides.Insert({ index: 2, slide: ppt.Slides.get_Item(0) });

    // Save the result
    const outputFileName = 'SlidesInserted.pptx';
    ppt.SaveToFile({ file: outputFileName, fileFormat: pptModule.FileFormat.Pptx2013 });
    ppt.Dispose();

    // 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/vnd.openxmlformats-officedocument.presentationml.presentation' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Insert PowerPoint Slides</h1>
      <button onClick={insertSlides}>
        Insert Slides
      </button>
    </div>
  );
}

export default App;

After inserting two slides in the middle:

After inserting two slides in the middle


Delete PowerPoint Slides

Spire.Presentation for JavaScript can delete slides too, with Slides.RemoveAt() or Slides.Remove(). They target a slide differently:

Method Based on
Slides.RemoveAt(index) The position of the slide (0-based index)
Slides.Remove(slide) The slide object itself

Each removal shifts the index of every later slide back by one.

The steps are as follows:

  1. Get the Spire.Presentation WASM module and confirm it is ready.
  2. Read the sample presentation and the font into the VFS, create a Presentation object, and load the file.
  3. Delete a slide by index with Presentation.Slides.RemoveAt().
  4. Save the result with Presentation.SaveToFile().
  5. Read the result back from the VFS and trigger a browser download.

The code for deleting PowerPoint slides:

function App() {
  const removeSlides = async () => {
    // Get the Spire.Presentation WASM module
    const pptModule = window.wasmModule?.spirepresentation;

    // Check whether the module is ready
    if (!pptModule) {
      alert('Spire.Presentation is not ready yet');
      return;
    }

    // Load the sample presentation and the font
    await window.spire.FetchFileToVFS('SamplePresentation.pptx', "", `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', "/Library/Fonts/", `${process.env.PUBLIC_URL}/font/`);

    // Create a Presentation instance and load the presentation
    const ppt = new pptModule.Presentation();
    ppt.LoadFromFile('SamplePresentation.pptx');

    // RemoveAt deletes by index: once the first slide is gone, the slides after it shift back
    ppt.Slides.RemoveAt(0);

    // Save the result
    const outputFileName = 'SlidesRemoved.pptx';
    ppt.SaveToFile({ file: outputFileName, fileFormat: pptModule.FileFormat.Pptx2013 });
    ppt.Dispose();

    // 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/vnd.openxmlformats-officedocument.presentationml.presentation' });
    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 PowerPoint Slides</h1>
      <button onClick={removeSlides}>
        Remove Slides
      </button>
    </div>
  );
}

export default App;

After deleting a slide:

After deleting a slide

Delete a Slide by Object

Slides.RemoveAt() above deletes by index; Slides.Remove() takes a slide object instead. To point at a page, fetch it with Slides.get_Item(index) and hand it to Slides.Remove():

// Delete by object: fetch the second slide, then remove it
ppt.Slides.Remove(ppt.Slides.get_Item(1));

FAQ

Do the files need to be uploaded to a server

Not with the usage in this article. Spire.Presentation for JavaScript runs in the browser on WebAssembly, and presentations are read and written locally through a virtual file system (VFS) — adding, deleting, and saving all happen locally, and the library itself does not send files to a server. Whether a file leaves the device in the end depends on the other code you add to the page.

Can I add or delete several slides at once

Yes, just call in a loop: call Slides.Append() / Slides.Insert() several times to add or insert; when deleting several slides, handle the indices in order, or it is easy to delete the wrong slide or go out of range (see the last question).

Why does a new Presentation already contain a blank slide

Cause: A presentation created with new Presentation() already contains one blank slide; Append() only adds to the end and does not replace it.

Fix: To start from empty, remove it first, then append the slides you need:

let ppt = new pptModule.Presentation();

// Remove the blank slide it comes with
ppt.Slides.RemoveAt(0);

// Then append as needed
ppt.Slides.Append({ template: ppt.Masters.get_Item(0).Layouts.get_Item(1) });

Why is Append() without arguments a blank page

Cause: With no arguments it creates a slide with the default (blank) layout, so there are no ready-made title or body placeholders and it looks like an empty page.

Fix: To get ready-made placeholders, pass a layout that has them, or clone an existing slide; you can also add content yourself on the new page with Shapes.AppendShape():

// Build from a layout, so the page comes with title and content placeholders
ppt.Slides.Append({ template: ppt.Masters.get_Item(0).Layouts.get_Item(1) });

// Clone the first slide, and its content comes along
ppt.Slides.Append({ slide: ppt.Slides.get_Item(0) });

Why do I delete the wrong slide, or hit an index-out-of-range error, when removing several slides

Cause: Slides is indexed by position, and every removal shifts the index of the slides after it back by one. When you delete "front to back" with fixed indices (RemoveAt(0), RemoveAt(1), …), the second step no longer deletes the slides you meant; and once an index goes past the current count (Slides.Count - 1), RemoveAt throws.

Fix: Do not use increasing fixed indices for several deletions; delete the current first slide (index 0) every time — the slides after it fill in automatically:

// Delete the first three slides: always remove index 0 and the following slides shift up
for (let i = 0; i < 3; i++) {
  ppt.Slides.RemoveAt(0);
}

Get a Free License

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

Friday, 16 October 2026 08:17

Convert SVG to PDF in React with JavaScript

The charts, flow diagrams, and logos exported on the web are mostly SVG, yet printing, archiving, or an approval workflow usually asks for PDF. Saving them one by one in office software takes a person sitting through it, and batch-converting on a server means uploading the files first.

This article uses Spire.PDF for JavaScript to bridge SVG and PDF. It is built on WebAssembly, loading and saving documents in the browser, and it reads and writes files through a virtual file system (VFS), with no backend involved. There are two paths: turn an SVG into a PDF file on its own, or draw the SVG as vector graphics onto a chosen page of an existing PDF.

This article covers two core features:

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


Convert an SVG to PDF

Spire.PDF for JavaScript provides the PdfDocument.LoadFromSvg method, which loads an SVG as a PDF document. The loaded document has a single page, and the page size is taken from the width/height on the SVG root element.

The steps are as follows:

  1. Get the Spire.PDF WASM module and make sure it is ready.
  2. Read the SVG file into the virtual file system (VFS).
  3. Create a PdfDocument object and load the SVG with the LoadFromSvg() method.
  4. Use the PdfDocument.SaveToFile() method to save the result file.
  5. Read the result file from the VFS and trigger a browser download.

The code to convert an SVG to PDF is as follows:

function App() {
  const convertSvgToPdf = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check if the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the SVG to convert into the VFS
    const inputFileName = 'VectorChart.svg';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // Load the SVG: the document has a single page, the size of the SVG
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromSvg({ fileName: inputFileName });

    // Save the result file
    const outputFileName = 'VectorChart.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>Convert SVG to PDF</h1>
      <button onClick={convertSvgToPdf}>
        Convert to PDF
      </button>
    </div>
  );
}

export default App;

The result is a single-page PDF whose page size matches the SVG:

The single-page PDF converted from the SVG bar chart


Draw an SVG onto an Existing PDF

Spire.PDF for JavaScript also provides the PdfPageBase.CreateTemplate method and the PdfCanvas.DrawTemplate method, which overlay an SVG onto an already laid-out PDF. Load the SVG, turn its single page into a template, then draw it onto the target page at a chosen position and size — the original document's page count, page size, and existing content are untouched.

The steps are as follows:

  1. Get the Spire.PDF WASM module and make sure it is ready.
  2. Read the existing PDF and the SVG logo into the virtual file system (VFS).
  3. Create a PdfDocument object for each and load the existing PDF and the SVG.
  4. Use the CreateTemplate() method to turn the SVG's first page into a template, then draw it onto the target page with the Canvas.DrawTemplate() method, at a given position and size.
  5. Use the SaveToFile() method to save the PDF that was drawn on, then read the result file from the VFS and trigger a browser download.

The code to draw an SVG onto an existing PDF is as follows:

function App() {
  const drawSvgOnPdf = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check if the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load both the existing PDF and the SVG logo into the VFS
    const pdfFileName = 'ProductManual.pdf';
    const svgFileName = 'VectorLogo.svg';
    await window.spire.FetchFileToVFS(pdfFileName, '', `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS(svgFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // Load the existing PDF
    let existingPdf = new pdfModule.PdfDocument();
    existingPdf.LoadFromFile(pdfFileName);

    // Load the SVG and turn its single page into a template
    let svgDoc = new pdfModule.PdfDocument();
    svgDoc.LoadFromSvg({ fileName: svgFileName });
    const template = svgDoc.Pages.get_Item(0).CreateTemplate();

    // Draw the template on page 1: position and size are in points, origin at the top-left of the page
    const location = new pdfModule.PointF(390, 50);
    const size = new pdfModule.SizeF(130, 60);
    existingPdf.Pages.get_Item(0).Canvas.DrawTemplate({ template, location, size });

    // Save the PDF that was drawn on
    const outputFileName = 'ProductManual_WithLogo.pdf';
    existingPdf.SaveToFile(outputFileName);
    svgDoc.Close();
    existingPdf.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>Draw SVG on Existing PDF</h1>
      <button onClick={drawSvgOnPdf}>
        Insert SVG
      </button>
    </div>
  );
}

export default App;

The original manual content is unchanged, with a vector logo added at the top right:

The SVG logo inserted at the top right of page 1 of the existing PDF


Frequently Asked Questions

Why isn't the output PDF A4-sized

Cause: When LoadFromSvg creates the page it takes the width/height from the SVG root element, converting pixels to points at 75%. In the example, a 760 × 520 SVG becomes a single page of 570 × 390 points — nothing to do with A4.

Solution: To use another size, change the width, height, or viewBox on the SVG root element and convert again.

How do I draw the SVG on another page or at another position

Cause: Which page DrawTemplate draws on is decided by how the page is fetched, and which corner it lands in is decided by location; both are passed in explicitly by the caller.

Solution: Switch pages with Pages.get_Item(index) (0-based), change the position via PointF, and the size via SizeF — all in points, with the origin at the top-left of the page:

// Draw on page 2, bottom left, at a size of 100 × 46 points
existingPdf.Pages.get_Item(1).Canvas.DrawTemplate({
  template: template,
  location: new pdfModule.PointF(40, 760),
  size: new pdfModule.SizeF(100, 46),
});

Is the inserted SVG vector or a bitmap

Cause: What CreateTemplate returns is a PDF template corresponding to the SVG content, and DrawTemplate draws it into the page as vectors, without going through a bitmap.

Solution: Nothing extra to set. Zoom in on the page afterwards and the logo's edges stay crisp; it prints without blurring either.


Get a Free License

If you would like to remove the evaluation message from the result document or lift feature restrictions, please contact sales for a 30-day temporary license.

Friday, 16 October 2026 06:01

Convert PDF to SVG in React with JavaScript

When charts and diagrams inside a PDF are reused on the web, the usual approach is to render them to PNG first — a bitmap blurs when scaled up, and a different resolution means taking a new screenshot. Web illustrations, vector icons, and print layouts that need to scale to any size really call for SVG.

This article uses Spire.PDF for JavaScript to convert PDF to SVG. It loads and saves PDF documents in the browser on top of WebAssembly, and the whole conversion runs locally: files are read and written through a virtual file system (VFS), with no backend involved. Output has two forms — the whole document merged into one SVG, or one SVG per page.

This article covers two core features:

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


Convert the Whole PDF to a Single SVG

Spire.PDF for JavaScript provides the PdfDocument.ConvertOptions.OutputToOneSvg property, which decides whether the whole document is written to one SVG or to one file per page. It defaults to false, so each page becomes its own file; set it to true and the pages are stacked vertically on a single canvas in their original order, then written out by SaveToFile with FileFormat.SVG.

The steps are as follows:

  1. Get the Spire.PDF WASM module and make sure it is ready.
  2. Load the sample PDF and the Arial font, create a PdfDocument object, and load the file.
  3. Set PdfDocument.ConvertOptions.OutputToOneSvg to true so all pages go into one SVG.
  4. Use the PdfDocument.SaveToFile() method to save the result file.
  5. Read the result file from the VFS and trigger a browser download.

The code to convert the whole PDF to a single SVG is as follows:

function App() {
  const convertPdfToOneSvg = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check if the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Converting to SVG restores the page text, so put the Arial font into the VFS font directory first
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Load the PDF to convert into the VFS
    const inputFileName = 'Chapter_Document.pdf';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // Output file name: the whole document merged into one SVG
    const outputFileName = 'Chapter_Document.svg';

    // Load the PDF and send all pages into a single SVG
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);
    doc.ConvertOptions.OutputToOneSvg = true;
    doc.SaveToFile(outputFileName, pdfModule.FileFormat.SVG);
    doc.Close();

    // Read the generated file from the VFS and trigger download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'image/svg+xml' });
    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>Convert PDF to SVG</h1>
      <button onClick={convertPdfToOneSvg}>
        Convert to a single SVG
      </button>
    </div>
  );
}

export default App;

The result is a single SVG file with the five pages joined top to bottom:

A single SVG converted from the whole PDF, with the five pages joined top to bottom


Convert Each PDF Page to a Separate SVG

Spire.PDF for JavaScript also provides the PdfToSvgConverter class, which converts a document to SVG page by page. It is a second, equivalent entry point to PdfDocument, and SvgOptions carries OutputToOneSvg as well. This section keeps the default per-page output and downloads the files one by one.

The steps are as follows:

  1. Get the Spire.PDF WASM module and make sure it is ready.
  2. Load the sample PDF and the Arial font.
  3. Create a PdfToSvgConverter object, passing the path of the PDF to convert.
  4. Use the PdfToSvgConverter.Convert() method to convert page by page, one SVG per page.
  5. Read each page's file from the VFS and trigger a download for each.

The code to convert each PDF page to a separate SVG is as follows:

function App() {
  const convertPdfPagesToSvg = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check if the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Converting to SVG restores the page text, so put the Arial font into the VFS font directory first
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Load the PDF to convert into the VFS
    const inputFileName = 'Chapter_Document.pdf';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // Output file name prefix: pages are written as Chapter_Document_1.svg, Chapter_Document_2.svg, ...
    const outputFileName = 'Chapter_Document.svg';

    // Convert page by page with the converter
    const converter = new pdfModule.PdfToSvgConverter(inputFileName);
    converter.Convert(outputFileName);

    // Read each page's file from the VFS and trigger a download for each
    const fs = window.dotnetRuntime.Module.FS;
    const prefix = outputFileName.replace(/\.svg$/i, '_');
    const pageFiles = fs.readdir('/').filter((name) => name.startsWith(prefix) && name.endsWith('.svg'));

    for (const pageFile of pageFiles) {
      const blob = new Blob([fs.readFile(pageFile)], { type: 'image/svg+xml' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = pageFile;
      a.click();
      URL.revokeObjectURL(url);
    }
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Convert PDF to SVG</h1>
      <button onClick={convertPdfPagesToSvg}>
        Convert page by page
      </button>
    </div>
  );
}

export default App;

Each page becomes its own SVG file, named with the page number:

SVG files converted from each page of the PDF

Set the SVG Size

The converter also exposes the SvgOptions.ScaleX and ScaleY properties, which scale the output size by a factor; both default to 1. Setting them to 2 doubles the size. The SVG content is vector, so — unlike a bitmap — it stays sharp when scaled.

// Scale the output size up by 2x
const converter = new pdfModule.PdfToSvgConverter('Chapter_Document.pdf');
converter.SvgOptions.ScaleX = 2;
converter.SvgOptions.ScaleY = 2;
converter.Convert('Chapter_Document.svg');

Frequently Asked Questions

Why do I get a bunch of xxx_1.svg, xxx_2.svg files instead of one .svg

Cause: When PdfDocument.SaveToFile converts to SVG it writes page by page, and ConvertOptions.OutputToOneSvg defaults to false, so the file name becomes "prefix_page.svg". A single-page PDF hides the difference; with more pages it shows up.

Solution: To get a single file, turn the property on before saving:

doc.ConvertOptions.OutputToOneSvg = true;
doc.SaveToFile('Chapter_Document.svg', pdfModule.FileFormat.SVG);

With PdfToSvgConverter, the matching switch is converter.SvgOptions.OutputToOneSvg — the two entry points use an equivalent switch and produce identical output.

How do I fix the error Cannot found font(Arial) installed on the system.

Cause: Converting to SVG restores page text as <text> elements, and the converter looks the font up by name in /Library/Fonts/ of the virtual file system. If that directory is empty, it throws this error.

Solution: Put the font into the VFS before LoadFromFile:

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

Arial and Arial Unicode MS are two different files; supplying only the latter still raises the same error.

Is the text in the converted SVG still selectable

Cause: <text> and stroked paths are different things. The converter keeps page text as <text> elements and turns only graphics and lines into vector paths (<path>), so the text stays selectable and searchable.

Solution: No extra settings. Open the generated SVG in an editor and you will see <text> nodes; zoom it in a browser and neither the text nor the lines blur.

What if I only want to convert a few pages

Cause: SaveToFile also has an overload that takes a page range, working on the same per-page files that are saved in the VFS.

Solution: Pass the start and end page indexes; the index is 0-based and inclusive:

// Write only pages 1 and 2, producing first_two_pages_1.svg and first_two_pages_2.svg
doc.SaveToFile('first_two_pages.svg', 0, 1, pdfModule.FileFormat.SVG);

Get a Free License

If you would like to remove the evaluation message from the result document or lift feature restrictions, please contact sales for a 30-day temporary license.

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

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

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


Set an Expiry Date for a PDF Document

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

Step 1: Load the PDF to Process

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

// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;

// Check whether the module is ready
if (!pdfModule) {
  alert('Spire.PDF is not ready yet');
  return;
}

// Load the PDF into the VFS
const inputFileName = 'ContractTemplate.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

Step 2: Write the Expiry Check Script

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

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

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

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

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

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

Step 4: Read the Result and Trigger the Download

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

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

Complete Code

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

function App() {
  const setExpiryDate = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check whether the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF into the VFS
    const inputFileName = 'ContractTemplate.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

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

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

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

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Set an Expiry Date for a PDF Document</h1>
      <button onClick={setExpiryDate}>
        Set Expiry Date
      </button>
    </div>
  );
}

export default App;

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

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


FAQ

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

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

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

Does the content become completely inaccessible once the document expires

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

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

How do I change the expiry date or the alert text

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

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

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

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

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

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

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

Get a Free License

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

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

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

This article covers two key operations:

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


Modify Hyperlinks

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

function App() {
  const updateHyperlinks = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check whether the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF to be processed into the VFS
    const inputFileName = 'ProductManual.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

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

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

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

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

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

export default App;

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

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


Delete Hyperlinks

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

function App() {
  const deleteHyperlinks = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check whether the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF to be processed into the VFS
    const inputFileName = 'ProductManual.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

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

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

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

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

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

export default App;

All hyperlinks in the document have been removed:

All hyperlinks in the document have been removed


FAQ

I changed Uri, but the address is unchanged after saving

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

Solution: test both classes and set the matching property:

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

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

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

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

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

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

Does deleting hyperlinks also delete comments?

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

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

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

Get a Free License

To remove the evaluation message from the generated documents, or to get rid of the function limitations, please contact sales for a temporary license valid for 30 days.

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

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

This article covers two core tasks:

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


Add Hyperlinks to a PDF

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

  • Simple text link: the URL drawn as ordinary text, with no annotation attached; whether it is clickable is up to the viewer — Chrome and others auto-detect the URL and make it clickable — so it is here only for contrast with the explicitly linked ones.
  • Hypertext link: PdfTextWebLink with Text, Url, Font and Brush set; DrawTextWebLink() draws the underlined link text in one step.
  • Document link: PdfFileLinkAnnotation(hotspot rectangle, target file), with the rectangle defined by you.
function App() {
  const addLinksToNewPdf = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check that the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

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

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

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

    const page = section.Pages.Add();

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

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

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

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

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

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

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

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

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

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

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

export default App;

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

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


Add Hyperlinks to Text

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

function App() {
  const addHyperlinksToText = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check that the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF to be processed into the VFS
    const inputFileName = 'ProductManual.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

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

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

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

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

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

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

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

export default App;

All three occurrences of Aurora now carry a hyperlink:

All three occurrences of Aurora now carry a hyperlink


FAQ

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

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

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

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

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

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

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

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


Get a Free License

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

Page 1 of 8