Fonts
A PDF embeds its fonts in the document. Unlike a browser, it cannot fall back to whatever the reader happens to have installed, so which font is used is decided at conversion time*
The order in which fonts are chosen
- The CLI options
--font, and--gothic-font,--serif-font,--mono-font @font-facein the CSS- A system font search using the names in
font-family - A system search for a font that can draw the characters in the document
Only if none of these finds anything does a system sans-serif candidate become the default font.
The fourth step is the safety net for documents where the names give no clue, such as Japanese text with no font-family anywhere. If the document contains characters that none of the fonts gathered in steps 1 to 3 can draw, a system font that has them, for example Noto Sans CJK JP for Japanese, is found and added. The search runs per weight and style, so a document that mixes bold and regular text gets both faces added, which keeps regular text from being drawn with the bold face. If characters still cannot be drawn, a warning is printed before they turn into empty boxes.
warning: no font can draw the character "ไ" (it will render as tofu).
Specify a font with --font/--gothic-font or @font-face
On a server or in CI, name the fonts with
--font. Without it the output depends on the fonts installed on that machine, and that is how the same HTML ends up looking different on a developer’s laptop and in production.
Generic family names
serif, sans-serif, and monospace are resolved against the system fonts. cursive and fantasy are not resolved.
For Japanese, leaving this to the environment changes the typeface of the body text, so the CLI lets you pin it explicitly.
sghtmltopdf invoice.html \
--gothic-font NotoSansJP-Regular.ttf \ # font-family: sans-serif の実体
--serif-font NotoSerifJP-Regular.ttf \ # font-family: serif の実体
--mono-font NotoSansMono-Regular.ttf # font-family: monospace の実体
For a TrueType Collection (.ttc), give the face index with --font-index, which applies to the --font option just before it.
@font-face
@font-face {
font-family: "MyFont";
src: url("fonts/MyFont-Regular.ttf");
font-weight: 400;
font-style: normal;
}
body { font-family: "MyFont", sans-serif; }
The descriptors that are honoured are font-family, src, unicode-range, font-weight, and font-style. Within src, both local() and url() with format() or tech() are accepted. Other descriptors, such as font-display, are ignored.
Only TTF and OTF files are supported. WOFF and WOFF2 are not, so pointing at a webfont as served on the web is an error. Use the original TTF or OTF.
What src: url() may contain
Besides local relative and absolute paths, it accepts data: URIs (base64) and http(s) URLs. Resolution and access control follow exactly the same rules as <img src> and external CSS, so <base href>, --base-url, --allow and --allow-remote-assets all apply unchanged.
@font-face {
font-family: "Embedded";
src: url(data:font/ttf;base64,AAEAAAAM…);
}
A data: URI keeps the font self-contained inside the HTML, which is what you need when the converting side’s filesystem cannot be relied on — passing an HTML string directly, or converting through the HTTP server.
There is nothing to wait for. The document.fonts.ready dance that headless Chrome needs is unnecessary here, and a PDF is never produced with fonts still unresolved.
unicode-range
Ranges
@font-face {
font-family: "Mixed";
src: url("fonts/Latin.ttf");
unicode-range: U+0-24F, U+1E00-1EFF;
}
@font-face {
font-family: "Mixed";
src: url("fonts/JP.ttf"); Ranges
}
- Ranges
- Ranges
- A font declared without
unicode-range, including those fromlocal(),--font, and the system search, covers the whole range - Ranges
Bold and italic
| Value | Behaviour |
|---|---|
font-weight | normal, bold, or 100 to 900. Numbers are reduced to two states, with 600 and above counting as bold. Without a bold face, synthetic bold is drawn by adding a stroke around the fill |
font-style | normal, italic, or oblique, with oblique treated as italic. Without italic shapes, synthetic italic is produced by shearing the text matrix |
The font shorthand is not supported. Write the longhands, such as font-size and font-family, individually.
Emoji
Colour emoji are drawn in colour. Embedded bitmaps (CBDT/CBLC, sbix) and version 0 of COLR/CPAL are supported.
sghtmltopdf report.html --font NotoColorEmoji.ttf
Both macOS’s Apple Color Emoji (sbix) and Linux’s Noto Color Emoji (CBDT/CBLC) work. You do not have to name one: the system font search finds it as a font that can draw the characters in the document.
A monochrome outline font, such as Google’s Noto Emoji, still works as before.
What this looks like in the PDF
Colour glyphs are written to the PDF as a Type 3 font. They stay text, so extraction, search and copy work exactly as they do for ordinary characters.
The original font program is never embedded. Subsetting has nothing to strip from a font without outlines, so embedding one would drag a 10MB file into the PDF almost untouched. Instead the bitmaps go in as images and the COLR layers as filled paths, only for the glyphs the document actually uses.
--grayscale applies to emoji bitmaps too.
What is not supported
- COLRv1 (gradients, transforms, compositing). A COLRv1 font carries
glyf, so its base outlines are drawn in monochrome - OpenType SVG (the
SVGtable) - Palette selection through
font-palette; palette 0 is always used
Subsetting
Only the glyphs that are actually used get embedded. Even if you name a complete Japanese font, the PDF only grows by the characters that appear in the document.
A note on streaming mode
In streaming mode the whole document is never held at once, so the system font searches in steps 3 and 4 above do not happen; a warning is printed and the default font is used. Naming the fonts with the --font options or @font-face gives you the fonts you intended even when streaming.
As an exception, when no font at all is given, one font that can draw CJK is loaded up front alongside the Latin default. That is why a Japanese document converted in streaming mode with no options does not come out as empty boxes. Scripts other than CJK still produce a warning.