Convert HTML to PDF, Images and Text with JavaScript in React

Contracts, reports and help pages stored as HTML often have to be reissued in a different format for archiving, distribution and retrieval: outbound delivery asks for PDF, a list preview needs only a thumbnail, and full-text search cares about the plain words alone. Queuing every conversion on a server adds deployment cost, and it means the content has to leave the user's device first.

This article uses Spire.Doc for JavaScript to produce all three outputs in the browser: convert HTML to PDF, convert HTML to images, and convert HTML to text. All three share the same loading pipeline and differ only in how the result is exported. The sample uses one HTML file, Sample.html, the "Xinglan Technology 2026 Third-Quarter Operations Report": the file is laid out with CSS, and it contains an information band with a background color, statistic cards, an ordered list and an unordered list, several data tables and an embedded bar chart, with forced page breaks before the business metrics and the risks and responses sections. The conversion produces a three-page A4 document.

This article covers three features:

For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume that Spire.Doc is installed and the WebAssembly module is initialized.


Convert HTML to PDF

Converting HTML to PDF takes two calls: Document.LoadFromFile() parses the page structure following HTML rules, and SaveToFile() writes the parsed document as PDF. The layout is fixed once the conversion is done and does not change with the reading environment, which suits archiving and outbound delivery.

LoadFromFile() takes its arguments as an object:

Argument Value Description
fileName 'Sample.html' Path of the file in the virtual file system (VFS)
fileFormat FileFormat.Html Parse as HTML; the file extension does not matter
validationType XHTMLValidationType.None Lenient validation that tolerates non-standard tags and attributes

XHTMLValidationType also offers Strict and Transitional. Pages exported from line-of-business systems rarely meet the strict standard, so None keeps parsing from stopping early.

The specific steps are as follows:

  1. Get the Spire.Doc WASM module and confirm that it is ready.
  2. Load the font and Sample.html into the VFS.
  3. Create a Document instance and load the file as HTML.
  4. Save it to the VFS with FileFormat.PDF.
  5. Read the PDF bytes from the VFS and trigger a download.

The code for converting HTML to PDF is as follows:

function App() {
  const htmlToPdf = async () => {
    // Get the Spire.Doc WASM module
    const docModule = window.wasmModule?.spiredoc;

    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the font and the HTML file to be converted
    await window.spire.FetchFileToVFS('segoeui.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    await window.spire.FetchFileToVFS('Sample.html', '', `${process.env.PUBLIC_URL}/data/`);

    const doc = new docModule.Document();
    // Load as HTML with lenient validation
    doc.LoadFromFile({
      fileName: 'Sample.html',
      fileFormat: docModule.FileFormat.Html,
      validationType: docModule.XHTMLValidationType.None,
    });
    // Relative resources are resolved from the VFS root
    doc.HtmlBaseUrl = '/';

    const outputFileName = 'HtmlToPdf.pdf';
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.PDF });

    // 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);

    doc.Dispose();
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>HTML to PDF</h1>
      <button onClick={htmlToPdf}>Generate</button>
    </div>
  );
}

export default App;

The sample HTML comes out as a three-page A4 document:

Convert HTML to PDF: the three-page result, with the cover information band, statistic cards, list numbering, table header shading and the embedded bar chart kept as styled in the HTML

The first page holds the cover information and the quarterly overview, the second the business metrics and the revenue composition, and the third the risks and responses, the next-quarter plan and the appendix. Heading levels, paragraph spacing, the numbering of the ordered list, the dark blue table headers and the zebra striping of the tables are all kept as the CSS specifies; the embedded chart.png is placed at the width set in the HTML, and page-break-before: always takes effect as written, so the fourth and seventh sections each start at the top of pages 2 and 3.

How to Control Where the PDF Breaks Pages

By default, pages break wherever the content height runs out: the body fills one page and then continues at the next natural position. To start a new page at a section boundary, set page-break-before: always on the element in the style sheet; the property still applies after the HTML is converted:

<!-- This heading and what follows it start on a new page -->
<h2 style="page-break-before: always;">4. Business Metrics</h2>

The sample sets the property on the "4. Business Metrics" and "7. Risks and Responses" headings, and in the result both sections start at the top of a page, matching what a browser would print.


Convert HTML to Images

