A PDF's version number decides which features the document may use, and whether older readers, print systems, and archival platforms can open it at all. Documents collected from many sources come with mixed versions: some produced by new tools and declaring 1.7, others from systems that have not been updated in years and still sit at 1.4. To deliver a uniform format for a target environment, the version number has to be rewritten — and desktop software only lets you do that by hand, one file at a time.

This article uses Spire.PDF for JavaScript to change a PDF document's version number. It runs on WebAssembly to load, modify, and save PDFs directly in the browser, working through a virtual file system (VFS) with no backend required.

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


Change the PDF Version

Assign a PdfVersion value to doc.FileInfo.Version to rewrite the version number; once saved, the file header is written as the matching %PDF-x.y. Set it to Version1_4 and a 1.7 document drops to 1.4, ready for readers or print systems that only accept the older specification.

function App() {
  const changePdfVersion = 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 = 'Multipage_Document.pdf';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

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

    // Set the version number to the target value, here downgrading to 1.4
    doc.FileInfo.Version = pdfModule.PdfVersion.Version1_4;

    const outputFileName = 'Version-1.4.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>Change PDF Version</h1>
      <button onClick={changePdfVersion}>
        Change
      </button>
    </div>
  );
}

export default App;

A document saved with the version set to 1.4 — its file header now reads %PDF-1.4

A document saved with the version set to 1.4 — its file header now reads %PDF-1.4


FAQ

Why does the version stay the same after I set it and reopen the file

Reason: FileInfo.Version changes a property of the in-memory document object, and only saving writes it into the file. Close the document right after setting the version, or open the original input file again, and you will of course still see the old version number.

Solution: After assigning the value you must call SaveToFile to write a new output file, then open that result to check it:

// Set the version number first
doc.FileInfo.Version = pdfModule.PdfVersion.Version1_4;

// Then save — only now is the version number written to the file
doc.SaveToFile('Version-1.4.pdf');

Which version number should I set

Reason: It depends on what the recipient supports. Older print systems and archival platforms often require 1.4 or lower, while newer tools mostly handle 1.7. FileInfo.Version takes a PdfVersion enum, where Version1_0 through Version1_7 map to PDF 1.0 through PDF 1.7 in order.

Solution: Set the highest version the recipient accepts, assigning the enum directly:

// Deliver to readers or print systems that only accept the older specification
doc.FileInfo.Version = pdfModule.PdfVersion.Version1_4;

// The target environment is newer — keep or raise it to 1.7
doc.FileInfo.Version = pdfModule.PdfVersion.Version1_7;

Why does the document fail to open or render incorrectly after downgrading

Reason: FileInfo.Version only changes the declaration; it does not clean up objects in the document that go beyond that version's specification. If the source uses features introduced in a later version — such as cross-reference streams, object streams, or compressed transparency from PDF 1.5 onward — an older reader parsing it as a lower version will fail.

Solution: Before changing the declaration, confirm the source file does not rely on those features. If it is clean, changing the version number is enough; otherwise, use a source file that avoids the newer features, or first rebuild the document in a way that pushes its objects back to the older specification (re-export, or print to PDF) and then change the version number.


Get a Free License

If you want to remove the evaluation message from the result documents or get rid of feature limitations, please contact sales to obtain a free 30-day temporary license.

Tuesday, 29 September 2026 03:01

Add a Cross-Page Seal in React with JavaScript

Contracts, bids, and other multi-page documents get a cross-page seal before they go out, so that every page carries a fragment of the seal — if the other party swaps or reorders pages, the fragments no longer line up. With paper you stamp the whole stack at once; an electronic PDF has no equivalent gesture: paste the seal image on whole and every page shows a complete seal, which serves no cross-page purpose.

This article shows how to add a cross-page seal to a multi-page contract with Spire.PDF for JavaScript. Built on WebAssembly, it loads, modifies, and saves PDF documents directly in the browser, so the whole process stays local and files are read and written through a virtual file system (VFS) with no backend service required.

For installation and project configuration, refer to Integrating Spire.PDF for JavaScript in a React Project. The following example assumes Spire.PDF is installed and the WebAssembly module has been initialized.


Add a Cross-Page Seal

A cross-page seal cuts one seal into as many pieces as there are pages, leaving one piece on each page, so that flipping through the pages rebuilds the complete seal. The cutting is carried by a PdfTemplate: the template's bounds are the clipping area, so shifting the seal one strip width further left on each successive page makes exactly that page's strip show through, and Canvas.DrawTemplate drops it at the right edge of the page.

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

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

    // Load the document to be sealed and the seal image into the VFS
    const inputFileName = 'Number.pdf';
    const sealFileName = 'Seal.png';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS(sealFileName, '', `${process.env.PUBLIC_URL}/data/`);

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

    // Load the seal image; its width and height are in pixels, treated as points (1 px = 1 pt) when laid out
    const seal = pdfModule.PdfImage.FromFile(sealFileName);

    // Overall scale of the seal, and the scaled seal width and height
    const scale = 0.55;
    const sealWidth = seal.Width * scale;
    const sealHeight = seal.Height * scale;

    // Split the seal evenly by page count, one strip per page
    const pageCount = doc.Pages.Count;
    const stripWidth = sealWidth / pageCount;

    // Draw the i-th strip of the seal at the right edge of each page
    for (let i = 0; i < pageCount; i++) {
      const page = doc.Pages.get_Item(i);

      // The template bounds are the clipping area: one strip wide and as tall as the seal
      const template = new pdfModule.PdfTemplate({ width: stripWidth, height: sealHeight });
      template.Graphics.ScaleTransform(scale, scale);

      // Shift the seal one more strip to the left on every page, so the next strip shows through
      template.Graphics.DrawImage({ image: seal, x: -(i * seal.Width) / pageCount, y: 0 });

      // Every page uses the same spot: 24 points in from the right edge, vertically centered
      const x = page.Canvas.ClientSize.Width - 24 - stripWidth;
      const y = (page.Canvas.ClientSize.Height - sealHeight) / 2;
      page.Canvas.DrawTemplate({ template: template, location: new pdfModule.PointF(x, y) });
    }

    // Save to the VFS
    const outputFileName = 'Cross-Page-Seal.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>Add a Cross-Page Seal</h1>
      <button onClick={addCrossPageSeal}>
        Add Seal
      </button>
    </div>
  );
}

