Import and Export PDF Form Data Using JavaScript in React
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:

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:

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.
Get the Page Count of a PDF Document in React Using JavaScript
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:

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.
Draw Superscripts and Subscripts in PDF Using JavaScript in React
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:

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:

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.
Add, Fill, and Delete PDF Form Fields Using JavaScript in React
Registration forms, sign-up sheets, and questionnaires go out as blank PDFs and come back needing to be filled in by hand, one file at a time. The forms themselves change too — a missing text box, an extra checkbox nobody uses anymore — and every change means opening desktop software like Acrobat. That is endurable for one or two files; in bulk it is nothing but manual clicking.
This article shows how to add, fill, and delete PDF form fields 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 and reads and writes files through a virtual file system (VFS), with no backend involved.
This article covers three core features:
For installation and project configuration, see Integrate Spire.PDF for JavaScript into a React Project. The examples below assume Spire.PDF is installed and the WebAssembly module is initialized.
Add Form Fields
Spire.PDF for JavaScript provides a complete set of form field classes covering text boxes, check boxes, radio buttons, combo boxes, list boxes, buttons, and signature fields. They all work the same way: create an instance on the page, position it with Bounds, and hand it to doc.Form.Fields.Add(). When you load an existing document, set doc.AllowCreateForm to true first.
| Class | Description |
|---|---|
PdfTextBoxField |
Text box field |
PdfCheckBoxField |
Check box field |
PdfRadioButtonListField |
Radio button field |
PdfComboBoxField |
Combo box field |
PdfListBoxField |
List box field |
PdfButtonField |
Button field |
PdfSignatureField |
Signature field |
Bounds, BorderWidth, BorderStyle, Required, ReadOnly, Visible, and ToolTip are shared by every field type.
function App() {
const addFormFields = 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 into the VFS
const inputFileName = 'BlankRegistrationForm.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);
// Form creation must be enabled explicitly for an existing document
doc.AllowCreateForm = true;
let page = doc.Pages.get_Item(0);
let uiFont = new pdfModule.PdfFont({
fontFamily: pdfModule.PdfFontFamily.Helvetica,
size: 10
});
const box = (x, y, width, height) => new pdfModule.RectangleF({ x, y, width, height });
const border = 0.75;
// 1. Text box: name
let nameBox = new pdfModule.PdfTextBoxField(page, 'name');
nameBox.Bounds = box(178, 168, 210, 20);
nameBox.BorderWidth = border;
nameBox.BorderStyle = pdfModule.PdfBorderStyle.Solid;
nameBox.Font = uiFont;
doc.Form.Fields.Add(nameBox);
// 2. Text box: email
let emailBox = new pdfModule.PdfTextBoxField(page, 'email');
emailBox.Bounds = box(178, 206, 210, 20);
emailBox.BorderWidth = border;
emailBox.BorderStyle = pdfModule.PdfBorderStyle.Solid;
emailBox.Font = uiFont;
doc.Form.Fields.Add(emailBox);
// 3. Combo box: department
let departmentBox = new pdfModule.PdfComboBoxField(page, 'department');
departmentBox.Bounds = box(178, 244, 210, 20);
departmentBox.BorderWidth = border;
departmentBox.Font = uiFont;
['Engineering', 'Marketing', 'Sales', 'Support'].forEach(function (item) {
departmentBox.Items.Add(new pdfModule.PdfListFieldItem({ text: item, value: item.toLowerCase() }));
});
doc.Form.Fields.Add(departmentBox);
// 4. Radio buttons: gender, each option is a PdfRadioButtonListItem
let genderBox = new pdfModule.PdfRadioButtonListField(page, 'gender');
['male', 'female'].forEach(function (value, index) {
let item = new pdfModule.PdfRadioButtonListItem();
item.Bounds = box(185.5 + index * 110, 285.5, 13, 13);
item.BorderWidth = border;
item.Value = value;
genderBox.Items.Add(item);
});
doc.Form.Fields.Add(genderBox);
// 5. List box: education
let educationBox = new pdfModule.PdfListBoxField(page, 'education');
educationBox.Bounds = box(178, 320, 210, 52);
educationBox.BorderWidth = border;
educationBox.Font = uiFont;
['Bachelor', 'Master', 'Doctor'].forEach(function (item) {
educationBox.Items.Add(new pdfModule.PdfListFieldItem({ text: item, value: item.toLowerCase() }));
});
doc.Form.Fields.Add(educationBox);
// 6. Check box: agree to terms
let agreeBox = new pdfModule.PdfCheckBoxField(page, 'agree_terms');
agreeBox.Bounds = box(178, 392, 15, 15);
agreeBox.BorderWidth = border;
agreeBox.Style = pdfModule.PdfCheckBoxStyle.Check;
agreeBox.Required = true;
doc.Form.Fields.Add(agreeBox);
// 7. Signature field: reserve a place to sign
let signatureBox = new pdfModule.PdfSignatureField(page, 'signature');
signatureBox.Bounds = box(178, 424, 210, 40);
doc.Form.Fields.Add(signatureBox);
// 8. Button: submit
let submitButton = new pdfModule.PdfButtonField(page, 'submit');
submitButton.Bounds = box(72, 478, 90, 26);
submitButton.Text = 'Submit';
submitButton.HighlightMode = pdfModule.PdfHighlightMode.Push;
doc.Form.Fields.Add(submitButton);
const outputFileName = 'AddFormFields.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 Form Fields</h1>
<button onClick={addFormFields}>
Add fields
</button>
</div>
);
}
export default App;
The blank registration form now carries a text box, check box, radio buttons, combo box, list box, button, and signature field:

Fill Form Fields
Filling an existing form means working from the widget side: PdfFormWidget wraps the document's form, FieldsWidget hands out the field instances one by one, and after checking the type you cast to the matching subclass to write a value — Text for text boxes, Checked for check boxes, SelectedIndex for combo boxes. Fields are told apart by Name, so a single pass fills the whole form.
function App() {
const fillFormFields = 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 filled into the VFS
const inputFileName = 'EmployeeRegistrationForm.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);
// Get the widget collection from the form
let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
let field = formWidget.FieldsWidget.get_Item({ index: i });
// Text box: assign Text directly
if (field instanceof pdfModule.PdfTextBoxFieldWidget) {
switch (field.Name) {
case 'name':
field.Text = 'Jane Doe';
break;
case 'email':
field.Text = '[email protected]';
break;
}
}
// Combo box: SelectedIndex takes an array of indices
if (field instanceof pdfModule.PdfComboBoxWidgetFieldWidget) {
if (field.Name === 'department') {
field.SelectedIndex = [1];
}
}
// Check box: set Checked to true to tick it
if (field instanceof pdfModule.PdfCheckBoxWidgetFieldWidget) {
if (field.Name === 'agree_terms') {
field.Checked = true;
}
}
}
const outputFileName = 'FillFormFields.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>Fill Form Fields</h1>
<button onClick={fillFormFields}>
Fill form
</button>
</div>
);
}
export default App;
The text box, combo box, and check box have each been given their values:

Delete Form Fields
Deleting also starts from FieldsWidget: locate the target instance by Name, then call Remove() to take it out of the field collection. Locating by field name is safer than by index — a document whose layout has changed will not lose the wrong field.
function App() {
const deleteFormField = 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 into the VFS
const inputFileName = 'EmployeeRegistrationForm.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);
let form = doc.Form;
if (form != null) {
let formWidget = new pdfModule.PdfFormWidget(form.H);
// Locate the target field by name and remove it
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
let field = formWidget.FieldsWidget.get_Item({ index: i });
if (field.Name === 'name') {
formWidget.FieldsWidget.Remove(field);
break;
}
}
}
const outputFileName = 'DeleteFormField.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>Delete Form Fields</h1>
<button onClick={deleteFormField}>
Delete field
</button>
</div>
);
}
export default App;
The Name text box has been removed from the form:

FAQ
New form fields cannot be clicked in the reader
Cause: AllowCreateForm defaults to false. When you open an existing document with LoadFromFile, Spire.PDF keeps the document's original form structure and does not allow fields to be appended, so doc.Form.Fields.Add() throws no error but the saved document has no new fields.
Fix: Turn the property on after loading the document and before adding fields:
doc.LoadFromFile(inputFileName);
doc.AllowCreateForm = true;
Fields are still empty after filling
Cause: The field name in the case branch is not character-for-character identical to the real name in the document. PDF field names are case-sensitive and keep leading and trailing spaces, so a name such as company_name (with a trailing space) never matches company_name written in code. Another common mistake is assigning values to objects taken from doc.Form.Fields — those fields have no widget properties such as Text.
Fix: Print every field name before filling and copy from the output. Assignments must land on the *FieldWidget instances that PdfFormWidget returns:
let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}
Deleting one field makes the fields after it disappear too
Cause: Remove() changes FieldsWidget.Count immediately, and every field after the removed element shifts up by one position. If you iterate forward and remove inside the loop, the next iteration has already skipped past the element that moved into that slot.
Fix: break after a single removal; to delete several, collect the targets by name and process them one by one, or iterate backwards from the end:
// Iterate backwards and remove every field whose name starts with temp_
for (let i = formWidget.FieldsWidget.Count - 1; i >= 0; i--) {
let field = formWidget.FieldsWidget.get_Item({ index: i });
if (field.Name.startsWith('temp_')) {
formWidget.FieldsWidget.Remove(field);
}
}
Get a Free License
If you want to remove the evaluation message from the result documents, or to get rid of the feature limitations, contact sales for a temporary license valid for 30 days.
How to Flatten PDF Form Fields with JavaScript in React
Archiving is usually the next step after a form is filled in. PDF form fields are exactly where that falls apart: the file looks complete, but the controls are still live, so the recipient can edit an amount, a date or a signature box, and some viewers re-validate the form as it opens. Turning that file into something nobody can change means pressing the controls and their values into the page content.
This article uses Spire.PDF for JavaScript to flatten PDF form fields. It runs on WebAssembly, loading, modifying and saving documents in the browser, and reads and writes 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 has been initialized.
Flatten the Whole Form
Spire.PDF for JavaScript provides the PdfForm.IsFlatten property to flatten an entire form in one pass. After it is set to true, every field in the document is converted into static page content together with its current value, and the saved PDF has no interactive controls left. The text stays in the text layer, so it can still be selected, copied and searched.
function App() {
const flattenWholeForm = 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 processed into the VFS
const inputFileName = 'EmployeeRegistrationForm.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);
// Flatten the entire form in one pass
doc.Form.IsFlatten = true;
const outputFileName = 'FlattenWholeForm.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>Flatten the whole form</h1>
<button onClick={flattenWholeForm}>
Flatten now
</button>
</div>
);
}
export default App;
Every input box disappears and the values remain on the page as plain text:

Flatten a Selected Field
Spire.PDF for JavaScript also provides the PdfField.Flatten property for field-level flattening. Take the field instance by Name from the FieldsWidget collection of a PdfFormWidget; only the field you pick is fixed and the rest stay editable — freezing an email address that has already been verified, for example, while leaving the date column for the recipient to fill in.
function App() {
const flattenSelectedField = 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 processed into the VFS
const inputFileName = 'EmployeeRegistrationForm.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);
// Get the widget collection from the form
let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
// Pick the target field by name and flatten only that one
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
let field = formWidget.FieldsWidget.get_Item({ index: i });
if (field.Name === 'email') {
field.Flatten = true;
}
}
const outputFileName = 'FlattenSelectedField.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>Flatten a selected field</h1>
<button onClick={flattenSelectedField}>
Flatten now
</button>
</div>
);
}
export default App;
Only the email input box disappears; the remaining fields are still editable controls:

Frequently Asked Questions
The fields are still clickable in the viewer after setting IsFlatten
Reason: The code mixes up two field collections. For an AcroForm produced by another tool, doc.Form.Fields frequently reads back no fields at all — in this article's sample document Count reads 0, and calling get_Item() on it throws ArgumentOutOfRange_IndexMustBeLess. Even when it does return entries, those PdfField objects have no control properties such as Text or Checked, so changing them leaves the page untouched.
Solution: Use doc.Form.IsFlatten alone to flatten the whole form; it does not depend on any collection. As soon as you work field by field — selecting by name, writing a value, flattening one field — go through PdfFormWidget:
let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}
How do I tell whether a PDF has already been flattened
Reason: PdfForm.IsFlatten is a write instruction, not a state flag. Reload the flattened output and doc.Form.IsFlatten still reads false — in testing the document already had 0 fields at that point.
Solution: Check the field count instead; a FieldsWidget.Count of 0 means no interactive controls are left:
let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
const hasFormFields = formWidget.FieldsWidget.Count > 0;
Only the selected field was flattened, but its value was not baked in
Reason: Name is compared character by character, so it is case-sensitive and keeps leading and trailing spaces. Write company_name when the document actually has company_name (with a trailing space) and the loop never matches — and it raises no error, it just saves the file unchanged.
Solution: Print every field name first and copy from that output. Both the comparison and the assignment have to run on the *FieldWidget instance that FieldsWidget returns:
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}
Get a Free License
If you want to remove the evaluation message from the result document or lift the feature limits, contact our sales team for a temporary license valid for 30 days.
Draw Text in PDF Documents Using JavaScript in React
Adding text to a PDF usually sits at the end of a generation pipeline: document numbers, review comments, annotations, or a line of pale text laid over a chart. Typing it in by hand is fine for a few pages, but once the text has to follow the data — a number that changes per copy, a note angled into the page corner, pale text sitting on top of a chart — manual layout stops keeping up.
This article shows how to use Spire.PDF for JavaScript to draw text on PDF pages, including text filled with a gradient, text laid out inside a rectangle, and text that is rotated, transformed, or semi-transparent. It runs on WebAssembly to create and save PDF documents directly in the browser, doing all the work locally and reading and writing files through a virtual file system (VFS) with no backend involved.
This article covers four core features:
For installation and project configuration, see Integrate Spire.PDF for JavaScript into a React Project. The examples below assume Spire.PDF is installed and the WebAssembly module is initialized.
Draw Text with a Color Gradient
The color of text comes from the brush passed to DrawString. Swap in a gradient brush — PdfLinearGradientBrush — and the glyphs transition from one color to another along a given direction: mode sets the direction, and the rect on the brush bounds where the gradient starts and ends.
function App() {
const drawGradientText = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the font into the VFS
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Create a PDF document and add a blank page
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 24 });
const text = 'Gradient Text';
// Measure this line of text so the gradient spans exactly its width
const textWidth = font.MeasureString({ text: text }).Width;
// Horizontal gradient: from red to blue
const gradient = new pdfModule.PdfLinearGradientBrush({
rect: new pdfModule.RectangleF({ x: 40, y: 90, width: textWidth, height: 40 }),
color1: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Red() }),
color2: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }),
mode: pdfModule.PdfLinearGradientMode.Horizontal,
});
// Align the text anchor with the left edge of the gradient rectangle so both colors sweep the whole line
page.Canvas.DrawString({ s: text, font: font, brush: gradient, x: 40, y: 110 });
// Define the output file name and save the document
const outputFileName = 'GradientText.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger a download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Draw Text with a Color Gradient</h1>
<button onClick={drawGradientText}>
Draw
</button>
</div>
);
}
export default App;
When the gradient rectangle matches the text width, red to blue sweeps across the entire line:

Draw Text Laid Out Inside a Rectangle
DrawString takes either a pair of coordinates or a layout rectangle, layoutRectangle. With a rectangle, the text wraps to the width of the box on its own, so you don't have to work out where each line breaks. Combined with alignment and lineAlignment from PdfStringFormat, you get more control over how the text is aligned.
function App() {
const drawTextInRectangle = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the font into the VFS for the page text
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Create a PDF document and add a blank page
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 14 });
const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Black() }) });
const borderPen = new pdfModule.PdfPen({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_LightGray() }), width: 1 });
const text = 'This is a longer paragraph of explanatory text. Handed to the rectangle, it wraps to the box width on its own.';
// Left box: left-aligned wrapping by default, text starts at the top-left corner of the box
const leftBox = new pdfModule.RectangleF({ x: 40, y: 80, width: 200, height: 100 });
page.Canvas.DrawRectangle({ pen: borderPen, rectangle: leftBox });
page.Canvas.DrawString({ s: text, font: font, brush: brush, layoutRectangle: leftBox });
// Right box: the same text, centered horizontally and vertically inside the box
const rightBox = new pdfModule.RectangleF({ x: 300, y: 80, width: 200, height: 100 });
page.Canvas.DrawRectangle({ pen: borderPen, rectangle: rightBox });
const center = new pdfModule.PdfStringFormat({
alignment: pdfModule.PdfTextAlignment.Center,
lineAlignment: pdfModule.PdfVerticalAlignment.Middle,
});
page.Canvas.DrawString({ s: text, font: font, brush: brush, layoutRectangle: rightBox, format: center });
// Define the output file name and save the document
const outputFileName = 'TextInRectangle.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger a download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Draw Text Laid Out Inside a Rectangle</h1>
<button onClick={drawTextInRectangle}>
Draw
</button>
</div>
);
}
export default App;
The same paragraph wraps inside a 200-point-wide box, while the right box adds horizontal and vertical centering:

Draw Rotated and Transformed Text
Rotation and deformation of text come from the canvas rather than from font parameters: you turn the canvas first, then draw on it. Four methods cover the common cases:
| API | Effect | Parameters and units |
|---|---|---|
TranslateTransform(dx, dy) |
Moves the canvas origin to the target position | Offset in points |
RotateTransform({ angle }) |
Rotates around the canvas origin | Angle; a positive value is clockwise on this canvas |
SkewTransform(angleX, angleY) |
Skews the axes so text runs along a slanted line | Skew angle; with (-20, 0) the right end of the line lifts |
ScaleTransform(scaleX, scaleY) |
Scales the canvas by a factor | Scale factors for both axes; (1, 0.6) compresses vertically to 0.6 |
The four transforms applied to the same sample line of text (gray is before the transform, red is after, and the dot marks the anchor point):