Converting HTML to an image uses Document.SaveImageToStreams(). Thumbnails, message cards and document previews need a single image, and this method renders the specified page into an image object, which then writes the file to the VFS through its Save() method. The arguments and related members are as follows:

Argument or member Value Description
pageIndex Page number starting from 0 0 is the first page
type ImageType.Bitmap Bitmap; ImageType.Metafile produces a vector metafile instead
Save(path) Path in the VFS Writes the file by extension; the sample writes HtmlToImage.png

The specific steps are as follows:

  1. Get the Spire.Doc WASM module and confirm that it is ready.
  2. Load the font and Sample.html into the VFS.
  3. Create a Document instance and load the file as HTML.
  4. Call SaveImageToStreams() to render the first page and write it to the VFS with Save().
  5. Read the PNG bytes from the VFS and trigger a download.

The code for converting HTML to an image is as follows:

function App() {
  const htmlToImage = async () => {
    // Get the Spire.Doc WASM module
    const docModule = window.wasmModule?.spiredoc;

    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the font and the HTML file to be converted
    await window.spire.FetchFileToVFS('segoeui.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    await window.spire.FetchFileToVFS('Sample.html', '', `${process.env.PUBLIC_URL}/data/`);

    const doc = new docModule.Document();
    doc.LoadFromFile({
      fileName: 'Sample.html',
      fileFormat: docModule.FileFormat.Html,
      validationType: docModule.XHTMLValidationType.None,
    });
    doc.HtmlBaseUrl = '/';

    const outputFileName = 'HtmlToImage.png';
    // pageIndex is zero-based; render the first page
    const image = doc.SaveImageToStreams({
      pageIndex: 0,
      type: docModule.ImageType.Bitmap,
    });
    image.Save(outputFileName);

    doc.Dispose();

    // Read the image from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'image/png' });
    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>HTML to Image</h1>
      <button onClick={htmlToImage}>Generate</button>
    </div>
  );
}

export default App;

The first page of the sample HTML renders as follows:

Convert HTML to Images: the A4 page rendered at 96 dpi to a 794 x 1123 pixel PNG, with content matching the first page of the PDF

The PNG is 794 x 1123 pixels, the size of an A4 page at 96 dpi; the content matches the first page of the PDF, and the information band, statistic cards, lists and timeline table keep their relative positions. The aspect ratio of the image follows the page setup rather than the width of the browser window.

How to Export Every Page in One Run

A long HTML document is split across pages, and Document.PageCount gives the total, so a loop over the page indexes exports all of them:

// Same as the PDF page count: three pages in the sample
const pageCount = doc.PageCount;

for (let i = 0; i < pageCount; i++) {
  const pageImage = doc.SaveImageToStreams({
    pageIndex: i,
    type: docModule.ImageType.Bitmap,
  });
  pageImage.Save(`HtmlPage-${i + 1}.png`);
}

// Release the document once after the loop
doc.Dispose();

The sample document has three pages, so the loop produces HtmlPage-1.png to HtmlPage-3.png, and PageCount agrees with the page count of the PDF. Rendering the document once and exporting several pages in a row is faster than reloading it page by page, and the document instance only has to be released once, after the loop.


Convert HTML to Text

Converting HTML to text is a matter of saving with FileFormat.Txt. Full-text search, content review and corpus loading care about the words alone, so the export extracts characters in document order and keeps no styles, table borders, images or hyperlink addresses; the result is a suitable source of plain-text material. The export rules are as follows:

Content Exported result
Headings and body text Written line by line in paragraph order, text only, without font size or styling
Ordered list Each line keeps a 1. prefix plus two spaces, with the number increasing in the actual order
Unordered list Each line keeps * plus one space as the bullet
Tables Each cell on its own line, expanded in row-major order, with no separator between cells
Images Not exported; the position is left blank
Pagination No page breaks; all content goes into a single file
Encoding UTF-8, with a BOM at the start of the file

The specific steps are as follows:

  1. Get the Spire.Doc WASM module and confirm that it is ready.
  2. Load the font and Sample.html into the VFS, keeping the same pipeline as the first two samples.
  3. Create a Document instance and load the file as HTML.
  4. Save it to the VFS with FileFormat.Txt.
  5. Read the text file from the VFS and trigger a download.

The code for converting HTML to text is as follows:

function App() {
  const htmlToText = async () => {
    // Get the Spire.Doc WASM module
    const docModule = window.wasmModule?.spiredoc;

    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the font and the HTML file to be converted
    await window.spire.FetchFileToVFS('segoeui.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    await window.spire.FetchFileToVFS('Sample.html', '', `${process.env.PUBLIC_URL}/data/`);

    const doc = new docModule.Document();
    doc.LoadFromFile({
      fileName: 'Sample.html',
      fileFormat: docModule.FileFormat.Html,
      validationType: docModule.XHTMLValidationType.None,
    });

    const outputFileName = 'HtmlToText.txt';
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Txt });

    doc.Dispose();

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>HTML to Text</h1>
      <button onClick={htmlToText}>Generate</button>
    </div>
  );
}

