ScreenshotNeo

BlogHTML to image & PDF

How to Render Mathematical Symbols When Converting HTML to PDF with node-html-pdf

Render equations before PDF capture, bundle the right CSS and fonts, and make PhantomJS wait for math to finish. Includes a KaTeX example and troubleshooting.

By the ScreenshotNeo team30 September 202610 min read

How to Render Mathematical Symbols When Converting HTML to PDF with node-html-pdf

To render mathematical symbols reliably with node-html-pdf, convert equations into final HTML or SVG before calling pdf.create, include the renderer’s CSS and font files, and make sure PhantomJS can resolve those assets. If math is typeset in the page by an asynchronous script, wait for a completion signal; a fixed delay is only a fallback. Pin the fonts and runtime environment because output can differ across operating systems.

For TeX input, KaTeX’s server-side renderToString is a direct option: it produces markup before PDF generation starts. MathJax-node is another option and can produce HTML, SVG, or MathML. The html-pdf package is deprecated; for new systems, assess a maintained Chromium renderer such as Puppeteer before committing to a PhantomJS pipeline. The npm package listing recommends migrating to a newer library.

1. Why math disappears or turns into boxes

PDF capture prints what the rendering engine can see at capture time. Missing equations usually trace back to one of four layers:

  • Typesetting: the source contains TeX delimiters, but no math renderer converted them to display markup.
  • Timing: browser-side MathJax or KaTeX has not finished when PhantomJS captures the page.
  • Assets: the output markup exists, but its stylesheet or font files cannot be found or loaded.
  • Glyph support: the requested symbol is not covered by the math font, so the renderer falls back to a system font or shows a missing-glyph box.

Diagnose which layer is failing before changing PDF dimensions or margins. Increasing the page size cannot repair absent markup, blocked fonts, or a missing glyph.

Server rendering removes the typesetting race: the HTML string passed to pdf.create already contains KaTeX markup. KaTeX’s Node documentation notes that the generated HTML still needs the KaTeX stylesheet and font files available to the consuming page. Keep those assets together and provide a base path PhantomJS can resolve.

Server-rendered equations still need their stylesheet and font assets available when PhantomJS lays out the page.
Server-rendered equations still need their stylesheet and font assets available when PhantomJS lays out the page.

Install the packages

npm install html-pdf katex

Install the KaTeX package in the application so its stylesheet and font directory are available. The example below assumes a project layout where the script can resolve node_modules/katex/dist. Adapt the absolute path if the script runs from another directory.

Complete runnable example

const fs = require('node:fs');
const path = require('node:path');
const pdf = require('html-pdf');
const katex = require('katex');

const katexDist = path.resolve(__dirname, 'node_modules/katex/dist');
const katexCss = fs.readFileSync(
  path.join(katexDist, 'katex.min.css'),
  'utf8'
);

const equation = katex.renderToString(
  String.raw`\int_0^1 x^2\,dx = \frac{1}{3}`,
  { displayMode: true, throwOnError: true }
);

const html = `<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>${katexCss}</style>
  <style>
    @page { margin: 24mm; }
    body { font-family: sans-serif; color: #222; }
    .equation { margin: 2rem 0; }
  </style>
</head>
<body>
  <h1>A short derivation</h1>
  <p>The definite integral is:</p>
  <div class="equation">${equation}</div>
</body>
</html>`;

const options = {
  format: 'A4',
  border: '0',
  base: `file://${katexDist}/`,
  timeout: 30000,
  renderDelay: 0
};

pdf.create(html, options).toFile('math.pdf', (err, result) => {
  if (err) {
    console.error(err);
    process.exitCode = 1;
    return;
  }
  console.log(`Wrote ${result.filename}`);
});

The example embeds the KaTeX CSS and renders the integral before PDF creation. KaTeX’s CSS references its font files using relative paths, so the base option points at the distribution directory. Check that your installed html-pdf version accepts the documented option shape and that the PhantomJS process can read the resolved path. The package documents options including phantomPath, localUrlAccess, timeout, and renderDelay; see its README for the version you use.