export default App;

The contract after the cross-page seal is applied: each page carries one fragment of the seal at its right edge, and turning to the last page shows the right half of the seal:

The contract after the cross-page seal is applied; each page carries one fragment of the seal at its right edge


FAQ

Why does each page show only a narrow slice of the seal

Reason: A cross-page seal is by definition cut evenly across the pages, so each page carries only the strip seal width / page count. The more pages, the narrower the fragment on each page — that is by design: a narrow fragment at a fixed position is what makes a single swapped page impossible to line back up.

Solution: The page count is decided by the document, so to make the seal more prominent the only lever is to enlarge the whole seal, and the fragments grow with it:

// Raise 0.55, say to 0.8: the whole seal and every page's fragment grow together
const scale = 0.8;
const sealWidth = seal.Width * scale;

The seal is so large it covers the body text

Reason: PdfImage.Width and Height report pixel values, and without scaling they are laid out at 1 pixel = 1 point. A 400×400-pixel seal takes up 400 points of width, while an A4 page is only 595 points wide, so it will cover the body text.

Solution: Scale it down proportionally inside the template with ScaleTransform, and convert the seal width, seal height, and strip width by the same factor — all three must come from one source, or the strips will not line up with the placement:

const scale = 0.55;
const sealWidth = seal.Width * scale;
const sealHeight = seal.Height * scale;
const stripWidth = sealWidth / pageCount;

Get a Free License

If you wish to remove the evaluation message from the result document or remove feature limitations, please contact sales to obtain a temporary license valid for 30 days.

A product rename, a wording change, a company name typed wrong in a template — edits like these are awkward once they land in a PDF. When the source document is not at hand, the only options are to export the whole page as an image and lay text over it, or to delete the original text in a reader and type a replacement in by hand. Contracts and manuals make this worse: the same name can be scattered across body text, bullet lists and closing notes, and fixing each one by hand is slow and easy to miss.

Spire.PDF for JavaScript loads, edits and saves PDF documents in the browser on WebAssembly, so a rename is only a matter of locating the text and writing it back, with files read and written through a virtual file system (VFS) and no backend involved. The four sections below cover replacing the first match, replacing every match (together with three switches: recoloring, match mode and replacement area), covering the original word with a background color, and drawing a rectangle over the original before redrawing the new word. In the sample the old and new names are the same length, so the layout does not shift after replacement.

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.


Replace the First Match

Spire.PDF for JavaScript provides the PdfTextReplacer class for turning text on a page into another piece of text. It is built per page, and ReplaceText replaces only the first match it finds, returning the number of replacements actually made (1 here); any remaining occurrences of the same name on the page are left as they are.

function App() {
  const replaceFirstMatch = 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.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Load the PDF file to be processed into the VFS
    const inputFileName = 'ProductOverview.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);

    // Take the first page; the replacement is limited to this page
    let page = doc.Pages.get_Item(0);

    // Create the text replacer and replace only the first match
    const replacer = new pdfModule.PdfTextReplacer(page);
    replacer.ReplaceText('Stellar Drive', 'Nova Drive');

    const outputFileName = 'ReplaceFirstMatch.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 First Match</h1>
      <button onClick={replaceFirstMatch}>
        Replace
      </button>
    </div>
  );
}

export default App;

Only the first match is replaced:

Only the first match is replaced — the lead already shows the new name, the list and the closing note still show the old one


Replace All Matches

Spire.PDF for JavaScript also provides PdfTextReplacer.ReplaceAllText(), which swaps every matching piece of text on the page in one go. It takes the same arguments as ReplaceText and likewise returns the number of replacements. It also has a three-argument overload, ReplaceAllText(old, new, color), which recolors the text as it replaces it.

function App() {
  const replaceAllMatches = 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.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Load the PDF file to be processed into the VFS
    const inputFileName = 'ProductOverview.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);

    let page = doc.Pages.get_Item(0);

    // The third argument is a color, recoloring the new word as it is written
    const replacer = new pdfModule.PdfTextReplacer(page);
    replacer.ReplaceAllText('Stellar Drive', 'Nova Drive', pdfModule.Color.get_Red());

    const outputFileName = 'ReplaceAllMatches.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 All Matches</h1>
      <button onClick={replaceAllMatches}>
        Replace
      </button>
    </div>
  );
}

export default App;

All old name are replaced, and the new name is recolored red at the same time:

All four occurrences of the old name replaced and the new name recolored red

Beyond recoloring, PdfTextReplacer.Options has two more switches. ReplaceType decides what counts as a match: by default only an identical string is accepted, and it can be switched to IgnoreCase, WholeWord or Regex. SetReplacementArea limits the replacement to a rectangle, which PdfTextFinder can work out first:

const replacer = new pdfModule.PdfTextReplacer(page);
// Ignore case
replacer.Options.ReplaceType = pdfModule.ReplaceActionType.IgnoreCase;

// Limit the replacement to a rectangle
const finds = new pdfModule.PdfTextFinder(page).Find('Stellar Drive');
replacer.Options.SetReplacementArea(finds.get(0).Bounds[0]);

// Call it once the options are set
const count = replacer.ReplaceAllText('Stellar Drive', 'Nova Drive');

Replace by Covering

Spire.PDF for JavaScript also provides PdfTextFragment.ApplyRecoverString(), which covers a located piece of text in place: it lays a background color over the original and then writes the new text in the same position. It spares you from working out coordinates and redrawing by hand. The price is that only the background color can be chosen: the new text is drawn with the font resolved from the VFS and keeps the original foreground color, and with a white background the seam is invisible. When the font and the color need to change as well, see the redrawing approach in the next section.

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

    // Load the PDF file to be processed into the VFS
    const inputFileName = 'ProductOverview.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);

    let page = doc.Pages.get_Item(0);

    // Locate the target text with a finder first
    const finder = new pdfModule.PdfTextFinder(page);
    const finds = finder.Find('Stellar Drive');

    // Cover each hit in turn: the second argument is the background color,
    // and the third one, true, writes the new text as Unicode
    for (let i = 0; i < finds.length; i++) {
      finds.get(i).ApplyRecoverString('Nova Drive', pdfModule.Color.get_White(), true);
    }

    const outputFileName = 'RecoverReplace.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 by Covering</h1>
      <button onClick={replaceByCovering}>
        Replace
      </button>
    </div>
  );
}