export default App;

All three pages go into the same text file, and its opening section reads as follows:

Convert HTML to Text: headings and body text in paragraph order, ordered-list numbering kept as

The English headline in the information band, the company name and the subtitle each take one line; headings such as "1. Quarterly Overview" keep only their text, with no markers; every ordered-list line starts with a number such as 1. or 2. plus two spaces; the statistic card becomes the two lines CNY 186M and Quarterly Revenue; and each cell of the timeline table gets its own line. The chart.png image has no counterpart in the text, while the caption "Figure 1: Revenue by business line, Q3 2026 (CNY 10k)" is kept as body text.

Which Output to Choose

Output Typical use Main cost
PDF Archiving, outbound delivery, printing Fonts have to be embedded, so files are larger
Image Thumbnails, previews, message cards Not searchable; sharpness is limited by the render resolution
Text Full-text search, corpora, content review All layout information is lost

All three conversions happen inside the browser, and the file does not leave the user's device. For batch processing, put the functions above into a single loop that loads, exports and releases each document in turn.


FAQ

Do the files have to be uploaded to a server during the conversion

No. Spire.Doc for JavaScript runs in the browser on WebAssembly, and the HTML file is read and written locally through the virtual file system (VFS): loading, converting and saving all happen on the device, and the library itself does not send files to a server. Whether a file eventually leaves the device depends on the other code you add to the page.

Why do some characters appear as boxes

Cause: PDF and image rendering read fonts from the VFS. When the VFS holds no font at all, saving throws Cannot find any fonts in specified font sources.; when only a Latin font is loaded, the conversion still completes, but characters that font does not cover (Chinese, for example) appear as boxes because their glyphs are missing, while Latin text and digits render normally.

Fix: load a font file that covers those characters together with the HTML, and place it in the /Library/Fonts/ directory:

// Write the font into the VFS font directory
await window.spire.FetchFileToVFS('segoeui.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

FetchFileToVFS does not stop when the request fails (a 404, for instance); the error page is written into the VFS just the same, and the problem only surfaces later as a font error. Check that the file really exists under public/font before loading it.

Why do the images referenced by the HTML not appear in the result

Cause: LoadFromFile parses only the HTML file itself; referenced images and style sheets have to be loaded into the VFS separately. A missing image does not raise an error: the position renders as a placeholder icon and the content after it shifts. In the sample, dropping chart.png turns the three-page document into four pages.

Fix: load the resources referenced by the HTML into the VFS as well. Relative paths resolve against the VFS directory of the loaded file, so placing Sample.html and chart.png in the same directory is enough; when the resources live elsewhere, set the base directory with HtmlBaseUrl:

// Load the image and the HTML from the same folder into the VFS
await window.spire.FetchFileToVFS('Sample.html', '', `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('chart.png', '', `${process.env.PUBLIC_URL}/data/`);

In the exported text, why does each table row come out as one line per cell

Cause: the text export writes characters in order and keeps no row or column structure. In the sample, the "4. Business Metrics" table comes out as four lines — Metric, This Quarter, Last Quarter, Change — followed by four lines for the first row — Revenue (CNY 10k), 18,600, 16,240, +14.5% — with neither tabs nor pipes between the cells, so it is not obvious which values belong to the same row.

Fix: when the row and column relationships have to be kept, group the cells by the table's fixed column count, or use PDF or images to keep the layout:

// The table has four fixed columns: slice out its line range, then regroup every four lines
const cells = text.split('\n').slice(tableStart, tableEnd);
const rows = [];
for (let i = 0; i + 4 <= cells.length; i += 4) {
  rows.push(cells.slice(i, i + 4));
}

When the column count changes from table to table, keep the PDF as the layout reference and use the text for search only.


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.