Knowledgebase (2597)
Children categories
Batch Payslip Splitting and PDF Distribution with Spire.Agent.Office
2026-09-21 02:26:59 Written by liu taliaIn HR and finance departments, distributing payslips is a monthly task that is small in substance yet remarkably tedious. A payroll sheet is usually an Excel table arranged by rows — each row is one employee, while the columns hold base salary, performance bonus, overtime pay, social insurance deduction, housing fund, individual income tax and net pay. But when the sheet reaches employees, each person should only see their own row. So "one sheet" must first be "split into N copies", and then each copy sent out one by one.
The traditional approach is copy-and-paste row by row inside the spreadsheet: create a new file, paste the header once, paste one employee's row into it, save as PDF, rename it, then loop to the next person. A team of a few dozen people means a few dozen repetitions; at a hundred people it is not only slow but highly prone to missed sends, wrong sends, or sending one person's salary to someone else — and payroll is exactly the information that must never be wrong.
The Excel AI capability of Spire.Agent.Office lets you describe the splitting rules in natural language. The AI agent understands the request and completes two kinds of work automatically: splitting one payroll sheet into individual payslip PDFs, one per employee, and applying a unified layout template to each payslip and encrypting it per person, producing files that are ready to distribute — along with a distribution ledger you can consult at any time.
Comparison with Traditional SDK API
| Traditional Spire.Office for .NET API | Spire.Agent.Office | |
|---|---|---|
| Driving style | Write complete code that loops over data rows, creates workbooks, copies headers and data and exports PDFs, controlling every step | Describe the splitting and distribution requirements in natural language; the AI understands them and orchestrates the execution path |
| Code size | Batch splitting and export usually needs 200-400 lines of C# (data reading, row loops, worksheet creation, style copying, page setup, PDF export, file naming, encryption parameters, etc.) | About 10 lines of calling code + 1 natural-language instruction |
| Layout handling | Copy header styles and set column widths and print areas by hand, or every payslip comes out misformatted | The AI recognizes and reuses the borders, alignment and column widths of the original header |
| Passwords and ledger | Generate each file's password yourself and register it separately; producing a ledger needs extra export logic | State the password rule in one sentence; the AI applies it and also outputs an Excel distribution ledger |
| Requirement change | Change a splitting rule / naming / encryption rule → change code → compile → redeploy | Modify the instruction and it takes effect immediately |
This article shows how to use Spire.Agent.Office to split an Excel payroll sheet into individual payslip PDFs. Two cases demonstrate the two typical uses — "splitting the payroll sheet directly" and "applying a template, encrypting and distributing":
- Case 1: Batch-split a payroll sheet into per-employee payslip PDFs
- Case 2: Apply a payslip template and encrypt to produce distributable files
For product installation and SpireToken configuration, please refer to Integrating Spire.Agent.Office in a .NET Project. The examples below assume that Spire.Agent.Office has been installed and SpireToken has been configured.
Case 1: Batch-split a payroll sheet into per-employee payslip PDFs
This is the most direct scenario: all you have is a raw payroll sheet, with no extra template file. The first row is the header and each following row holds one employee's pay data for the month. The task is to split the sheet by row, generate a payslip for each employee that contains only their own information, and save it named "employeeID_name" for later distribution or archiving.
The pain of doing this by hand is the repetition: the more employees there are, the more copy-and-paste cycles are needed, and the further you get the easier it is for rows to slip — leaving the previous person's net pay in the next person's file. That is a class of error that is hard to notice and serious in its consequences.
The example below uses the Spire.Agent.Office agent to read every employee record in the payroll sheet through a natural-language instruction, split it row by row into individual payslips and export them as PDFs:
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Xls;
// Excel-related configuration
string inputPath = @"E:\payroll.xlsx"; // path to the payroll sheet to split
string savePath = null; // result path (null here: the output folder below will be used)
string OutDir = @"E:\output"; // output directory (the split payslip PDFs are saved here)
string key = "**************************"; // SpireToken key
string instruction =
"Read the employee payroll data in the payroll sheet and generate an individual payslip for each employee:" +
"1. The first row is the header and each following row is one employee; extract the employee ID, name, department and each pay item row by row;" +
"2. At the top of each payslip show the title 'Payslip' and the pay month, and list that employee's ID, name and department, then list each pay item in the source column order: base salary, performance bonus, overtime pay, social insurance deduction, housing fund, individual income tax and net pay;" +
"3. Generate a separate PDF file for each employee, named 'employeeID_name' and saved to the output directory;" +
"4. Keep the borders and column widths of the original header, right-align the amount columns and keep two decimal places;" +
"5. Make the page size fit the payslip content, with tight margins and a height that adapts to the content, leaving no large blank areas;" +
"Save the final result as a PDF file";
// Call the Excel document processing function
AIResult result = ExecuteDemoExcel(instruction, inputPath, savePath, key, OutDir);
// Execute Excel AI processing
static AIResult ExecuteDemoExcel(string instruction, string inputPath, string savePath, string key, string output)
{
// Create the AIOptions configuration object
AIOptions options = new AIOptions();
options.WorkDir = output; // set the working directory to the output directory
options.SpireToken = key; // set the SpireToken key
// Process the Excel document using a Workbook object
using (Workbook workbook = new Workbook())
{
// Load the Excel payroll sheet from file
if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
{
workbook.LoadFromFile(inputPath);
}
// Create the AI document processor
AIDocumentProcessor processor = workbook.AI(options);
// Execute the AI instruction
return processor.ExecuteInstruction(workbook, instruction, savePath);
}
}
The Excel payroll sheet to be split
The individual payslip PDFs produced by the split

