How to Fix Missing Fonts in React jsPDF Exports
Fix missing or garbled characters in React jsPDF PDFs by registering a TTF font, verifying glyph coverage, and loading it before export.
Missing or garbled characters in a React jsPDF export usually mean the PDF is using one of jsPDF’s standard fonts, which only cover an ASCII code page, or that a custom font was not registered before text was written. Use a TTF font containing every required glyph, add it to jsPDF’s virtual file system, register its family and style, select it, and only then call text().
The official jsPDF guidance says UTF-8 output requires a custom font with the needed glyphs. A font that does not contain a character can still produce missing or corrupted output even when registration succeeds. See the jsPDF Unicode and UTF-8 documentation.
1. Reproduce the exact missing characters
Before changing your app, create the smallest export that contains the characters that fail. Include the actual punctuation, accented letters, currency symbols, emoji, or non-Latin script used by your users.
import { jsPDF } from "jspdf";
const doc = new jsPDF();
doc.text("Café — Ελληνικά — العربية — 中文", 20, 30);
doc.save("font-check.pdf");
If ASCII text works but one or more of these characters do not, treat font coverage as the first suspect. CSS font settings in your React page do not automatically change the font embedded in the PDF.
2. Register a TTF font before writing text
jsPDF’s documented custom-font sequence is:
- Load the TTF as a binary string.
- Pass that data to
addFileToVFS(). - Register the file with
addFont(), including family and style. - Select the same family and style with
setFont(). - Write text only after those calls complete.
The family name passed to setFont() must match the family name passed to addFont(). The filename is the virtual filename used by jsPDF; it does not need to be the same as the family name.
Complete React example with a generated font module
The jsPDF font converter can turn a TTF into a JavaScript module containing base64 font data and registration code. The converter route is useful when the font is a fixed application asset that should be bundled with your React code. The generated module installs an addFonts event handler that adds the font to the VFS and registers its family/style. Follow the converter instructions in the official jsPDF repository.
// src/pdf/my-font.js
// This file is generated by the jsPDF font converter.
// It exports or registers the font according to the converter version.
// src/exportInvoice.js
import { jsPDF } from "jspdf";
import "./pdf/my-font"; // Import before creating the document
export function exportInvoice(invoice) {
const doc = new jsPDF();
// Use the exact family and style names generated for your TTF.
doc.setFont("MyFont", "normal");
doc.setFontSize(12);
doc.text(`Customer: ${invoice.customerName}`, 20, 25);
doc.text("Café — Ελληνικά — العربية — 中文", 20, 40);
doc.save("invoice.pdf");
}
Import the generated module on the code path that creates the PDF. If the module is imported only in a route that is not loaded in production, the registration hook will not run before export.
Complete React example loading a TTF at runtime
The alternative is to load the TTF yourself and register it explicitly. This is useful when the font is fetched asynchronously or selected at runtime.
import { jsPDF } from "jspdf";
function binaryStringFromArrayBuffer(buffer) {
const bytes = new Uint8Array(buffer);
let result = "";
const chunkSize = 0x8000;
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
result += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
}
return result;
}
let fontPromise;
function loadMyFont() {
if (!fontPromise) {
fontPromise = fetch("/fonts/MyFont-Regular.ttf")
.then((response) => {
if (!response.ok) {
throw new Error(`Font request failed: ${response.status}`);
}
return response.arrayBuffer();
})
.then(binaryStringFromArrayBuffer);
}
return fontPromise;
}
export async function exportDocument(text) {
const fontBinary = await loadMyFont();
const doc = new jsPDF();
doc.addFileToVFS("MyFont-Regular.ttf", fontBinary);
doc.addFont("MyFont-Regular.ttf", "MyFont", "normal");
doc.setFont("MyFont", "normal");
doc.setFontSize(12);
doc.text(text, 20, 30);
doc.save("document.pdf");
}
Call the export function only after the font promise resolves. Registering the font in one document instance does not automatically register it in a different instance, so repeat registration for each new jsPDF document unless your integration provides a reusable registration hook.
3. Choose a font with the required glyphs
Registration cannot add glyphs that are absent from the TTF. Check the actual characters your export can contain, including:
- Accented Latin characters such as
é,ñ, andø. - Non-Latin scripts such as Greek, Cyrillic, Arabic, Hebrew, Hindi, Chinese, Japanese, or Korean.
- Symbols such as typographic dashes, smart quotes, currency signs, and mathematical operators.
- Every weight and style you plan to use. A regular face does not guarantee that bold or italic registration has the same coverage.
Also review the font’s license before bundling or serving the TTF. The sources for this guide do not verify licenses for particular fonts, so use the license supplied with the font you select.
4. Register multiple styles correctly
Register each file under the family and style you will select. Do not register a bold file as normal and then expect jsPDF to synthesize a reliable bold face.
const doc = new jsPDF();
doc.addFileToVFS("MyFont-Regular.ttf", regularBinary);
doc.addFont("MyFont-Regular.ttf", "MyFont", "normal");
doc.addFileToVFS("MyFont-Bold.ttf", boldBinary);
doc.addFont("MyFont-Bold.ttf", "MyFont", "bold");
doc.setFont("MyFont", "bold");
doc.text("Bold text with the registered glyphs", 20, 30);
5. React and bundler considerations
jsPDF can be imported like another third-party library:
import { jsPDF } from "jspdf";
The jsPDF README mentions react-app-rewired or ejecting for certain Create React App external-configuration needs. That setting is separate from font registration. For any React bundler, verify that the generated font module or TTF asset is included in the production build and that the export code imports or fetches it before creating the document.
6. A practical diagnostic checklist
- Confirm the failing character is present in the input string at export time.
- Test an ASCII-only string and the exact failing string in separate PDFs.
- Confirm the TTF contains every required glyph.
- Confirm
addFileToVFS()receives the font’s binary data, not a URL or an unparsed response object. - Confirm
addFont()uses the same virtual filename you passed toaddFileToVFS(). - Confirm the family and style in
setFont()exactly matchaddFont(). - Confirm registration finishes before the first
text(),html(), or other text-writing call. - Build the application for production and repeat the test with the deployed asset paths.
- Open the resulting PDF in another viewer if only one viewer displays the problem.
7. Troubleshooting common errors
| Symptom | Likely cause | Fix |
|---|---|---|
setFont() runs but output is still garbled |
The family or style does not match the registration, or the font module has not loaded. | Compare the exact strings passed to addFont() and setFont(); import or await the font before creating the document. |
| Some characters work and others disappear | The TTF lacks those glyphs. | Inspect coverage for the exact language and symbols in the export, then choose a font that includes them. |
| Works in development but fails after deployment | The generated module or TTF asset was omitted, moved, or loaded through the wrong path. | Check the production bundle and network request, and ensure the export code imports the generated module or uses a valid public asset URL. |
| Font request returns 404 or an opaque response | The runtime URL is incorrect or the server does not expose the asset. | Fix the asset path and verify the response is successful before converting it to a binary string. |
| TypeScript reports that a method is missing | Your installed jsPDF version or type declarations may not match the API being used. | Compare the installed package version and its declarations with the methods in that version; do not assume an old issue describes current typings. |
| Only one PDF viewer shows the defect | Viewer behavior may differ, or the PDF may still have incomplete registration. | Inspect the file in another viewer, then recheck registration and glyph coverage. The reviewed sources do not establish a viewer-specific workaround. |
8. Performance, reliability, and cost notes
- Bundle size: embedding a full TTF in a generated JavaScript module increases the client bundle. A runtime-loaded font moves those bytes to a separate request but adds asynchronous loading.
- Reuse: cache the font promise when several exports use the same face so concurrent exports do not download it repeatedly.
- Reliability: fail clearly when the font request fails; silently falling back to a standard font can recreate the missing-glyph problem.
- Memory: large fonts and multiple styles increase browser memory use while generating the PDF. Subset fonts only when the font license and your character requirements permit it.
- Validation: test representative strings in the deployment build and in the PDF viewers your users rely on.
9. Or skip the browser setup
If your goal is to capture a rendered page or PDF rather than maintain a browser font pipeline, ScreenshotNeo provides a website screenshot API. It accepts one GET request and returns PNG, JPEG, WebP, or PDF output. Its cleanup steps can accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off.
Only clean shots are billed. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response reports the page verdict and billing result in X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
require('node:fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));
You can also configure full-page capture with lazy images, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, click and wait actions, blocked requests or resource types, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, and usage reporting. Plans include 1,000 free shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
10. FAQ
Do I need a custom font for every jsPDF export?
No. ASCII-only text can use a standard font. You need a custom font when the characters you export are outside the standard font’s supported code page.
Can CSS @font-face fix the PDF?
Not by itself. The font used by your React page and the font embedded by jsPDF are separate. Register the TTF with jsPDF before writing PDF text.
Why does a registered font still show boxes?
Successful registration does not prove glyph coverage. Check that the selected TTF contains the exact missing characters.
Should I use the converter module or runtime loading?
Use the converter module for a bundled, fixed asset. Use runtime loading when the font is fetched or selected dynamically. The official material does not establish a universal speed advantage for either route.
Can ScreenshotNeo repair fonts inside a jsPDF document?
No. ScreenshotNeo captures web pages and returns image or PDF output. jsPDF font registration remains the appropriate fix when your application itself creates the PDF with jsPDF.