export default App;

A document where the original text is covered with a white background and the new word is written in its place:

The original text covered with a white background and the new word written in its place


Draw a Rectangle to Cover the Original and Redraw the Text

The first three sections all work on the text layer. This one takes a different tack: use PdfTextFinder to get the position of the keyword, draw a white rectangle to cover the original text, and then write the new text inside that rectangle. Compared with ApplyRecoverString, the extra freedom is that you choose the font, size and color yourself.

function App() {
  const replaceByDrawing = 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.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Load the PDF file to be processed into the VFS
    const inputFileName = 'ProductOverview.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);

    let page = doc.Pages.get_Item(0);

    // Locate the target text with a finder first
    const finder = new pdfModule.PdfTextFinder(page);
    const finds = finder.Find('Stellar Drive');

    // The white rectangle that covers the original, plus the new word's font and color
    const white = pdfModule.PdfBrushes.get_White();
    const FONT_SIZE = 9;
    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: FONT_SIZE });
    const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_DarkBlue() }) });

    // Turn LineLimit off so the text is not clipped to the box
    const format = new pdfModule.PdfStringFormat();
    format.LineLimit = false;
    // Set the alignment of the text
    format.Alignment = pdfModule.PdfTextAlignment.Center;
    format.LineAlignment = pdfModule.PdfVerticalAlignment.Middle;

    for (let i = 0; i < finds.length; i++) {
      const rec = finds.get(i).Bounds[0];

      // Cover the original text with a white rectangle first
      page.Canvas.DrawRectangle({ brush: white, rectangle: rec });

      // Write the new word straight into the hit box
      page.Canvas.DrawString({ s: 'Nova Drive', font: font, brush: brush, layoutRectangle: rec, format: format });
    }

    const outputFileName = 'DrawRectangleAndText.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>Draw a Rectangle to Cover the Original and Redraw the Text</h1>
      <button onClick={replaceByDrawing}>
        Replace
      </button>
    </div>
  );
}

export default App;

A document where a white rectangle covers the original text and the new word is written directly inside the hit box:

A white rectangle covering the original text with the new word written directly inside the hit box


FAQ

The old name can still be found and copied after replacement

Cause: ApplyRecoverString is a visual-layer operation — it lays a background color over the original text and draws the new word on top, while the original text object stays in the text layer, so searching or copying in a reader still returns the old name. Measured on the covering replacement, the occurrence count of the old word is unchanged while the new word gains four hits.

Fix: When a real replacement at the content level is needed, switch to PdfTextReplacer; in its output the old word disappears from the text layer and the new word enters it. Measured, the four occurrences of the old word drop to 0 and the new word rises from 1 to 5:

// Content-level replacement: the old text no longer remains
const replacer = new pdfModule.PdfTextReplacer(page);
replacer.ReplaceAllText('Stellar Drive', 'Nova Drive');

Following the official sample's white rectangle plus DrawString redraw, the white background appears but the new text does not

Cause: The official sample follows the same approach as the fourth section here — locate the position with PdfTextFinder, draw a white rectangle to cover the original text, then paint the new text with Canvas.DrawString. The white rectangle is fine, but the redraw step has a trap:

  • The rectangle overload of DrawString is governed by PdfStringFormat.LineLimit, which defaults to true and clips the text to the box; the hit box hugs the glyphs tightly, so if its height is 11 points (exactly the font size) it cannot hold a full line height, and the whole line is clipped away — the new text appears neither on the page nor in the text layer.

Fix: Pass a PdfStringFormat with LineLimit turned off and the hit box can be used as the layout box as it is (which is what the fourth section does).

const format = new pdfModule.PdfStringFormat();
format.LineLimit = false;

page.Canvas.DrawString({ s: 'Nova Drive', font: font, brush: brush, layoutRectangle: rec, format: format });

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.

Monday, 28 September 2026 08:26

Convert PDF to PCL in React with JavaScript

Many printers and print services only speak PCL (Printer Command Language). When the contract or manual at hand is a PDF, the old routes were to install a driver and save the file under a new format from desktop software, or to hand the file to a server that does the conversion — the first cannot be folded into a web workflow, and the second means the document leaves the user's device.

This article shows how to convert a PDF to PCL with Spire.PDF for JavaScript. Built on WebAssembly, it loads and saves PDF documents directly in the browser, so the whole conversion stays local and files are read and written through a virtual file system (VFS) with no backend service required.

For installation and project configuration, refer to Integrating Spire.PDF for JavaScript in a React Project. The following example assumes Spire.PDF is installed and the WebAssembly module has been initialized.


Convert PDF to PCL

To hand a document to a print pipeline that only accepts PCL, use PdfDocument.SaveToFile to write the whole document out as a PCL file — pass FileFormat.PCL as the second argument and the output is HP-PCL XL (PCL6).

function App() {
  const convertPdfToPcl = 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;
    }

    // The conversion needs the Arial Unicode MS font, load it into the VFS first
    await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

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

    // Define the output file name
    const outputFileName = 'Business_Data_Overview.pcl';

    // Load the PDF document and pass FileFormat.PCL to write out a PCL file
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);
    doc.SaveToFile(outputFileName, pdfModule.FileFormat.PCL);
    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/octet-stream' });
    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 PCL</h1>
      <button onClick={convertPdfToPcl}>
        Start Converting
      </button>
    </div>
  );
}

export default App;

The generated PCL file, whose header states the PCL-XL language:

The generated PCL file, whose header states the PCL-XL language


FAQ

The output file name ends with .pcl, so why is the file still a PDF

Reason: The single-argument overload SaveToFile(fileName) always writes in PDF format — the extension takes no part in deciding the format, so a file whose name ends with .pcl still comes out as a document whose content is PDF.

Solution: Pass the output format explicitly as the second argument:

// Single argument: writes PDF
doc.SaveToFile(outputFileName);

// FileFormat.PCL: writes a real PCL file
doc.SaveToFile(outputFileName, pdfModule.FileFormat.PCL);