Case 2: Apply a payslip template and encrypt to produce distributable files
When a company distributes payslips, two more requirements usually come up: a unified layout and confidentiality. Companies normally have a fixed payslip template — with a company letterhead, the pay month, the order of pay items and explanatory text at the bottom. At the same time, salary is sensitive information: sending a plain-text PDF directly means that once it is forwarded or misdelivered, the employee's pay is exposed.
The safer approach is to apply the same template to every payslip and then set an individual open password on each employee's PDF (for example, generated from their own employee ID), so that only that person can open and view their payslip.
But once the passwords "all differ", a new management problem appears: who records them, and where do you look them up afterwards. Each password is built from "employee ID + 4 random digits", and those random digits cannot be reconstructed from the file name or any other information. After dozens or hundreds of files have gone out, if an employee reports that a file will not open, the only fallback is to dig out the original payroll sheet and try one by one. So the complete loop of encrypted distribution should also produce a distribution ledger — recording file names, employee details and open passwords in one-to-one correspondence, for later distribution, reconciliation and archive lookups.
This case takes the payslip template as the input document, while the employee payroll data is passed in as an attachment. The AI needs to fill the attachment data into the corresponding positions of the template and output that ledger alongside the encrypted PDFs.
The example below uses the Spire.Agent.Office agent to read the employee data in the attached payroll sheet through a natural-language instruction, fill it into the payslip template row by row, generate a separate PDF for each employee, encrypt it with "employee ID + 4 random digits", and additionally produce an Excel-format "Payslip Distribution Ledger":
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Xls;
// Path to the employee payroll data file (passed in as an attachment)
string[] attachmentPaths = new string[]
{
@"E:\payroll.xlsx"
};
// Excel-related configuration
string inputPath = @"E:\template-payslip.xlsx"; // path to the payslip template file
string savePath = null; // result path (null here: the output folder below will be used)
string OutDir = @"E:\output-encrypted"; // output directory (encrypted payslip PDFs and the distribution ledger are saved here)
string key = "**************************"; // SpireToken key
string instruction =
"Read the employee payroll data from the attached 'payroll.xlsx' and process it as follows:" +
"1. The first row is the header and each following row is one employee; read the employee ID, name, department and each pay item row by row;" +
"2. Fill each row of data into the corresponding positions of the payslip template, keeping the template's company letterhead, notes and item order unchanged;" +
"3. Generate a separate PDF file for each employee, named 'employee ID + employee name' and saved to the output directory;" +
"4. Set an open password on each PDF as 'employee ID + 4 random digits'; it only restricts opening the document and does not affect printing or copying;" +
"5. Also generate an Excel-format 'Payslip Distribution Ledger' in the output directory, listing the pay month, employee ID, name, department, payslip file name and the corresponding open password in each row, for later distribution and archive lookups;" +
"6. Keep the layout, fonts and column widths of the template;" +
"Save the final result as a PDF file";
// Call the Excel document processing function
AIResult result = ExecuteDemoExcel(instruction, inputPath, savePath, key, OutDir, attachmentPaths);
// Execute Excel AI processing
static AIResult ExecuteDemoExcel(string instruction, string inputPath, string savePath, string key, string output, string[] attachmentPaths)
{
// Create the AIOptions configuration object
AIOptions options = new AIOptions();
options.WorkDir = output; // set the working directory to the output directory
options.SpireToken = key; // set the SpireToken key
// Process the Excel document using a Workbook object
using (Workbook workbook = new Workbook())
{
// Load the payslip template from file
if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
{
workbook.LoadFromFile(inputPath);
}
// Create the AI document processor
AIDocumentProcessor processor = workbook.AI(options);
// Execute the AI instruction
return processor.ExecuteInstruction(workbook, instruction, savePath, attachmentPaths);
}
}
The payslip template file
Template-applied, encrypted payslip PDFs and the payslip distribution ledger