function App() {
const drawTransformedText = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the font into the VFS for the page text
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Create a PDF document and add a blank page
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 16 });
const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_SteelBlue() }) });
// Translation: move only the origin to the anchor point, shifting the text as a whole
let state = page.Canvas.Save();
page.Canvas.TranslateTransform(60, 110);
page.Canvas.DrawString({ s: 'Translated text', font: font, brush: brush, x: 0, y: 0 });
page.Canvas.Restore({ state: state });
// Rotation: move the origin to the anchor point, then rotate 30°
state = page.Canvas.Save();
page.Canvas.TranslateTransform(120, 210);
page.Canvas.RotateTransform({ angle: 30 });
page.Canvas.DrawString({ s: 'Rotated 30° text', font: font, brush: brush, x: 0, y: 0 });
page.Canvas.Restore({ state: state });
// Skew: horizontal shear of -20°, lifting the right end of the line
state = page.Canvas.Save();
page.Canvas.TranslateTransform(60, 430);
page.Canvas.SkewTransform(-20, 0);
page.Canvas.DrawString({ s: 'Horizontally skewed text', font: font, brush: brush, x: 0, y: 0 });
page.Canvas.Restore({ state: state });
// Transform: compress vertically to 0.6, squashing the glyphs
state = page.Canvas.Save();
page.Canvas.TranslateTransform(60, 560);
page.Canvas.ScaleTransform(1, 0.6);
page.Canvas.DrawString({ s: 'Vertically compressed text', font: font, brush: brush, x: 0, y: 0 });
page.Canvas.Restore({ state: state });
// Define the output file name and save the document
const outputFileName = 'TransformText.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger a download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Draw Rotated and Transformed Text</h1>
<button onClick={drawTransformedText}>
Transform
</button>
</div>
);
}
export default App;
Text drawn with four canvas transforms: translation, rotation, horizontal skew, and vertical compression:

Draw Semi-Transparent Text
SetTransparency is set on the canvas: alphaBrush and alphaPen control how transparent the fill and the stroke are, taking a decimal between 0 and 1 (0 fully transparent, 1 opaque), and blendMode decides how the text composites with what lies underneath. It takes effect for everything drawn from the moment it is set, so wrap it in Save and Restore — otherwise the content that follows fades as well.
function App() {
const drawTransparentText = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the font into the VFS for the page text
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Create a PDF document and add a blank page
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 20 });
const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_SeaGreen() }) });
const text = 'Spire.PDF Semi-Transparent Text';
// First line: opaque, as a reference
page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 90 });
// Turn on transparency: both the fill and the stroke alpha are set to 0.3
const state = page.Canvas.Save();
page.Canvas.SetTransparency({ alphaPen: 0.3, alphaBrush: 0.3, blendMode: pdfModule.PdfBlendMode.Normal });
page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 140 });
// Restore the canvas state so drawing outside this block goes back to opaque
page.Canvas.Restore({ state: state });
page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 190 });
// Define the output file name and save the document
const outputFileName = 'TransparentText.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger a download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Draw Semi-Transparent Text</h1>
<button onClick={drawTransparentText}>
Draw
</button>
</div>
);
}
export default App;
The same line drawn three times: opaque, at alpha 0.3, and opaque again after Restore:

FAQ
Text lands outside the page, or its vertical position is reversed
Cause: The canvas origin sits at the top-left corner of the page (inset by the page margins), the y axis points down, and the unit is the point. The anchor marks the top-left corner of the line, not the text baseline. Working from the assumption that the origin is at the bottom-left with the y axis pointing up puts the text on the opposite side, or even outside the drawable area — the sample page is A4 with 40-point top and bottom margins, leaving a usable height of 762 points, and a line drawn below roughly y = 740 points is clipped.
Solution: Lay the text out from the top-left corner with y increasing downwards. To measure from the bottom of the page instead, subtract from the usable height:
// Height of the drawable area (762 points for A4 with 40-point margins)
const height = page.Canvas.ClientSize.Height;
// Draw 100 points above the bottom of the drawable area
page.Canvas.DrawString({ s: 'Text near the bottom of the page', font: font, brush: brush, x: 40, y: height - 100 });
Gradient text shows only a single color, or the transition is incomplete
Cause: The rect on a PdfLinearGradientBrush bounds where the gradient starts and ends, in absolute canvas coordinates, independent of the text anchor. When the rectangle does not cover the whole line, the text only lands on one segment of the gradient, which looks like a flat color. In a test with the rectangle starting at x = 0 and the text anchored at x = 40, the left edge of the text was already a third of the way through the gradient, and the red-to-blue transition was no longer complete.
Solution: Use MeasureString to get the text width, then make the rectangle the same width as the text and align its start with the anchor point, so the gradient sweeps across the whole line:
// Use the text width as the width of the gradient rectangle
const textWidth = font.MeasureString({ text: text }).Width;
const gradient = new pdfModule.PdfLinearGradientBrush({
rect: new pdfModule.RectangleF({ x: 40, y: 90, width: textWidth, height: 40 }),
color1: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Red() }),
color2: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }),
mode: pdfModule.PdfLinearGradientMode.Horizontal,
});
// Align the anchor with the left edge of the rectangle
page.Canvas.DrawString({ s: text, font: font, brush: gradient, x: 40, y: 110 });
Get a Free License
If you want to remove the evaluation message from the resulting documents, or lift the feature limits, contact sales for a temporary license valid for 30 days.
Find and Highlight Text in PDF Documents Using JavaScript in React
A contract or a report of a few dozen pages lands on your desk, and you need to confirm where a certain clause or amount appears and how many times. Going through it page by page by eye is easy to get wrong. Marking the hits is the least effort, but find-and-highlight in a desktop application is hard to fit into a web workflow, and screenshotting every page to annotate it is not realistic either.
Spire.PDF for JavaScript loads, processes and saves PDF documents in the browser on WebAssembly, so finding and highlighting happen entirely locally, reading and writing files through a virtual file system (VFS) with no backend involved. This article uses PdfTextFinder to implement three ways of finding and highlighting: the whole document, a given area, and a regular expression.
This article covers three core features:
- Find and Highlight All Matches
- Find and Highlight Within an Area
- Find and Highlight by Regular Expression
For installation and project configuration, see Integrate Spire.PDF for JavaScript into a React Project. The examples below assume Spire.PDF is installed and the WebAssembly module is initialized.
Find and Highlight All Matches
PdfTextFinder locates given text in the text layer of a page. It works page by page: build one finder for each page of the document and you can find every match across the whole file at once. Each hit is highlighted by calling HighLight(), which is yellow by default; pass a color when different keywords have to be told apart.
function App() {
const findAndHighlightAll = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the PDF file to be processed into the VFS
const inputFileName = 'Flowers.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the PDF document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Search page by page and highlight every hit
for (let i = 0; i < doc.Pages.Count; i++) {
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(i));
finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
const finds = finder.Find('Ornamental');
for (let j = 0; j < finds.length; j++) {
finds.get(j).HighLight();
}
}
// Save the document
const outputFileName = 'FindAndHighlight.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Find and Highlight All Matches</h1>
<button onClick={findAndHighlightAll}>
Find and Highlight
</button>
</div>
);
}
export default App;
Every Ornamental in the document is highlighted:

Find and Highlight Within an Area
Text that reads the same on a page is often only partly worth marking. PdfTextFinder also provides Options.Area, which narrows the search to a rectangle; matches that fall outside it are not returned, and therefore not highlighted. The rectangle is described in page coordinates, with the origin at the top-left corner of the page and units in points.
function App() {
const findAndHighlightInArea = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the PDF file to be processed into the VFS
const inputFileName = 'Flowers.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the PDF document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Set the search range: page coordinates, origin at the top-left, units in points
const area = new pdfModule.RectangleF({ x: 60, y: 488, width: 420, height: 160 });
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(0));
finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
finder.Options.Area = area;
// Only matches that fall inside the rectangle are returned
const finds = finder.Find('Ornamental');
for (let j = 0; j < finds.length; j++) {
finds.get(j).HighLight();
}
// Save the document
const outputFileName = 'HighlightInArea.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Find and Highlight Within an Area</h1>
<button onClick={findAndHighlightInArea}>
Find and Highlight
</button>
</div>
);
}
export default App;
Only the Ornamental inside the comparison table is highlighted; the body text and the list stay as they are:

Find and Highlight by Regular Expression
The target is not necessarily a fixed set of characters. Options.Parameter decides the matching rule; set it to Regex and the argument to Find() becomes a regular expression, so targets that share a shape but differ in content can be circled in one pattern. The default value matches by substring, which is what the previous two sections do, and the same enumeration also offers IgnoreCase and WholeWord.
function App() {
const findAndHighlightByRegex = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the PDF file to be processed into the VFS
const inputFileName = 'Flowers.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the PDF document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Match page by page with a regular expression and highlight every hit
for (let i = 0; i < doc.Pages.Count; i++) {
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(i));
finder.Options.Parameter = pdfModule.TextFindParameter.Regex;
const finds = finder.Find('Figure\\s*\\d');
for (let j = 0; j < finds.length; j++) {
finds.get(j).HighLight({ color: pdfModule.Color.get_Orange() });
}
}
// Save the document
const outputFileName = 'HighlightByRegex.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Find and Highlight by Regular Expression</h1>
<button onClick={findAndHighlightByRegex}>
Find and Highlight
</button>
</div>
);
}
export default App;
The three figure captions are matched by the pattern and highlighted in orange:

FAQ
The highlight works, but it is missing from the reader's comments panel
Cause: HighLight() writes into the page content, not into PDF annotations. The highlight block is written to the page's content stream when the document is saved, so the file grows by roughly 2 KB, and no annotation object is added to the output — reading it back with PyMuPDF gives an empty set from page.annots().
Fix: Treat the highlight as page graphics. It displays the same way as an annotation; it simply has no annotation identity, so it cannot be selected, deleted or recolored one by one in the reader. When highlights have to be managed as annotations, record the hit positions before saving and keep that list on the application side.
The search area is set, but nothing is highlighted
Cause: Options.Area uses page coordinates (origin at the top-left corner of the page, units in points). If the rectangle is too small or mispositioned, every match falls outside it. It also takes effect on the current page only — in a multi-page document, apply the same rectangle to the finder of the page you want.
Fix: Measure the target region in whole-page coordinates first, then narrow it down. In the sample below the comparison table falls within x≈60–480 and y≈488–648, so RectangleF({ x: 60, y: 488, width: 420, height: 160 }) frames it exactly, and the body text and list outside the rectangle are not matched:
// Search the current page only, and match inside this rectangle only
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(0));
finder.Options.Area = new pdfModule.RectangleF({ x: 60, y: 488, width: 420, height: 160 });
When the coordinates are uncertain, run the search once without Area and read the actual position from each hit's finds.get(i).Bounds[0] to work back to the rectangle.
The same regex matches a Chinese document but fails on a Japanese one
Cause: A regular expression matches the actual characters in the PDF text layer, not meanings. The dash that separates the ranges is not the same across the three samples: Chinese and English use – (U+2013), while Japanese uses the full-width tilde ~ (U+FF5E), so a pattern with only one of them hits only one kind of document.
Fix: Put the dashes in a character class so that both spellings work at once:
// Number ranges: 6–9 / 1–3 m / 6~9月 all match
finder.Options.Parameter = pdfModule.TextFindParameter.Regex;
const finds = finder.Find('[0-9]+\\s*[–-~]\\s*[0-9]+');
Get a Free License
If you want to remove the evaluation message from the result document, or get past the feature limits, contact sales for a 30-day temporary license.
Detect PDF Page Orientation and Rotation with JavaScript in React
Before printing, imposing or watermarking pages, you need to know whether each page is landscape or portrait: a document that mixes the two leaves blank edges when printed directly, and lands out of place when imposed. A page may also have been rotated as a whole, in which case the recorded page size no longer matches the direction you see. Picking those pages out before batch processing means flipping through them one at a time in desktop software.
This article shows how to detect PDF page rotation and display orientation with Spire.PDF for JavaScript. It is built on WebAssembly and loads and parses PDF documents directly in the browser, all locally, reading and writing files through a virtual file system (VFS) with no backend involved.
Two features are covered:
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 is initialized.
Detect page rotation
PdfPageBase.Rotation reads the angle a page has been rotated by; the value falls within the PdfPageRotateAngle enumeration of 0°, 90°, 180° and 270°.
function App() {
const detectPageRotation = 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 inspect into the VFS
const inputFileName = 'PageOrientationSample.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);
// The enumeration values are indices rather than angles, so map indices to degrees first
const DEGREES = {
[pdfModule.PdfPageRotateAngle.RotateAngle0.value]: 0,
[pdfModule.PdfPageRotateAngle.RotateAngle90.value]: 90,
[pdfModule.PdfPageRotateAngle.RotateAngle180.value]: 180,
[pdfModule.PdfPageRotateAngle.RotateAngle270.value]: 270,
};
// Read the rotation angle page by page
const lines = [];
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
lines.push(`Page ${i + 1}: rotation angle ${DEGREES[page.Rotation.value]}°`);
}
// Write the detection result into the VFS
const outputFileName = 'PageRotationResult.txt';
window.dotnetRuntime.Module.FS.writeFile(outputFileName, lines.join('\r\n'));
doc.Close();
// Read the generated file back 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>Detect Page Rotation</h1>
<button onClick={detectPageRotation}>
Start Detection
</button>
</div>
);
}
export default App;
Detection result with the rotation angle recorded page by page:

Detect page orientation
Whether a page is landscape or portrait cannot be told from its size alone: PdfPageBase.Size gives the width and height of the visible page box and does not include the rotation; when a page is rotated by 90° or 270°, the width and height have to be swapped to get the direction the page actually shows.
function App() {
const detectPageOrientation = 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 inspect into the VFS
const inputFileName = 'PageOrientationSample.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);
const { RotateAngle90, RotateAngle270 } = pdfModule.PdfPageRotateAngle;
// Judge the display orientation page by page
const lines = [];
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
const pageSize = page.Size;
// Size excludes rotation: at 90° or 270° the width and height swap to give the displayed size
const quarterTurn = page.Rotation === RotateAngle90 || page.Rotation === RotateAngle270;
const width = quarterTurn ? pageSize.Height : pageSize.Width;
const height = quarterTurn ? pageSize.Width : pageSize.Height;
const orientation = width >= height ? 'Landscape' : 'Portrait';
lines.push(`Page ${i + 1}: ${orientation} (${width.toFixed(0)} × ${height.toFixed(0)} pt)`);
}
// Write the detection result into the VFS
const outputFileName = 'PageOrientationResult.txt';
window.dotnetRuntime.Module.FS.writeFile(outputFileName, lines.join('\r\n'));
doc.Close();
// Read the generated file back 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>Detect Page Display Orientation</h1>
<button onClick={detectPageOrientation}>
Start Detection
</button>
</div>
);
}
export default App;
Detection result after judging each page by its display orientation:

FAQ
page.Rotation.value returns 0, 1, 2, 3 instead of 0, 90, 180, 270
Cause: The four members of PdfPageRotateAngle — RotateAngle0, RotateAngle90, RotateAngle180, RotateAngle270 — carry the values 0, 1, 2, 3 in the WebAssembly binding, and .value returns that index rather than the angle. Writing page.Rotation.value === 90 is always false.
Solution: Build an index-to-angle map before using the value. Rotation is corrected with an index too — page.Rotation takes an index, so passing 90 is read as index 2 and writes 180° into the document:
// Index → angle
const DEGREES = {
[pdfModule.PdfPageRotateAngle.RotateAngle0.value]: 0,
[pdfModule.PdfPageRotateAngle.RotateAngle90.value]: 90,
[pdfModule.PdfPageRotateAngle.RotateAngle180.value]: 180,
[pdfModule.PdfPageRotateAngle.RotateAngle270.value]: 270,
};
// Set the current page to 90° — .value is the index
page.Rotation = pdfModule.PdfPageRotateAngle.RotateAngle90.value;
page.Size returns portrait dimensions for a page that clearly shows as landscape
Cause: PdfPageBase.Size and ActualSize return the width and height of the page's visible box — the CropBox if one was set, otherwise the MediaBox — and that value does not follow /Rotate. Page 3 of the sample document has a page box of 595 × 842 pt with /Rotate at 90°, so it renders as an 842 × 595 landscape page while Size still returns 595 × 842.
Solution: After reading Rotation, swap the width and height yourself:
const { RotateAngle90, RotateAngle270 } = pdfModule.PdfPageRotateAngle;
const quarterTurn = page.Rotation === RotateAngle90 || page.Rotation === RotateAngle270;
const width = quarterTurn ? page.Size.Height : page.Size.Width;
const height = quarterTurn ? page.Size.Width : page.Size.Height;
doc.Sections is empty in a loaded PDF, and PageSettings.Orientation cannot be read
Cause: Sections holds page settings, and LoadFromFile does not rebuild sections for pages that already exist, so doc.Sections.Count is 0 and a following doc.Sections.get_Item(0) throws Arg_IndexOutOfRangeException. PageSettings.Orientation describes the layout intent for a newly created page, not the direction recorded in the file.
Solution: For existing pages, read page-level properties only — doc.Pages.get_Item(i) followed by Rotation and Size. Leave PageSettings.Orientation for new pages added through Sections.Add():
// Inspecting existing pages
const page = doc.Pages.get_Item(0);
const angle = DEGREES[page.Rotation.value];
// Orientation only comes into play when creating a new page
const section = doc.Sections.Add();
section.PageSettings.Orientation = pdfModule.PdfPageOrientation.Landscape;
Get a Free License
If you want to remove the evaluation message from the result documents, or lift the feature limits, contact sales for a 30-day temporary license.
Crop PDF Pages with JavaScript in React
Scanned pages, drawings and electronic invoices often carry a wide white margin while the page size stays at its original spec; the opposite also happens, where only a small block of a page matters and the rest does not need to appear. Removing the extra part used to mean framing each page by hand in desktop software, or sending the file to a server — the first is hard to fit into a web workflow, the second means the document leaves the user's device.
This article shows how to crop PDF pages with Spire.PDF for JavaScript. It is built on WebAssembly and loads, modifies and saves PDF documents directly in the browser, all locally, reading and writing files through a virtual file system (VFS) with no backend involved. The steps are demonstrated on a two-page sample 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 is initialized.
Crop a PDF Page
Cropping a page is done through page.CropBox, a RectangleF whose x and y are measured from the top-left corner of the page, while width and height decide how much is kept; content outside the box no longer shows up. Cropping the whole document by one uniform margin means walking doc.Pages and, for each page, giving up the distance on all four sides of its MediaBox.
function App() {
const cropPdfPage = 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 cropped into the VFS
const inputFileName = 'ToCrop.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);
// Trim 60 points off every side
const margin = 60;
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
// MediaBox gives the full extent of the page, from which the crop box is derived
// (x and y are measured from the top-left corner of the page)
const width = page.MediaBox.Width;
const height = page.MediaBox.Height;
page.CropBox = new pdfModule.RectangleF({
x: margin,
y: margin,
width: width - margin * 2,
height: height - margin * 2,
});
}
const outputFileName = 'CropByMargins.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>Crop PDF Pages</h1>
<button onClick={cropPdfPage}>
Crop PDF
</button>
</div>
);
}
export default App;
Both pages are cropped by a 60-point margin, so the white margin and the frame around the page are removed:

FAQ
Can I crop a single page instead of the whole document
Why: CropBox is a page-level property; there is no "whole document" interface. The loop above exists only so that every page gets the same margin.
Solution: To crop one page only, drop the loop and assign to the target page directly. x and y are likewise measured from the top-left corner of the page:
// Crop page 1 only: keep a 400 x 500 point block starting at (80, 80) from the top-left corner
const page = doc.Pages.get_Item(0);
page.CropBox = new pdfModule.RectangleF({ x: 80, y: 80, width: 400, height: 500 });
How do I undo a crop
Why: CropBox changes the page box in place, and the document keeps no record of the original one. The instinct is to assign page.MediaBox back, but that has no effect — the coordinates are applied on top of the origin of the current visible area, so only the size changes and the origin stays put. After a 60-point crop, assigning MediaBox back still leaves the visible area starting at (60, 60).
Solution: If the original document is still around, loading it again is the simplest route. When only the cropped file is left, offset the origin back to the top-left corner with negative values and give the full page size:
// Undo when the crop offset was (offsetX, offsetY)
page.CropBox = new pdfModule.RectangleF({
x: -offsetX,
y: -offsetY,
width: page.MediaBox.Width,
height: page.MediaBox.Height,
});
The file didn't get smaller and the cropped content is still searchable
Why: CropBox is a soft crop — it only changes the visible box of the page, while content outside it stays in the file and can still be picked up by text search or copy.
Solution: If the goal is to actually remove the content from the page, setting CropBox is not enough; the page has to be rebuilt — create a new document of the same size, take the content out with page.CreateTemplate(), draw it onto a new page, and save to a new file.
Get a Free License
If you want to remove the evaluation message from the result document, or lift the feature limitations, contact sales for a temporary license valid for 30 days.
Copy PDF Pages in React with JavaScript
Adding the cover of a product manual to the front of a project description, merging pages 2 and 3 of a quotation into a contract, combining several reports into one summary — all of it comes down to moving pages from one PDF into another. Desktop software means opening two windows and dragging back and forth, and one wrong drop means starting over; with more pages, the resulting order is easy to get wrong. A different kind of trouble is mismatched page sizes: the cover is A5 and the target document is A4, so moving the page across as-is leaves a band of white space around it.
Spire.PDF for JavaScript loads, modifies and saves PDF documents directly in the browser based on WebAssembly, so the whole copying process runs locally and reads and writes files through a virtual file system (VFS), with no backend service required.
This article covers four core features. The first three move whole pages, carrying the source page's size, rotation and margins over as they are; the fourth takes only the page content, and how large a page it is drawn onto is up to you.
Move whole pages
- Copy a Single Page to a Specific Position
- Copy a Range of Pages to the End of a Document
- Copy Every Page of a Whole Document
Copy content (templates)
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.
Copy a Single Page to a Specific Position
Spire.PDF for JavaScript provides the PdfDocument.InsertPage method, which copies a page from another document into the current one and lets you choose which position it lands at. Leave the target index out and the page is appended to the end.
function App() {
const copyPageAtPosition = 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 both the source and the target document into the VFS
const sourceFileName = 'SourceDocument.pdf';
const targetFileName = 'TargetDocument.pdf';
await window.spire.FetchFileToVFS(sourceFileName, "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS(targetFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Load the two documents
const sourceDoc = new pdfModule.PdfDocument();
sourceDoc.LoadFromFile(sourceFileName);
const targetDoc = new pdfModule.PdfDocument();
targetDoc.LoadFromFile(targetFileName);
// Copy page 1 of the source document to the front of the target document
// pageIndex comes from the source document, resultPageIndex is where the copy lands
targetDoc.InsertPage({ ldDoc: sourceDoc, pageIndex: 0, resultPageIndex: 0 });
// Save the result document
const outputFileName = 'CopyPageAtPosition.pdf';
targetDoc.SaveToFile(outputFileName);
sourceDoc.Close();
targetDoc.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>Copy Page at Position</h1>
<button onClick={copyPageAtPosition}>
Start
</button>
</div>
);
}
export default App;
resultPageIndexis the only place among the four features where you control where the page lands: 0 puts it first, 1 puts it second, and passing the current page count is the same as appending it.
Page 1 of the source document now sits in front of the target document, which goes from 2 pages to 3:

Copy a Range of Pages to the End of a Document
Spire.PDF for JavaScript also provides the PdfDocument.InsertPageRange method, which copies a run of consecutive pages from the source document. It takes the source document plus a start and an end index, has no parameter for a target position, and always appends the result to the end of the current document.
function App() {
const appendPageRange = 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 both the source and the target document into the VFS
const sourceFileName = 'SourceDocument.pdf';
const targetFileName = 'TargetDocument.pdf';
await window.spire.FetchFileToVFS(sourceFileName, "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS(targetFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Load the two documents
const sourceDoc = new pdfModule.PdfDocument();
sourceDoc.LoadFromFile(sourceFileName);
const targetDoc = new pdfModule.PdfDocument();
targetDoc.LoadFromFile(targetFileName);
// Append pages 2 to 3 of the source document to the end of the target document
// Note: these are positional arguments, not an object; endIndex is inclusive
targetDoc.InsertPageRange(sourceDoc, 1, 2);
// Save the result document
const outputFileName = 'CopyPageRange.pdf';
targetDoc.SaveToFile(outputFileName);
sourceDoc.Close();
targetDoc.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>Copy Page Range</h1>
<button onClick={appendPageRange}>
Copy pages 2-3
</button>
</div>
);
}
export default App;
After pages 2 and 3 of the source document are appended, the document has 4 pages:

Copy Every Page of a Whole Document
When an entire document has to move, there is no need to work out the indices first: PdfDocument.AppendPage takes the source document object and appends all of its pages to the end of the current document in their original order. For merging several documents or attaching material to a report, just pass the documents in one after another.
function App() {
const appendWholeDocument = 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 both the source and the target document into the VFS
const sourceFileName = 'SourceDocument.pdf';
const targetFileName = 'TargetDocument.pdf';
await window.spire.FetchFileToVFS(sourceFileName, "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS(targetFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Load the two documents
const sourceDoc = new pdfModule.PdfDocument();
sourceDoc.LoadFromFile(sourceFileName);
const targetDoc = new pdfModule.PdfDocument();
targetDoc.LoadFromFile(targetFileName);
// Use AppendPage when the whole document has to be copied; all pages are appended in order
targetDoc.AppendPage({ doc: sourceDoc });
// Save the result document
const outputFileName = 'CopyAllPages.pdf';
targetDoc.SaveToFile(outputFileName);
sourceDoc.Close();
targetDoc.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>Copy Whole Document</h1>
<button onClick={appendWholeDocument}>
Start
</button>
</div>
);
}
export default App;
After all 4 pages of the source document are appended, the document has 6 pages:

Copy Page Content with a Page Template
Spire.PDF for JavaScript also provides the PdfPageBase.CreateTemplate method, which takes the content of one page as a PdfTemplate that Canvas.DrawTemplate then draws onto a newly created page. The first three methods move whole pages, so the new page inherits the source page's size; a template takes the content instead, leaving the page size, the drawing position and the number of times you draw it entirely up to you — the same template can be reused again and again.
function App() {
const copyPageWithTemplate = 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 work on into the VFS
const inputFileName = 'SourceDocument.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Load the document
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Take the page to be reused and turn it into a template: read the content once, draw it many times
const sourcePage = doc.Pages.get_Item(0);
const template = sourcePage.CreateTemplate();
// First placement: insert an A4 page at position 2, a different size from the source,
// and draw the content scaled to 297.6 x 421.6 at (80, 80)
const page1 = doc.Pages.Insert(1, new pdfModule.SizeF(595.0, 842.0), new pdfModule.PdfMargins({ margin: 0.0 }));
page1.Canvas.DrawTemplate(template, new pdfModule.PointF(80.0, 80.0), new pdfModule.SizeF(297.6, 421.6));
// Second placement: insert another A4 page, drawing the same template smaller in the lower right
const page2 = doc.Pages.Insert(2, new pdfModule.SizeF(595.0, 842.0), new pdfModule.PdfMargins({ margin: 0.0 }));
page2.Canvas.DrawTemplate(template, new pdfModule.PointF(320.0, 460.0), new pdfModule.SizeF(200.0, 283.3));
// Save the result document
const outputFileName = 'CopyPageWithTemplate.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>Copy Page with Template</h1>
<button onClick={copyPageWithTemplate}>
Start
</button>
</div>
);
}
export default App;
When the third argument of DrawTemplate is omitted, the template is drawn at its original coordinates without scaling — the new page is larger than the source, so the content occupies only one corner of it. Both the size of the new page and its margins come from Pages.Insert; in the example the margins are 0 on all four sides, which is what makes the drawing origin the top-left corner of the page.
The content of page 1 in the source document is scaled onto two new A4 pages, taking the document from 4 pages to 6:

Frequently Asked Questions
Creating a page with new PdfMargins(0.0) throws Arg_NullReferenceException
Cause: The PdfMargins constructor treats a single numeric argument as an internal handle, so new pdfModule.PdfMargins(0.0) does not give you a margins object — reading its Left or Top throws Arg_NullReferenceException, and using it to create a page does not produce the margins you expect.
Solution: Pass the margins as an object; for 0 on all four sides write { margin: 0.0 }:
// Zero margins on all four sides
const margins = new pdfModule.PdfMargins({ margin: 0.0 });
// Or set each side separately
const custom = new pdfModule.PdfMargins({ left: 20.0, top: 20.0, right: 20.0, bottom: 20.0 });
An out-of-range or reversed-range error is thrown when copying pages
Cause: Page indices start at 0 and endIndex is inclusive, so the valid range is 0 to Pages.Count - 1. Going outside it throws Index out of range, and a startIndex greater than endIndex throws The start index is greater then the end index.
Solution: Clamp the upper bound with Pages.Count before passing it in:
// To copy pages 2 to 4: start = 1, end = 3, with the page count as the upper bound
const start = 1;
const end = Math.min(3, sourceDoc.Pages.Count - 1);
targetDoc.InsertPageRange(sourceDoc, start, end);
A rotated page comes out with the wrong orientation after copying
Cause: CreateTemplate() takes the page content, and the page's rotation angle (/Rotate) is not part of the template. When the source page is rotated, the template's coordinate system no longer lines up with the target page — drawing it straight onto a new page with DrawTemplate puts the content outside the visible area, and the copy's Rotation is 0.
Solution: When the source page is rotated, use a whole-page copy instead; the content and the rotation angle travel together:
// Whole-page copy: the rotation angle comes with the page
targetDoc.InsertPage({ ldDoc: sourceDoc, pageIndex: 0, resultPageIndex: 1 });
If the template approach is required, temporarily zero the source page's rotation before taking the template, then restore the angle on both the source page and the copy:
const rotation = sourcePage.Rotation.value;
// Zero it temporarily so the template exports at the page's real coordinates
sourcePage.Rotation = 0;
const newPage = doc.Pages.Insert(1, sourcePage.Size, new pdfModule.PdfMargins({ margin: 0.0 }));
newPage.Canvas.DrawTemplate(sourcePage.CreateTemplate(), new pdfModule.PointF(0.0, 0.0));
// Restore the source page and give the copy the same angle
sourcePage.Rotation = rotation;
newPage.Rotation = rotation;
Get a Free License
If you would like to remove the evaluation message from the result documents or lift the feature limits, contact sales for a temporary 30-day license.