What to do about Cannot found font(Arial) installed on the system.

Reason: Converting to PCL means turning the text on the page into font subsetting instructions a printer can read, and the converter looks for fonts in /Library/Fonts/ in the virtual file system — an empty directory raises this error.

Solution: Send the font into the VFS before loading the PDF:

// Placed before LoadFromFile, the font is already in the VFS when the conversion runs
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

What format is the generated PCL file, and how do I open it

Reason: The output is PCL-XL (PCL6), and the file begins with ESC%-12345X@PJL ENTER LANGUAGE = PCLXL, followed by binary HP-PCL XL;2;0 data. It is not text, so opening it in an editor only shows garbage.

Solution: Hand it straight to a printer or print service that supports PCL; there is no need to open it by hand. To confirm the format is right, read the PJL declaration at the start of the file.


Get a Free License

If you wish to remove the evaluation message from the result document or remove feature limitations, please contact sales to obtain a temporary license valid for 30 days.

Color documents burn ink when printed, and once filed away that color is of no use; a PDF also stores its objects in the order they were written, so a reader has to download the whole file before it can render the first page — open a manual that runs to tens of megabytes and you stare at a blank screen first. Both jobs used to need desktop software, one file at a time, with no way into a web workflow.

This article uses Spire.PDF for JavaScript to convert PDF documents into grayscale and into linearized PDF files. Grayscaling changes the colors on the page, while linearization only changes how the objects are arranged inside the file, so the two do not affect each other. The library is built on WebAssembly and loads, modifies and saves PDF documents directly in the browser, reading and writing files through a virtual file system (VFS) with no backend service required.

This article covers two core features:

For installation and project configuration, refer to Integrating Spire.PDF for JavaScript in a React Project. The following examples assume Spire.PDF is installed and the WebAssembly module has been initialized.


Convert PDF to Grayscale

Grayscaling a document before printing or archiving saves color ink. Pass the input file to the PdfGrayConverter constructor and call ToGrayPdf to write out the grayscale document — the color images on the page are re-encoded as grayscale, and the text stays searchable.

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

    // Define the output file name
    const outputFileName = 'GrayscaleDocument.pdf';

    // Construct the grayscale converter from the input file and write out the grayscale document
    const converter = new pdfModule.PdfGrayConverter({ filePath: inputFileName });
    converter.ToGrayPdf({ filePath: outputFileName });
    converter.Dispose();

    // 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 PDF to Grayscale</h1>
      <button onClick={convertToGrayscale}>
        Start Converting
      </button>
    </div>
  );
}

export default App;

The sample document after grayscaling, with the color charts turned to grayscale

The sample document after grayscaling, with the color charts turned to grayscale


Linearize a PDF

Linearization is what fast web view is built on: a reader does not have to wait for the whole file to arrive before it renders the first page. The usage is the same as in the first feature — pass the input file to the PdfToLinearizedPdfConverter constructor and call ToLinearizedPdf to write out the result, and the document keeps its appearance and content unchanged.

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

    // Define the output file name
    const outputFileName = 'LinearizedDocument.pdf';

    // Construct the linearization converter from the input file and write out the linearized document
    const converter = new pdfModule.PdfToLinearizedPdfConverter({ filePath: inputFileName });
    converter.ToLinearizedPdf({ filePath: outputFileName });
    converter.Dispose();

    // 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>Linearize a PDF</h1>
      <button onClick={convertToLinearized}>
        Start Converting
      </button>
    </div>
  );
}

export default App;

The linearized document looks no different on the page, the change is inside the file structure

The linearized document looks no different on the page, the change is inside the file structure


FAQ

Why does the converted document contain an extra Evaluation Warning line

Reason: An unlicensed Spire.PDF inserts a line Evaluation Warning : The document was created with Spire.PDF for JavaScript. into the output document. It is appended per call — convert the same file twice and the warning appears twice.

Solution: Request a temporary 30-day license and the output document no longer carries the warning. See "Get a Free License" at the end of this article.

Why is the grayscale file smaller

Reason: Grayscaling works by re-encoding color images as single-channel DeviceGray images, and the space they take drops along with the channel count. The color images in the sample document were 3-channel PNGs totaling about 66 KB, and come out at about 31 KB; the whole file went from 82 KB down to 44 KB.

Solution: This is the normal result of image re-encoding and needs no extra handling. The page count and the text content are unchanged, the text is still searchable and copyable, and only the colors are rewritten.


Get a Free License

If you wish to remove the evaluation message from the result document or remove feature limitations, please contact sales to obtain a temporary license valid for 30 days.

Once a table has been turned into a PDF, the data is sealed up together with the layout. Send the same document to ten people and what comes back is ten separately filled-in PDFs; to move the contents of one onto a different template, the only way is to copy it off the screen one field at a time. The form fields themselves do have names and the values do hang off those names, but as soon as you leave a reader, that structure can no longer be pulled back out.

This article uses Spire.PDF for JavaScript to export the values in a form's fields to a data file, then import that data file back into a blank form. ExportData and ImportData both support Xml, Fdf, and XFdf — the three are nothing more than a difference of DataFormat enum values, called in exactly the same way, differing only in the structure of the file written out. The code below runs the whole flow with XML, with the FDF and XFDF versions listed alongside in comments; uncomment to switch. Spire.PDF for JavaScript reads and writes documents in the browser on top of WebAssembly, so the whole process happens locally, going through a virtual file system (VFS) to read and write files, 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.


Export PDF Form Data

PdfFormWidget.ExportData writes the values in a form's fields out to a single data file, with the format given by the second parameter, DataFormat. The three formats hold the same set of field values; they differ in file structure:

Data format File structure
DataFormat.Xml Adobe form data XML — the field name is the element name, the value is the element content
DataFormat.Fdf Forms Data Format (FDF) — a text structure starting with %FDF-, where /T holds the field name and /V the value
DataFormat.XFdf XFDF, standard XML — one <field name="…"> per field, with the value inside <value>

The third parameter is the form name; for an unnamed AcroForm, pass an empty string.