FAQ
The split payslips have slipped rows or misaligned data
Cause: The payroll sheet contains blank rows, merged cells or subtotal rows, so when the AI reads by row it counts non-employee rows as records and everything after them shifts.
Solution: Make sure the first row of the payroll sheet is the header and every row after it corresponds to exactly one employee, with no blank or subtotal rows in between. If the table structure is complex, state the rule in the instruction, for example "treat a row as an employee record only when the 'Employee ID' column is not empty".
An encrypted payslip will not open even for me
Cause: Each payslip's password is generated as "employee ID + 4 random digits", so it differs per person, and the random digits cannot be worked out on your own.
Solution: State the password rule explicitly in the instruction (for example, "the password is the employee ID plus 4 random digits") and make sure the generated passwords are recorded in the distribution ledger — once a random number is lost, the file can no longer be opened. Before distributing, try opening one payslip using a record from the ledger to verify.
Getting a SpireToken Key
- Contact sales@e-iceblue.com or visit https://www.e-iceblue.com/TemLicense.html to obtain a trial/commercial API key
Configure it in your code:
AIOptions options = new AIOptions();
options.SpireToken = key;
A chart plots averages, and an average says nothing about how steady the underlying readings were. The same rising line can sit on top of six near-identical measurements or six wildly scattered ones -- the curve alone will not tell you which. Error bars supply that missing layer: a short segment drawn at each data point marks the spread or uncertainty around it, so the reader can see how much confidence each turn of the line deserves. Percentage error bars derive their range from a fixed proportion of the value, while standard error bars derive theirs from the data itself, and the two suit different situations. Spire.XLS for JavaScript performs both directly in the browser through WebAssembly, managing input and output files with a virtual file system (VFS) and requiring no backend service.
This article covers two key features:
For installation and project setup, see Integrating Spire.XLS for JavaScript in a React Project. The examples below assume Spire.XLS is installed and the WebAssembly module has been initialised.
Add percentage error bars to a line chart
A percentage error bar takes the value of each data point as its base and works out the range from the given percentage. Set it to 10, and a point valued at 4.0 gets an error amount of 0.4; which side of the data point that length is drawn on is decided by a separate parameter. This kind of error bar expresses a tolerance -- a production target allowed to drift by a tenth, or an instrument reading with a fixed relative precision. Error bars belong to a series, so the series object has to be fetched first and its ErrorBar method called on it. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook and get the first worksheet.
- Add a line chart whose data range is the planned output column.
- Use the month column as the category labels.
- Add a percentage error bar to the series, setting its direction and amount, then save the workbook.
The complete code example below shows how to add percentage error bars to a line chart in React:
function App() {
const addPercentageErrorBar = async () => {
// Get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// Check if the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// Load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ErrorBarChartData.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// Load the workbook and get the first worksheet
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// Add a line chart whose data range is the planned output column
const chart = sheet.Charts.Add({ chartType: xlsModule.ExcelChartType.Line });
chart.ChartTitle = "Planned Output";
chart.ChartTitleArea.IsBold = true;
chart.ChartTitleArea.Size = 12;
chart.DataRange = sheet.Range.get("B1:B7");
chart.SeriesDataFromRange = false;
// Set the chart position on the worksheet
chart.TopRow = 9;
chart.BottomRow = 26;
chart.LeftColumn = 1;
chart.RightColumn = 9;
// Get the series and use the month column as its category labels
const serie = chart.Series.get(0);
serie.CategoryLabels = sheet.Range.get("A2:A7");
// Add a percentage error bar to the series: plus direction, 10% of the value
serie.ErrorBar({
bIsY: true,
include: xlsModule.ErrorBarIncludeType.Plus,
type: xlsModule.ErrorBarType.Percentage,
numberValue: 10.0,
});
// Save the workbook
const outputFileName = "PercentageErrorBar.xlsx";
workbook.SaveToFile({ fileName: outputFileName });
// Dispose of the workbook object to free resources
workbook.Dispose();
// Read the result file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Percentage Error Bars</h1>
<button onClick={addPercentageErrorBar}>Start</button>
</div>
);
}
export default App;
After running, the effect of adding percentage error bars to a line chart:

Add standard error bars to a column chart
A standard error bar takes no percentage from you; it computes the standard error of the series itself, so a series with scattered values automatically gets longer error bars. That is the division of labour between the two kinds: the standard error bar describes how steady the data is, the percentage one describes how far you are willing to let it drift. Column charts are usually there to compare several groups side by side, and hanging a standard error bar on each group shows at a glance which one's readings cluster more tightly. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook and get the first worksheet.
- Add a column chart covering both the planned and the actual output columns.
- Add a standard error bar to each of the two series.
- Save the workbook.
The complete code example below shows how to add standard error bars to a column chart in React:
function App() {
const addStandardErrorBar = async () => {
// Get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// Check if the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// Load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ErrorBarChartData.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// Load the workbook and get the first worksheet
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// Add a column chart covering both the planned and the actual output columns
const chart = sheet.Charts.Add({ chartType: xlsModule.ExcelChartType.ColumnClustered });
chart.ChartTitle = "Planned vs. Actual Output";
chart.ChartTitleArea.IsBold = true;
chart.ChartTitleArea.Size = 12;
chart.DataRange = sheet.Range.get("B1:C7");
chart.SeriesDataFromRange = false;
// Set the chart position on the worksheet
chart.TopRow = 9;
chart.BottomRow = 26;
chart.LeftColumn = 1;
chart.RightColumn = 9;
// Get the first series and use the month column as its category labels
const serie = chart.Series.get(0);
serie.CategoryLabels = sheet.Range.get("A2:A7");
// Draw a standard error bar on the first series, minus direction
serie.ErrorBar({
bIsY: true,
include: xlsModule.ErrorBarIncludeType.Minus,
type: xlsModule.ErrorBarType.StandardError,
numberValue: 0.3,
});
// Draw a standard error bar on the second series as well, both directions
const serie2 = chart.Series.get(1);
serie2.ErrorBar({
bIsY: true,
include: xlsModule.ErrorBarIncludeType.Both,
type: xlsModule.ErrorBarType.StandardError,
numberValue: 0.5,
});
// Save the workbook
const outputFileName = "StandardErrorBar.xlsx";
workbook.SaveToFile({ fileName: outputFileName });
// Dispose of the workbook object to free resources
workbook.Dispose();
// Read the result file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Standard Error Bars</h1>
<button onClick={addStandardErrorBar}>Start</button>
</div>
);
}
export default App;
After running, the effect of adding standard error bars to a column chart:

FAQ
Which directions do error bars support?
The direction comes from the include parameter of ErrorBar, which takes three values from ErrorBarIncludeType:
| Value | Effect |
|---|---|
Both |
Both directions, a segment drawn above and below the data point |
Minus |
Negative only, drawn below the data point |
Plus |
Positive only, drawn above the data point |
// Draw it in both directions
serie.ErrorBar({
bIsY: true,
include: xlsModule.ErrorBarIncludeType.Both,
type: xlsModule.ErrorBarType.Percentage,
numberValue: 10.0,
});
Direction only decides which side of the data point the error bar is drawn on, and does not change its length. The same parameters give the same error amount in all three directions.
Which error amount types are supported?
The error amount type comes from the type parameter of ErrorBar, which takes five values from ErrorBarType:
| Type | Description |
|---|---|
Fixed |
A fixed value, given by numberValue |
Percentage |
A percentage of each data point's value |
StandardDeviation |
A standard deviation, given by numberValue |
StandardError |
A standard error, computed from the series data; numberValue plays no part |
Custom |
Custom ranges,needs a different overload |
The first four take their amount in numberValue:
serie.ErrorBar({
bIsY: true,
include: xlsModule.ErrorBarIncludeType.Both,
type: xlsModule.ErrorBarType.StandardDeviation,
numberValue: 2,
});
Get a Free License
Spire.XLS for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.
Converting a Word document to HTML preserves the original paragraph structure, styles, and images while rendering directly in the browser, which makes it widely useful for online preview, content publishing, and full-text search. Spire.Doc for JavaScript performs this conversion entirely in the browser via WebAssembly, using a virtual file system (VFS) to manage input and output files — no backend server required.
This article covers two core features:
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume Spire.Doc is installed and the WebAssembly module is initialized.
Convert Word to HTML
Converting Word to HTML involves three stages: first, load the font file and the target Word file into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the file, use HtmlExportOptions to specify that both CSS and images are output in embedded form, and call SaveToFile to save the document as HTML; finally, read the generated HTML file from VFS, wrap it as a Blob, and trigger a browser download.
function App() {
const wordToHtml = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Check if the module is ready
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load fonts and the Word file into VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
const inputFileName = 'ToHtml.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// Load the Word document
const wordDocument = new docModule.Document();
wordDocument.LoadFromFile(inputFileName);
// Embed the CSS styles into the HTML and embed images as Base64
wordDocument.HtmlExportOptions.CssStyleSheetType = docModule.CssStyleSheetType.Internal;
wordDocument.HtmlExportOptions.ImageEmbedded = true;
// Convert the document to HTML
const outputFileName = 'ToHtml-result.html';
wordDocument.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Html });
// Read the converted file from VFS and trigger download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = window.document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
// Release resources
wordDocument.Dispose();
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Convert Word To HTML</h1>
<button onClick={wordToHtml}>
Generate
</button>
</div>
);
}
export default App;
HTML page generated from a Word document via SaveToFile

