When you convert HTML to PDF in the browser, nothing affects the perceived quality of the final document more than typography. The default font may be perfectly legible, but it cannot replace the brand typeface your design system specifies, the serif face your legal documents require, or the display font your marketing templates rely on. dompdf.js takes a pure frontend approach to PDF generation: it renders real DOM and CSS, which means the same @font-face declarations and font-family rules that work in your web page also work in the PDF output. Custom fonts in TTF and OTF format load through standard CSS, Chinese text falls back to the built-in Source Han Sans SC, and the resulting vector PDF preserves the exact glyphs the browser would have drawn. The catch is that font loading is asynchronous: font files must be fetched over the network, parsed, and registered with the document before rendering, and if you call addPage before that process completes, the PDF silently falls back to a substitute face. This guide walks through every stage of the custom font pipeline in order: choosing between font formats, writing correct @font-face declarations, understanding the loading timeline and the role of document.fonts.ready, dealing with the size problem of CJK fonts through subsetting, and debugging the common failure modes you will actually encounter. By the end you will have a repeatable, production-grade recipe for embedding custom fonts in every PDF your application generates, with no guessing and no silent fallbacks. The techniques here apply whether you are embedding one display face for headings or managing a multi-language font stack, and each section ends with the practical decision you need to make rather than theory you have to translate into code.
Default fonts cover generic scenarios, but corporate documents, brand materials, and files delivered to clients usually carry strict visual requirements: the heading typeface from the brand guidelines, the serif face used in contracts, the display font from the design mockups. Substituting the default font visibly cheapens the result, and in PDF output the difference is permanent — the file is a deliverable that gets printed, archived, and shared, so every typographic compromise stays visible forever. dompdf.js ships with Source Han Sans SC built in, which guarantees correct Chinese glyphs, but Latin letters, digits, and special symbols still need custom fonts to match the brand and produce output that is visually identical to the web page the user just looked at.
Custom fonts also solve subtle layout problems. Different typefaces have different character widths, letter spacing, and vertical metrics, so the same copy laid out with a different font breaks lines at different points and produces different page lengths. Fixing the font before generating the PDF means every export is reproducible and predictable, which matters enormously for contracts, reports, and documents whose pagination must stay stable across revisions. If the font is not pinned down, the same template can produce different page counts on different machines, and nobody can say which version is correct.
For multilingual documents, custom fonts are not a nicety but a requirement. Arabic and Hebrew text need fonts that support their scripts and shaping tables; uncommon Han characters and dialect-specific glyphs need extended coverage. Declaring the full set of font families up front means the generated PDF renders correctly in every language the document touches, and archived files never degrade into missing-glyph boxes when reopened later on a machine without the right fonts installed.
dompdf.js supports both TTF and OTF, the two mainstream outline font formats. Both describe glyph outlines, and the visual quality is essentially identical, so the practical decision is driven by what the foundry delivers: most free and commercial fonts ship both flavors, and you can use whichever you have without converting. The technical difference between TrueType and OpenType lies in how curves are described, and for rendering results it is negligible; picking the file the vendor provides is the most maintenance-free choice.
Licensing deserves attention before anything else. Embedding a font into a PDF counts as redistribution, so the license must explicitly permit embedding. Commercial fonts in particular require checking the EULA; some licenses restrict embedding to a specific document, while others forbid it entirely. Getting this wrong in a shipped product can create legal exposure, so make font licensing part of the procurement checklist rather than an afterthought. If the license is ambiguous, contact the foundry before going to production.
The second practical consideration is that a font family is usually several files: regular, bold, italic, and bold-italic are separate files with their own outlines. Declaring only the regular weight means font-weight: bold has no file to resolve to, and the browser falls back to synthetic bolding, which renders as distorted strokes in a vector PDF. Declare each weight and style with its own @font-face rule and matching font-weight and font-style descriptors, so every CSS usage resolves to a real file with real outlines.
File size also factors into the choice. A complete CJK font weighs tens of megabytes, while a Latin font is typically tens to hundreds of kilobytes. If the font is used only for headings or a handful of Latin characters, a subsetted version loads dramatically faster; if the document must cover rare Han characters, prefer the fuller version and accept the load time, because missing glyphs in a delivered PDF are far worse than a slightly slower export.
Put the @font-face declarations inside the HTML passed to addPage, and dompdf.js resolves the styles, loads the font files, and renders with the declared font family in fallback order. The font-family name in the declaration must match the name used in the CSS exactly, including case; a mismatch is the single most common reason custom fonts silently do not apply. Map font-weight and font-style descriptors to actual files one-to-one instead of letting one file pretend to cover several weights, because the fallback logic depends on those descriptors matching reality.
The url in src accepts relative paths, absolute URLs, and data URLs. Relative paths resolve against the environment where the template is evaluated, so verify the path from the caller's perspective during development. For large files, prefetch the font with fetch and pass it as a data URL, or serve it from the same origin as the page to avoid CORS failures; either approach also makes offline generation possible and keeps the template self-contained and reproducible on any machine.
The order of families in font-family is the fallback order: custom font first, built-in Source Han Sans SC in the middle, and a generic family last. Characters the custom font does not cover then fall through to Source Han Sans SC, guaranteeing Chinese text always has glyphs while Latin and special symbols use the brand face. This three-tier arrangement keeps mixed-language documents stable and eliminates surprise missing-glyph output without any per-character configuration.
import { DomPDF } from 'dompdf.js';
const html = `
<style>
@font-face {
font-family: 'BrandSans';
src: url('fonts/BrandSans-Regular.ttf') format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'BrandSans';
src: url('fonts/BrandSans-Bold.ttf') format('truetype');
font-weight: 700;
}
body { font-family: 'BrandSans', 'Source Han Sans SC', sans-serif; }
.title { font-size: 18pt; font-weight: 700; }
</style>
<h1 class="title">Brand Font Heading</h1>
<p>Body text prefers BrandSans; Chinese falls back to the built-in Source Han Sans SC.</p>`;
await document.fonts.ready;
const pdf = new DomPDF();
pdf.addPage(html, { format: 'A4' });
pdf.save('custom-font-demo.pdf');
Web fonts load asynchronously. If addPage runs while a font is still downloading, the PDF renders with the fallback family, and only a second generation after the font finishes would produce the correct result. Gating generation on font readiness is therefore the standard pattern: document.fonts.ready returns a Promise that resolves once all fonts in the document have loaded, and awaiting it before addPage eliminates the race entirely. This one line is the difference between reliable exports and intermittent font issues that reproduce only on slow networks.
For fonts declared through CSS @font-face, document.fonts.ready waits for them automatically. For fonts loaded dynamically with the JavaScript FontFace API, first await face.load(), add the face to document.fonts, and then await document.fonts.ready — the order matters, because the ready promise only knows about fonts registered in the FontFaceSet at the time it is created. Await both paths uniformly before generating, so every family used in the template is confirmed available in a single, deterministic step rather than by luck.
Failures need a plan too. A 404 on the font file, a CORS block, or an unsupported format silently leaves the page on the fallback font with no visible error. Listen for the FontFaceSet loadingdone and loadingerror events to diagnose, or verify explicitly with document.fonts.check('12px BrandSans'), which returns true only if the family is usable. When it returns false, surface a message to the user or switch to an alternative family before generating, so a font failure never results in a delivered document with the wrong typeface.
There is one more timing subtlety worth knowing: document.fonts.ready resolves after the fonts currently being loaded finish, but a font that fails to load can still leave the promise resolved, because the FontFaceSet treats failed loads as terminal states. That is why the check() verification step matters even after an await: readiness tells you loading finished, and only check() tells you the family you asked for is actually usable. In production code, combine both — await ready for the common path and check() for the failure path — so a broken font file surfaces in your logs or UI instead of silently shipping a wrong-typeface PDF.
CJK fonts are the heavyweight problem of web typography: a full Chinese font starts at several megabytes and can reach tens of megabytes. Loading one as a page resource slows both the page and the PDF generation wait, which is painful precisely in the high-frequency export scenarios where fonts matter most. When a document uses only a limited set of Chinese characters, subsetting is the answer: extract the used characters into a small subset font, and the file drops from tens of megabytes to a few hundred kilobytes, cutting load time from seconds to milliseconds.
Tooling is mature: fonttools with pyftsubset, or similar command-line utilities, take the text content, compute the character set, and emit a subset TTF or OTF for the template to use. The trade-off is that the subset contains only the characters it was built from, so new rare characters added to the copy require regenerating the subset; a stale subset silently renders new characters as missing glyphs. For documents whose content is not predictable in advance, keep the full font or adopt a dynamic subsetting pipeline so coverage never silently lags behind the copy.
The cheapest strategy, however, is to use the built-in Source Han Sans SC for the bulk of Chinese body text and reserve custom fonts for headings and brand elements. Most documents look professional with the built-in face for body copy, and the custom font carries the visual identity exactly where it is seen. This mix reduces font payload to a fraction, loads faster, generates more reliably, and is the highest-ROI font strategy available for Chinese PDF generation.
Whichever strategy you choose, verify coverage with real copy before production. Run the actual strings that will appear in documents through a glyph-coverage check, not just the sample text in the demo, because one uncovered character in a contract template is exactly the kind of defect that surfaces months later in a customer-facing file.
Q: The custom font does not apply; the PDF uses the default face. A: Check that the @font-face family name matches the usage exactly, including case; verify the file path and format descriptor; then confirm the font actually loaded with document.fonts.check. These three checks cover the overwhelming majority of cases, and most problems live in the first two, so run them in order before touching anything else in the template.
Q: Bold and italic look synthetic. A: Declare a separate @font-face file for every weight and style, with font-weight and font-style descriptors matching the file. Synthetic bolding in a vector PDF renders as visibly distorted strokes, and the difference from a true bold face is obvious in print, so formal documents should always reference real weight files rather than relying on the browser's simulation.
Best-practice summary: keep all @font-face declarations at the top of the template style block; make family names and weights map one-to-one to files; await document.fonts.ready before addPage; use the built-in Source Han Sans SC for Chinese body text; subset custom fonts on demand. Following these five rules, custom fonts stop being a source of surprises, delivered PDFs match the design system, and the integration is something the whole team can rely on without re-debugging it in every project.
Q: The PDF renders fine in my browser but the customer's machine shows the wrong font? A: That is exactly what font embedding solves — the font is embedded in the PDF itself, so the customer never needs it installed. If the delivered file still shows fallback glyphs, the export ran before the font finished loading; regenerate after document.fonts.ready and verify with check() before sending the file out.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。