For production, avoid assuming that __dirname or a relative URL points to the same place after packaging or deployment. Resolve assets from a known application directory, and keep the KaTeX CSS and font files in the deployed artifact. If you generate HTML files separately, give the renderer a base URL that matches where those files and fonts actually live.

Handle invalid TeX and untrusted input

With throwOnError: true, malformed TeX fails where it is rendered, which makes the source easier to identify than a broken PDF. Catch that error and report the equation index or document section. If user-provided expressions should appear literally instead, configure KaTeX’s error behavior deliberately and escape the surrounding HTML. Never interpolate untrusted text into HTML without context-appropriate escaping.

Render each expression with the desired display mode. Inline expressions belong in surrounding text; display mode creates a standalone equation. If output from renderToString is inserted into a template, treat it as HTML generated by the math renderer, while still escaping unrelated user text.

3. MathJax and browser-side typesetting

Use MathJax when the input needs MathML support or its output formats fit your document pipeline. The mathjax-node project accepts TeX, inline TeX, or MathML and can emit HTML, SVG, or MathML. Its HTML output depends on configured webfont URLs. Bundle or host those assets in a way PhantomJS can reach.

If the page loads MathJax in the browser, PDF generation must wait until typesetting has completed. A renderDelay can give a script time to run, but it is a timing guess: slow machines, large documents, and network delays can outlast it. Prefer a completion event or callback that fires after the math renderer has inserted its final markup and styles. The exact event API depends on the MathJax version and integration; connect that signal to the completion mechanism supported by your rendering setup.

For small documents, a delay may be acceptable if you have bounded asset loading and a generous timeout. Keep it separate from the PDF timeout: the delay controls when capture begins, while the timeout limits how long the render operation may take. A longer delay does not fix a failed script URL or a blocked font.

4. Choosing HTML, SVG, MathML, or Unicode

Representation Useful when Things to verify
KaTeX HTML and CSS You have TeX input and want synchronous server rendering. Ship the matching CSS and font files; verify asset paths.
MathJax HTML Your typesetting flow uses MathJax and its HTML output. Configure and resolve the webfont URLs; wait for typesetting if it runs in-page.
SVG You want a self-contained vector representation for each equation. Confirm the PDF engine accepts the SVG features and embedded or referenced assets used.
MathML Your downstream renderer supports the MathML subset you need. Do not assume PhantomJS renders all MathML consistently; validate the specific expressions.
Unicode symbols The document contains a small set of ordinary mathematical characters. Verify glyph coverage and font fallback on the deployment OS; use TeX commands for consistency-critical symbols.

KaTeX documents broad support for Unicode mathematical alphanumeric symbols, but unrecognized characters may be treated as text and use system fonts, with possible alignment differences. For equations that must look the same in every environment, use supported TeX commands and a known math renderer instead of relying on whichever fallback font happens to be installed.

5. Configure paths, access, and capture timing

PhantomJS does not interpret local paths exactly like a modern browser session. A URL such as /css/site.css may resolve against a web origin in one setup but have no meaningful filesystem location when the page is loaded from file://. Use absolute paths or a correct base URL, then verify the process can access them.

A pinned runtime and bundled fonts help prevent symbol substitutions between development and production.
A pinned runtime and bundled fonts help prevent symbol substitutions between development and production.
  • base: Set it to the directory or origin that makes relative CSS and font URLs resolve. Confirm the generated URL from the deployed working directory.
  • localUrlAccess: The package documents this as a security-sensitive control for local URL access. Enable only the access your document needs; avoid opening broad local filesystem access for untrusted HTML.
  • phantomPath: Set this when the PhantomJS binary is not at the package’s expected location. Ensure the deployed binary is executable and compatible with the host.
  • timeout: Give large documents and local resource loading enough time, but surface timeouts as failed jobs rather than returning partial PDFs.
  • renderDelay: Use a completion signal for asynchronous browser rendering when possible. A millisecond delay is a fallback, not proof that math finished.

Also set the document encoding explicitly to UTF-8. Confirm that custom styles do not override KaTeX’s layout rules, clip tall equations, or split a display equation awkwardly across pages. Long derivations may need page-break rules around equation blocks.

6. Troubleshooting common failures