function App() {
  const exportFormData = 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 file to be exported into the VFS
    const inputFileName = 'CustomerInformationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

    // Build a PdfFormWidget from the document's form handle to reach the data export API
    const formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
    
    // This demo exports XML
    const dataFiles = [
      { fileName: 'FormData.xml', format: pdfModule.DataFormat.Xml },
      // { fileName: 'FormData.fdf', format: pdfModule.DataFormat.Fdf },
      // { fileName: 'FormData.xfdf', format: pdfModule.DataFormat.XFdf },
    ];

    for (const item of dataFiles) {
      // The third parameter is the form name; pass an empty string for an unnamed form
      formWidget.ExportData(item.fileName, item.format, '');
    }
    doc.Close();

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Export Form Data</h1>
      <button onClick={exportFormData}>
        Export
      </button>
    </div>
  );
}

export default App;

The exported XML form data file:

The exported XML form data file


Import PDF Form Data

PdfFormWidget.ImportData reads a data file and writes the values back into the form fields by field name; the second parameter, DataFormat, only determines how the file is parsed and has nothing to do with the file extension — the same for all three formats.

What gets imported is the blank form. The template goes out empty, and once the data files come back the values are filled in one by one — with a lot of fields there is no need to key everything in a second time.

function App() {
  const importFormData = 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 blank form to be filled into the VFS
    const inputFileName = 'BlankCustomerInformationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // This demo refills from the XML data file
    const dataFiles = [
      { fileName: 'FormData.xml', format: pdfModule.DataFormat.Xml, outputFileName: 'ImportedXMLData.pdf' },
      // { fileName: 'FormData.fdf', format: pdfModule.DataFormat.Fdf, outputFileName: 'ImportedFDFData.pdf' },
      // { fileName: 'FormData.xfdf', format: pdfModule.DataFormat.XFdf, outputFileName: 'ImportedXFDFData.pdf' },
    ];

    for (const item of dataFiles) {
      // The data file also has to be loaded into the VFS first
      await window.spire.FetchFileToVFS(item.fileName, "", `${process.env.PUBLIC_URL}/data/`);

      const doc = new pdfModule.PdfDocument();
      doc.LoadFromFile(inputFileName);

      // Read the data file and write the values back into the fields by name
      const formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
      formWidget.ImportData(item.fileName, item.format);

      doc.SaveToFile(item.outputFileName);
      doc.Close();

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Import Form Data</h1>
      <button onClick={importFormData}>
        Import
      </button>
    </div>
  );
}

export default App;

The form after the XML data has been imported:

The form after the XML data has been imported


FAQ

Some fields are still empty after import

Cause: Import matches by field name, so the names in the data file have to be exactly the same as the field names in the form, case and spaces included. A field that doesn't match is skipped outright — no error and no return value saying so; only the fields that do match get a value.

Solution: Walk the field collection first and print out the real names, then check the data file against them:

const fields = formWidget.FieldsWidget;
for (let i = 0; i < fields.Count; i++) {
  console.log(fields.get_Item({ index: i }).Name);
}

Which of the three data formats should you choose

Cause: All three hold the same field values; the difference is structure and tool support. Fdf is the smallest, starts with %FDF-, and suits passing data between form programs only; XFdf and Xml are both XML, so they can be opened and read directly and diffed with text tools, which makes them safer for moving between tools; Xml puts the field name right in the element name, the most straightforward structure of the three.

Solution: Use Fdf for round trips inside a program; use XFdf when the file goes into version control, needs a human eye, or has to talk to another system; use Xml when all you need is a readable list of field names and values.

Import throws Xml_MessageWithErrorPosition or "not a valid FDF file"

Cause: ImportData parses the file as whatever format the second parameter names, and never looks at the extension. When the content doesn't match the format, it fails at the first step: XML reports Xml_MessageWithErrorPosition, Xml_InvalidRootData, and a non-FDF file reports The source is not a valid FDF file because it does not start with "%FDF-".

Solution: Pass the DataFormat that matches the file's real format, and use the original exported data file rather than another format after re-saving it.


Get a Free License

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

A form-based PDF keeps its whole value in what was filled in, but once the file is filed away or handed over, that data is locked inside the layout. To find out what a field holds you have to open a reader and copy it out one by one; with a few dozen fields, transcribing by hand is slow and easy to get wrong. Before those values can be validated, imported into a database, or used to track an order, the program has to be able to read them out first.

This article shows how to extract form field values from an existing PDF with Spire.PDF for JavaScript: walk the field collection, determine each field's type, then read the current value of each text box, list box, combo box, radio button, and check box by type. Spire.PDF for JavaScript is built on WebAssembly and opens and parses documents in the browser, so the whole read happens locally. Files are read and written through a virtual file system (VFS), with no backend involved.

This article covers one core feature:

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.


Extract All Form Field Values

Spire.PDF for JavaScript provides PdfFormWidget to take over the form fields already present in a document; FieldsWidget is its field collection, and fields can be pulled out one by one by index. The value properties are not uniform across field types: a text box keeps its value on Text, a check box is judged by Checked, list boxes and combo boxes split into an option collection and a selected value, and a radio button is read straight from Value. So once a field is in hand, dispatch on its type and then read the matching value, writing the type name into the result alongside it — you never need to know in advance which fields the document contains.