Convert Word to HTML with export options
The output in the previous section is a single HTML file with CSS and images embedded in it. When a document is large, or when you want to maintain styles centrally and reuse image resources, you usually need to export CSS and images as separate files. HtmlExportOptions provides the corresponding settings, allowing HTML, style sheets, and images to be output separately.
The conversion flow is similar to the previous section, except that the result is a directory: you need to create the directory in VFS first, then use properties such as CssStyleSheetFileName and ImagesPath to specify where each type of resource is stored. Once conversion is complete, read that directory recursively, package everything into a zip, and download it in one go.
import JSZip from 'jszip';
function App() {
const wordToHtmlWithOptions = async () => {
// Get the Spire.Doc WASM module
const docModule = window.wasmModule?.spiredoc;
// Check if the module is ready
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load fonts and the Word file into VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
const inputFileName = 'ToHtml.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// Create the output directory in VFS
const outputDirectoryName = 'ToHTMLFolder/';
window.dotnetRuntime.Module.FS.mkdirTree(outputDirectoryName);
// Load the Word document
const wordDocument = new docModule.Document();
wordDocument.LoadFromFile(inputFileName);
// Export the CSS styles to a separate file
wordDocument.HtmlExportOptions.CssStyleSheetFileName = outputDirectoryName + 'sample.css';
wordDocument.HtmlExportOptions.CssStyleSheetType = docModule.CssStyleSheetType.External;
// Export images to a separate directory
wordDocument.HtmlExportOptions.ImageEmbedded = false;
wordDocument.HtmlExportOptions.ImagesPath = outputDirectoryName + 'Demo/';
// Export form fields as plain text
wordDocument.HtmlExportOptions.IsTextInputFormFieldAsText = true;
// Convert the document to HTML
const outputFileName = 'ToHtmlExportOption-out.html';
wordDocument.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Html });
// Release resources
wordDocument.Dispose();
// Read the output directory recursively and write each level of files into the zip
const zip = new JSZip();
const addFilesToZip = async (folderPath, zipFolder) => {
let items = await window.dotnetRuntime.Module.FS.readdir(folderPath);
items = items.filter((item) => item !== '.' && item !== '..');
for (const item of items) {
const itemPath = `${folderPath}/${item}`;
try {
const fileData = await window.dotnetRuntime.Module.FS.readFile(itemPath);
zipFolder.file(item, fileData);
} catch (error) {
const zipSubFolder = zipFolder.folder(item);
await addFilesToZip(itemPath, zipSubFolder);
}
}
};
// Package the HTML file together with the resource directory
zip.file(outputFileName, window.dotnetRuntime.Module.FS.readFile(outputFileName));
await addFilesToZip(outputDirectoryName, zip);
const zipBlob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(zipBlob);
// Trigger download
const a = window.document.createElement('a');
a.href = url;
a.download = 'ToHTMLFolder.zip';
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Convert Word To HTML With Export Options</h1>
<button onClick={wordToHtmlWithOptions}>
Generate
</button>
</div>
);
}
export default App;
HTML, CSS, and image files generated after configuring the export options

Note that Spire.Doc does not write images directly into the directory pointed to by ImagesPath. Instead, it creates an external_images subdirectory underneath it to hold the images. As a result, the output directory typically forms a hierarchy such as Demo/external_images/*.png, which must be read level by level — this is why addFilesToZip is implemented recursively in the example above.
FAQ
Fonts in the exported HTML do not match the original document
Cause: The font files are missing from the WASM virtual file system. Spire.Doc reads fonts from VFS during conversion to perform layout calculations and font name resolution. If the fonts are not preloaded, the fonts used in the original document are replaced with substitute fonts, and the font-family in the exported CSS will not match the original. If the original document uses a symbol font such as Wingdings, the corresponding characters will also appear garbled.
Solution: Load the font files into VFS via FetchFileToVFS before conversion. For Chinese, Japanese, and Korean documents, use a font with broad coverage such as ARIALUNI.TTF:
await window.spire.FetchFileToVFS(
'ARIALUNI.TTF', '/Library/Fonts/', '/'
);
Exported HTML loses its styles and images when opened
Cause: In external mode (CssStyleSheetType.External combined with ImageEmbedded = false), CSS and images are output as separate files to the specified directory, and the HTML keeps only relative path references. If you download the HTML file on its own, the browser cannot find the corresponding style sheet and images, and the page degrades into unstyled plain text.
Solution: Package the HTML file together with the resource directory and download them as a whole, so that the relative path references remain valid (see the addFilesToZip example above). If you do not need separate resource files, you can switch to embedded mode instead:
wordDocument.HtmlExportOptions.CssStyleSheetType = docModule.CssStyleSheetType.Internal;
wordDocument.HtmlExportOptions.ImageEmbedded = true;
Get a Free License
Spire.Doc for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.