glu
Choosing fonts

Choosing fonts

The built-in families

Without any font setup, three families are available under their generic CSS names:

CSS name Font Character
serif Crimson Pro The default for body text: an old-style serif with a small x-height.
sans TeX Gyre Heros A Helvetica descendant.
monospace CamingoCode The default inside code and pre.
The same sample sentence in Crimson Pro, TeX Gyre Heros and CamingoCode

glu deliberately does not pick up fonts installed on the system. A document that renders on one machine renders identically on every other machine, including CI, because the complete font set is either embedded in the binary or referenced explicitly by the document. If you want a system font, point an @font-face rule at its file.

Loading your own fonts

@font-face works as in the browser. Relative paths resolve against the document (in Lua’s htmlbag.render, against base_dir):

@font-face {
  font-family: "Source Serif";
  src: url("fonts/SourceSerif4-Regular.ttf");
}
@font-face {
  font-family: "Source Serif";
  font-weight: bold;
  src: url("fonts/SourceSerif4-Bold.ttf");
}
body { font-family: "Source Serif", serif; }

Declare one @font-face rule per style you use: the regular, bold, italic and bold-italic cuts each map to a font-weight / font-style combination. <strong> and <em> then pick the right cut automatically. When a requested weight has no exact match, the closest registered weight is used.

Variable fonts

For a variable font you do not declare one rule per weight. Declare the file once with its supported range, and every weight inside the range pins the font’s wght axis to exactly that value:

@font-face {
  font-family: "Source Code Pro";
  font-weight: 200 900;
  src: url("fonts/SourceCodePro-VariableFont_wght.ttf");
}
code { font-family: "Source Code Pro"; font-weight: 450; }
One variable font file rendered at seven weights from 200 to 900

All seven rows above come from a single font file. Weights outside the declared range clamp to its nearest end. Other axes are set with font-variation-settings:

h1 { font-variation-settings: "wdth" 75, "opsz" 32; }

Matching the x-height: size-adjust

Mixing families in running text usually exposes their different x-heights: code spans in a serif paragraph look too large or too small. The size-adjust descriptor scales a face relative to its declared font-size so the proportions match:

@font-face {
  font-family: "CamingoCode";
  src: url("fonts/CamingoCode-Regular.ttf");
  size-adjust: 89%;
}
A serif sentence with monospace code spans, without and with size-adjust

Fallback happens per glyph

A font-family list is not a single choice but a priority-ordered stack. Coverage is decided per glyph cluster: each character is set in the first font of the stack that has a glyph for it, and the rest of the text stays in the primary font.

A serif sentence in which the arrow glyphs come from the monospace fallback

This is also how color emoji work: put an emoji font at the end of the stack and only the emoji clusters use it. See the color emoji example for a complete document.

When a glyph is missing everywhere

If no font in the stack covers a character, the PDF shows a blank of the missing glyph’s width (with the original character attached as ActualText, so copy and paste and screen readers still see it), and the log carries one warning per font and character:

WARN Font has no glyph for character, shown as .notdef char=쎾 codepoint=U+C3BE font=CrimsonPro-Regular

An unexpectedly blank character in the output is therefore always worth a look into the log file. See Debugging.