function App() {
  const getAllFieldValues = 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 file to be read into the VFS
    const inputFileName = 'ApplicationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

    // Build a PdfFormWidget from the document's form handle; FieldsWidget is its field collection
    const formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
    const fields = formWidget.FieldsWidget;

    let report = '';

    // Walk the field collection, check each type, and read the matching value
    for (let i = 0; i < fields.Count; i++) {
      const field = fields.get_Item({ index: i });

      // Both the type name and the value are filled in by the type dispatch
      let type = 'Unknown';
      let value = '(Unrecognized field type)';

      if (field instanceof pdfModule.PdfTextBoxFieldWidget) {
        // Text box field: read Text directly
        type = 'TextBox';
        value = field.Text;
      } else if (field instanceof pdfModule.PdfListBoxWidgetFieldWidget) {
        // List box field: Values holds every option, SelectedValue is the current one
        const options = [];
        for (let j = 0; j < field.Values.Count; j++) {
          options.push(field.Values.get_Item(j).Value);
        }
        type = 'ListBox';
        value = `Selected ${field.SelectedValue}, options ${options.join(', ')}`;
      } else if (field instanceof pdfModule.PdfComboBoxWidgetFieldWidget) {
        // Combo box field: like a list box, it has an option collection and a selected value
        const options = [];
        for (let j = 0; j < field.Values.Count; j++) {
          options.push(field.Values.get_Item(j).Value);
        }
        type = 'ComboBox';
        value = `Selected ${field.SelectedValue}, options ${options.join(', ')}`;
      } else if (field instanceof pdfModule.PdfRadioButtonListFieldWidget) {
        // Radio button field: Value is the selected item
        type = 'RadioButton';
        value = `Selected ${field.Value}`;
      } else if (field instanceof pdfModule.PdfCheckBoxWidgetFieldWidget) {
        // Check box field: Checked gives the state, not Value
        type = 'CheckBox';
        value = field.Checked ? 'Checked' : 'Not checked';
      }

      report += `Field "${field.Name}" (${type}): ${value}\n`;
    }

    const outputFileName = 'AllFieldValues.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, report);
    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: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Extract Form Field Values</h1>
      <button onClick={getAllFieldValues}>
        Extract values
      </button>
    </div>
  );
}

export default App;

The values collected by walking every form field:

The values collected by walking every form field


FAQ

A check box's Value doesn't give you its state

Cause: The check box widget (PdfCheckBoxWidgetFieldWidget) has no Value property — reading it gets you undefined. A check box tracks its state through export values: Off when it is not ticked, Yes or a custom export value when it is. A string value cannot tell you whether the box is checked.

Solution: Use Checked for the state:

// Check the state with Checked, not Value
const checked = field.Checked;

Should a list box or combo box be read with SelectedValue or Values

Cause: For these two fields Values is the full option set — walking it gives you every choice, and each entry is a PdfListWidgetItem whose .Value is the option text, so the item has to be unwrapped one more time; the item the user actually selected lives on SelectedValue. Treat Values as the value and what you get is not the filled-in result.

Solution: Read SelectedValue for the current value; walk Values only when you need to show the available range:

// The text of the currently selected item
const selected = field.SelectedValue;

// Every available option
const options = [];
for (let j = 0; j < field.Values.Count; j++) {
  options.push(field.Values.get_Item(j).Value);
}

Loading an encrypted PDF throws "Can not open an encrypted document. The password is invalid."

Cause: Reading a form means the document has to open first; when the document is password-protected, LoadFromFile without the password throws during loading, and no empty document comes back.

Solution: Pass the open password as the second argument to LoadFromFile:

doc.LoadFromFile(inputFileName, 'spire123');

Get a Free License

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

Once a table has been turned into a PDF, the data is sealed up together with the layout. Send the same document to ten people and what comes back is ten separately filled-in PDFs; to move the contents of one onto a different template, the only way is to copy it off the screen one field at a time. The form fields themselves do have names and the values do hang off those names, but as soon as you leave a reader, that structure can no longer be pulled back out.

This article uses Spire.PDF for JavaScript to export the values in a form's fields to a data file, then import that data file back into a blank form. ExportData and ImportData both support Xml, Fdf, and XFdf — the three are nothing more than a difference of DataFormat enum values, called in exactly the same way, differing only in the structure of the file written out. The code below runs the whole flow with XML, with the FDF and XFDF versions listed alongside in comments; uncomment to switch. Spire.PDF for JavaScript reads and writes documents in the browser on top of WebAssembly, so the whole process happens locally, going through a virtual file system (VFS) to read and write files, 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.


Export PDF Form Data

PdfFormWidget.ExportData writes the values in a form's fields out to a single data file, with the format given by the second parameter, DataFormat. The three formats hold the same set of field values; they differ in file structure:

Data format File structure
DataFormat.Xml Adobe form data XML — the field name is the element name, the value is the element content
DataFormat.Fdf Forms Data Format (FDF) — a text structure starting with %FDF-, where /T holds the field name and /V the value
DataFormat.XFdf XFDF, standard XML — one <field name="…"> per field, with the value inside <value>

The third parameter is the form name; for an unnamed AcroForm, pass an empty string.

function App() {
  const exportFormData = 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 file to be exported into the VFS
    const inputFileName = 'CustomerInformationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

    // Build a PdfFormWidget from the document's form handle to reach the data export API
    const formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
    
    // This demo exports XML
    const dataFiles = [
      { fileName: 'FormData.xml', format: pdfModule.DataFormat.Xml },
      // { fileName: 'FormData.fdf', format: pdfModule.DataFormat.Fdf },
      // { fileName: 'FormData.xfdf', format: pdfModule.DataFormat.XFdf },
    ];

    for (const item of dataFiles) {
      // The third parameter is the form name; pass an empty string for an unnamed form
      formWidget.ExportData(item.fileName, item.format, '');
    }
    doc.Close();

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Export Form Data</h1>
      <button onClick={exportFormData}>
        Export
      </button>
    </div>
  );
}

export default App;

The exported XML form data file:

The exported XML form data file


Import PDF Form Data

PdfFormWidget.ImportData reads a data file and writes the values back into the form fields by field name; the second parameter, DataFormat, only determines how the file is parsed and has nothing to do with the file extension — the same for all three formats.

What gets imported is the blank form. The template goes out empty, and once the data files come back the values are filled in one by one — with a lot of fields there is no need to key everything in a second time.

function App() {
  const importFormData = 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 blank form to be filled into the VFS
    const inputFileName = 'BlankCustomerInformationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // This demo refills from the XML data file
    const dataFiles = [
      { fileName: 'FormData.xml', format: pdfModule.DataFormat.Xml, outputFileName: 'ImportedXMLData.pdf' },
      // { fileName: 'FormData.fdf', format: pdfModule.DataFormat.Fdf, outputFileName: 'ImportedFDFData.pdf' },
      // { fileName: 'FormData.xfdf', format: pdfModule.DataFormat.XFdf, outputFileName: 'ImportedXFDFData.pdf' },
    ];

    for (const item of dataFiles) {
      // The data file also has to be loaded into the VFS first
      await window.spire.FetchFileToVFS(item.fileName, "", `${process.env.PUBLIC_URL}/data/`);

      const doc = new pdfModule.PdfDocument();
      doc.LoadFromFile(inputFileName);

      // Read the data file and write the values back into the fields by name
      const formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
      formWidget.ImportData(item.fileName, item.format);

      doc.SaveToFile(item.outputFileName);
      doc.Close();

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Import Form Data</h1>
      <button onClick={importFormData}>
        Import
      </button>
    </div>
  );
}