Symptom Likely cause Fix
Equation source appears as raw \frac or delimiters No typesetting pass ran, or the renderer script failed. Render TeX on the server before pdf.create, or inspect browser console/resource errors and wait for the page renderer to finish.
Boxes appear instead of symbols Missing math fonts, unsupported glyph, or fonts blocked by local URL policy. Bundle the KaTeX fonts, correct the base path, check local access, and use a supported TeX command for the symbol.
Equations work locally but fail in production Different paths, missing deployed font files, or different OS font sets. Include CSS and fonts in the build artifact, use explicit resolved paths, and pin the runtime image and installed fonts.
Equation is present but misaligned Fallback font substituted for an unsupported Unicode character, or CSS conflicts. Use a supported math command, compare loaded fonts, and remove conflicting styles around math spans.
Some pages have equations and others do not Capture races an asynchronous renderer or remote font loading. Wait for a renderer completion signal that also follows required style/font loading; do not rely on a short fixed delay.
PDF times out Remote assets stall, the delay is excessive, or the document is costly to lay out. Prefer local assets, remove unnecessary network dependencies, tune timeout based on document needs, and log the failing resource.
Local CSS or fonts load from the wrong directory Relative paths were interpreted from a different base, especially under file://. Set a correct base path and inspect the exact absolute file locations available to the PhantomJS process.
Output differs between Windows and Linux Different system fonts, runtime versions, or platform-specific rendering behavior. Build and render in a pinned OS image with the same fonts and PhantomJS runtime used in production.

Custom-font failures and Windows/Linux differences have been reported in the project’s issue tracker. Treat the host OS and font set as part of the build inputs, not incidental server details. These reports establish possible failure modes, not how frequently they occur.

7. Reliability, performance, and cost

Server-side equation rendering adds work before PDF creation, but it removes the uncertainty of waiting for a browser-side typesetting pass. For repeated documents, cache rendered equation markup keyed by the expression and renderer configuration if the content and configuration are stable. Keep font and renderer versions in that key or invalidate the cache when either changes; otherwise the HTML can be paired with incompatible assets.

Measure the whole job by stages: equation rendering, HTML assembly, resource loading, layout, and PDF writing. The dossier provides no authoritative benchmark for this stack, so do not assume a particular throughput or latency. Large documents, many equations, remote assets, and font loading can all affect completion time. Prefer local assets for predictable deployment and avoid launching more PhantomJS jobs concurrently than the host can support.

Reliability comes from making inputs explicit: validate TeX, bundle fonts and CSS, pin the runtime, use deterministic paths, wait for actual completion, and treat renderer errors as failed jobs. Because html-pdf wraps PhantomJS and is deprecated, include migration work in maintenance planning. Puppeteer or another maintained Chromium renderer may be a better base for new systems, but test the required math output and pagination before switching.

Operational cost includes CPU and memory for rendering, storage and transfer of PDFs, and the engineering time spent maintaining an older PhantomJS stack. The available sources do not establish a numeric cost or speed comparison, so benchmark with representative documents on the target deployment environment.

8. Or skip the browser setup

For a website screenshot or PDF capture, ScreenshotNeo makes one GET request and returns the rendered output. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, with the outcome shown in response headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

See the ScreenshotNeo API documentation for request options. This captures a URL; it does not replace server-side TeX-to-HTML conversion when your source is an equation or an HTML document you are generating yourself. ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Create a free account for 1,000 screenshots a month, with no card.

9. Frequently asked questions

Can node-html-pdf render TeX directly?

No. It converts HTML through PhantomJS; use KaTeX, MathJax, or another typesetting step to turn TeX into markup the page can render.

Is renderDelay enough to guarantee equations are ready?

No. It gives asynchronous work time to run, but only a completion signal tied to typesetting and required assets tells you the work finished. A fixed delay can still be too short.

Should I use KaTeX or MathJax?

Choose based on input and output needs. KaTeX offers synchronous server rendering of TeX to an HTML string. MathJax-node accepts TeX and MathML and can emit HTML, SVG, or MathML.

Is node-html-pdf appropriate for a new service?

It is deprecated. Compare a maintained renderer for new work, and validate math layout, font availability, and pagination before migrating an existing pipeline.