export default App;

The form after the XML data has been imported:

The form after the XML data has been imported


FAQ

Some fields are still empty after import

Cause: Import matches by field name, so the names in the data file have to be exactly the same as the field names in the form, case and spaces included. A field that doesn't match is skipped outright — no error and no return value saying so; only the fields that do match get a value.

Solution: Walk the field collection first and print out the real names, then check the data file against them:

const fields = formWidget.FieldsWidget;
for (let i = 0; i < fields.Count; i++) {
  console.log(fields.get_Item({ index: i }).Name);
}

Which of the three data formats should you choose

Cause: All three hold the same field values; the difference is structure and tool support. Fdf is the smallest, starts with %FDF-, and suits passing data between form programs only; XFdf and Xml are both XML, so they can be opened and read directly and diffed with text tools, which makes them safer for moving between tools; Xml puts the field name right in the element name, the most straightforward structure of the three.

Solution: Use Fdf for round trips inside a program; use XFdf when the file goes into version control, needs a human eye, or has to talk to another system; use Xml when all you need is a readable list of field names and values.

Import throws Xml_MessageWithErrorPosition or "not a valid FDF file"

Cause: ImportData parses the file as whatever format the second parameter names, and never looks at the extension. When the content doesn't match the format, it fails at the first step: XML reports Xml_MessageWithErrorPosition, Xml_InvalidRootData, and a non-FDF file reports The source is not a valid FDF file because it does not start with "%FDF-".

Solution: Pass the DataFormat that matches the file's real format, and use the original exported data file rather than another format after re-saving it.


Get a Free License

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

Validating the page count before an upload, estimating paper before a batch print, knowing the document's size before splitting it by page — in each of these cases what you need is a single number: how many pages the document has. Getting that number in the browser isn't straightforward. A rendering library only draws the pages, so you end up counting them yourself; sidestep that layer and the file has to go to a server to be parsed.

This article shows how to get the page count of a PDF document with Spire.PDF for JavaScript. It loads and parses PDF documents directly in the browser on top of WebAssembly, so everything happens locally. Files are read and written through a virtual file system (VFS), with no backend involved. This article walks through counting the total number of pages in a document.

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


Get the Page Count of a PDF Document

Once the document is loaded, doc.Pages is its page collection, and Count gives the number of pages in that collection — the document's total page count. There is no need to loop over the pages and tally them up.

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

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

    // Pages is the document's page collection; Count is the total page count
    const pageCount = doc.Pages.Count;

    // Write the result to the VFS
    const outputFileName = 'PageCountResult.txt';
    const report = `Document: ${inputFileName}\r\nTotal pages: ${pageCount}`;
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, report);
    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: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Get PDF Page Count</h1>
      <button onClick={getPageCount}>
        Count Pages
      </button>
    </div>
  );
}

export default App;

The result is written to a text file that records the document's total page count:

The result is written to a text file that records the document's total page count


FAQ

The page number shown in a reader doesn't match the total page count

Cause: Pages.Count counts physical pages — how many pages the document actually contains. The number shown in a reader comes from page labels (/PageLabels), which can be configured separately: excluding the cover from numbering, using roman numerals for the table of contents, restarting the body at 1. As a result, the 5th physical page may display as iii or 1 in a reader.

Solution: If what you need is the displayed number, read the page's own PageLabel instead of Count:

// What label the 5th physical page displays in a reader
const page = doc.Pages.get_Item(4);
console.log(page.PageLabel);

When the document has no page labels set, PageLabel returns an empty string — in that case the displayed number matches the physical page order, so Count is the answer.

Loading an encrypted PDF to count pages throws "Can not open an encrypted document. The password is invalid."

Cause: The document is protected by an open password, and without it LoadFromFile cannot read the content at all, so the page count is out of reach as well. This check happens at load time — it is thrown before Pages.Count is ever read.

Solution: Pass the open password as the second argument to LoadFromFile, then read the page count:

// The second argument is the open password
doc.LoadFromFile(inputFileName, 'spire123');
const pageCount = doc.Pages.Count;

Using Count as a loop bound throws an index-out-of-range error on the last page

Cause: Pages is zero-indexed, so the valid range is 0 to Count - 1. When the loop is written as i <= doc.Pages.Count, the final pass tries to fetch page number Count, and get_Item throws an out-of-range error — underneath it is .NET's ArgumentOutOfRangeException, which the WASM runtime wraps as a JS Error with the message ArgumentOutOfRange_IndexMustBeLess Arg_ParamName_Name, index. The error's name is just the generic Error, so match on that message.

Solution: Use < in the loop condition:

// The upper bound is Count - 1, so use < rather than <=
for (let i = 0; i < doc.Pages.Count; i++) {
  const page = doc.Pages.get_Item(i);
}

Get a Free License

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

Formulas, chemical formulas, unit symbols, and footnote markers all carry superscripts and subscripts: a² + b² = c², H₂SO₄, 30m². Laying them out in a PDF is not a matter of drawing one line of plain text — the raised characters have to be smaller than the body text with their baseline lifted or dropped, and being a few points off is enough to break the expression apart.

This article shows how to use Spire.PDF for JavaScript to draw superscripts and subscripts on PDF pages. 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 two 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 Superscripts and Subscripts

Whether text is raised or lowered comes from SubSuperScript on PdfStringFormat: PdfSubSuperScript.SuperScript lifts the baseline and shrinks the glyphs, while SubScript drops it. It applies to the whole run drawn by one DrawString call, so the body text and the marker are drawn as two runs, with the second anchored at the width measured by MeasureString.

function App() {
  const drawSuperAndSubScript = 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_Black() }) });

    // Subscript: draw the body text as usual
    let text = 'The formula of water is H';
    page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 110 });

    // Measure the body text so the subscript lands right after its right edge
    let x = 40 + font.MeasureString({ text: text }).Width;

    // SubSuperScript set to SubScript shrinks this run and drops it below the baseline
    const subFormat = new pdfModule.PdfStringFormat();
    subFormat.SubSuperScript = pdfModule.PdfSubSuperScript.SubScript;
    page.Canvas.DrawString({ s: '2', font: font, brush: brush, x: x, y: 110, format: subFormat });

    // Superscript: also two runs, with the anchor continuing after the body text
    text = 'The mass-energy equation is E = mc';
    page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 170 });
    x = 40 + font.MeasureString({ text: text }).Width;

    // SubSuperScript set to SuperScript shrinks this run and lifts it above the baseline
    const superFormat = new pdfModule.PdfStringFormat();
    superFormat.SubSuperScript = pdfModule.PdfSubSuperScript.SuperScript;
    page.Canvas.DrawString({ s: '2', font: font, brush: brush, x: x, y: 170, format: superFormat });

    // Define the output file name and save the document
    const outputFileName = 'SuperscriptAndSubscript.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 Superscripts and Subscripts</h1>
      <button onClick={drawSuperAndSubScript}>
        Draw
      </button>
    </div>
  );
}

export default App;

The subscript and the superscript drawn with the two SubSuperScript values:

The subscript and the superscript drawn with the two SubSuperScript values


Control the Scale and Offset of Superscripts and Subscripts

SubSuperScript sets a whole run to one form, so superscripts and subscripts that alternate with body text inside a line have to be laid out by hand: measure how wide the run you just drew is with MeasureString, advance x, then switch to a smaller font and offset y in points. You pick the size and the lift — nothing is bound to the ratio the library has built in.

function App() {
  const drawInlineSuperAndSubScript = 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 brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Black() }) });

    // Body text at 16 points; the markers get their own size, 10 points here
    const BASE_SIZE = 16;
    const MARK_SIZE = 10;
    const baseFont = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: BASE_SIZE });
    const markFont = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: MARK_SIZE });

    // Superscripts lift 8 points, subscripts drop 6; the y axis points down, so a lift is a subtraction
    const SUPER_RISE = 8;
    const SUB_SINK = 6;

    // Draw run by run: after each run, measure it with its own font and push x to its right edge
    const writeRuns = (runs, lineY) => {
      let x = 40;
      runs.forEach((run) => {
        const runFont = run.kind === 'base' ? baseFont : markFont;
        // The anchor is the top-left of the run and the baseline sits one font size below it,
        // so a smaller run makes up that difference first
        const baselineFix = run.kind === 'base' ? 0 : BASE_SIZE - MARK_SIZE;
        const riseFix = run.kind === 'super' ? -SUPER_RISE : run.kind === 'sub' ? SUB_SINK : 0;
        const runY = lineY + baselineFix + riseFix;
        page.Canvas.DrawString({ s: run.s, font: runFont, brush: brush, x: x, y: runY });
        x += runFont.MeasureString({ text: run.s }).Width;
      });
    };

    // Formula: a² + b² = c²
    writeRuns([
      { s: 'a', kind: 'base' }, { s: '2', kind: 'super' },
      { s: ' + b', kind: 'base' }, { s: '2', kind: 'super' },
      { s: ' = c', kind: 'base' }, { s: '2', kind: 'super' },
    ], 110);

    // Chemical formula: H₂SO₄
    writeRuns([
      { s: 'H', kind: 'base' }, { s: '2', kind: 'sub' },
      { s: 'SO', kind: 'base' }, { s: '4', kind: 'sub' },
    ], 170);

    // Define the output file name and save the document
    const outputFileName = 'InlineSuperscriptAndSubscript.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>Control the Scale and Offset of Superscripts and Subscripts</h1>
      <button onClick={drawInlineSuperAndSubScript}>
        Draw
      </button>
    </div>
  );
}

export default App;

Body text and smaller markers joined up run by run, one line of formula and one of chemical formula:

Body text and smaller markers joined up run by run, one line of formula and one of chemical formula


FAQ

Why did the whole line turn into a superscript

Cause: SubSuperScript lives on PdfStringFormat, and it applies to every character drawn by that one DrawString call, not to a few of them. Put the body text and the superscript into the same call and both are shrunk and raised together.

Solution: split it into two calls — the body text with no format, the marker on its own with the format, anchored at the width measured by MeasureString:

// Body text, no format
page.Canvas.DrawString({ s: 'E = mc', font: font, brush: brush, x: 40, y: 170 });

// Measure the body text, then set only the marker run as a superscript
const x = 40 + font.MeasureString({ text: 'E = mc' }).Width;
const superFormat = new pdfModule.PdfStringFormat();
superFormat.SubSuperScript = pdfModule.PdfSubSuperScript.SuperScript;
page.Canvas.DrawString({ s: '2', font: font, brush: brush, x: x, y: 170, format: superFormat });

Hand-placed superscripts and subscripts do not line up, or leave a gap

Cause: The anchor is the top-left corner of the run, not the baseline — the baseline sits one font size below that corner, so shrinking the font also shortens the drop to the baseline. Hand a superscript an offset worked out from the body text's anchor and the baseline shift you get is not the one you asked for: the smaller marker rises further than intended, while a subscript can end up sitting right on the baseline. Advance x using a width measured with the wrong font and the runs either overlap or leave a gap.

Solution: Turn the baseline shift you want back into an anchor — anchor plus font size gives the baseline — and measure the width with the font you actually draw with:

// The baseline of the body text sits one font size below its anchor
const baselineY = lineY + BASE_SIZE;

// To lift the superscript 8 points, the anchor is the target baseline minus its own font size
const superY = baselineY - 8 - MARK_SIZE;
page.Canvas.DrawString({ s: '2', font: markFont, brush: brush, x: x, y: superY });

// Measure with the font that actually draws the run
x += markFont.MeasureString({ text: '2' }).Width;

There is no parameter for making the superscript larger or smaller

Cause: PdfStringFormat exposes SubSuperScript as a switch only. The scale factor and the lift are fixed by the library, with no public value to adjust.

Solution: lay the markers out by hand when you need a different ratio — draw them with a smaller font object and set the offset in points yourself (see feature 2).


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.

Page 3 of 8