6 Fonts
dxtr edited this page 2026-10-19 07:15:59 +02:00

Fonts

A face is a typeface: one face of a font file, with the settings of its variation axes. A font is a face at a size, in layout units per em. Layout, outlines and the backends all take fonts.

Faces and fonts

(tr:make-face source &key (index 0) variations)
(tr:face-font face size)
  • source is a file name, a pathname, or an octet vector holding the font file. TrueType, OpenType (CFF) and font collections all work.
  • index picks a face from a collection (.ttc, .otc). Single fonts have only face 0.
  • variations sets the axes of a variable font, as an alist of axis tags and values: '(("wght" . 700) ("wdth" . 87.5)).
  • size is the size in layout units per em, a positive real.

face-font gives the face at a size, and the same font each time for one size, so ask for it wherever you need it rather than keeping fonts of your own:

(let* ((face (tr:make-face "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"))
       (body (tr:face-font face 16))
       (title (tr:face-font face 32)))
  (list (tr:font-line-height body)
        (tr:font-line-height title)
        (eq body (tr:face-font face 16))))
;; => (18.625 37.25 T)

The fonts of one face share what does not depend on the size: the font file, its parsed tables, and the distance fields of the distance field atlas.

One size only

When one size is all you need, make-font makes a font with a face of its own, from the same arguments as make-face and a :size:

(tr:make-font source &key (size 16) (index 0) variations)

with-font takes the same arguments, and with-face those of make-face; each destroys what it made when its body is left:

(tr:with-font (font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
                    :size 16)
  (list (tr:font-size font)
        (tr:font-units-per-em font)
        (tr:font-ascent font)
        (tr:font-descent font)
        (tr:font-line-gap font)
        (tr:font-line-height font)))
;; => (16.0 2048 14.8515625 3.7734375 0.0 18.625)

A face prints with its full name, as #<CL-TEXTRENDER:FACE DejaVu Sans {1203AE7FD3}>, and a font with its name and size, as #<CL-TEXTRENDER:FONT DejaVu Sans 16.0 {12048F6D83}>; once destroyed, the name prints as ?.

Fonts from memory

An octet vector works wherever a file name does, for fonts read from an archive or embedded in an image:

(tr:make-face (alexandria:read-file-into-byte-vector "DejaVuSans.ttf"))

The vector is kept, because backends such as the raster backend open the font again from it. Do not modify it afterwards.

Variable fonts

Give the axis values in :variations. The tags must be axes of the font; anything else is an error that names the axes the font has:

(tr:make-face "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
              :variations '(("wght" . 700)))
;; signals TEXTRENDER-ERROR:
;;   The font has no "wght" axis; it is not a variable font.
;;   Leave out :VARIATIONS for a font that is not variable.

A face's variations apply to its layout, outlines, rasterized glyphs and distance fields alike. Two weights of one variable font are two faces.

Metrics

All metrics are single-floats in layout units:

Function What it is
font-size The size, in layout units per em.
font-units-per-em The face's design units per em, an integer (often 1000 or 2048).
font-ascent From the baseline up to the top of the line.
font-descent From the baseline down to the bottom of the line, as a positive number.
font-line-gap Extra space the font asks for between lines.
font-line-height The default distance between baselines: ascent + descent + line gap.

These are the font's line metrics as HarfBuzz reports them (the OS/2 or hhea table). A line of text is as tall as the largest of them among the fonts on it; see Laying Out Text.

Single glyphs

For a single glyph, such as a caret, a bullet or an icon from a symbol font, there is no need to lay out text:

(tr:font-glyph-id font #\A)                 ; => 36
(tr:font-glyph-id font #x2026)              ; => 2825, the ellipsis
(tr:font-glyph-id font (code-char #xE000))  ; => NIL, no glyph
(tr:font-glyph-advance font 36)          ; => 10.9453125
(tr:font-glyph-count font)               ; => 6253, glyph IDs 0 to 6252
(tr:font-glyph-or-notdef font 99999)     ; => 0, drawn as .notdef
(tr:font-glyph-advance font 99999)       ; => 9.6015625, .notdef's advance
  • font-glyph-id takes a character or a code point and returns the glyph ID the font draws it with, or nil. This is the font's own character map, without shaping: use layout-text for text, where shaping picks ligatures, contextual forms and kerning.
  • font-glyph-advance returns a glyph's horizontal advance in layout units, before kerning or any other shaping adjustment.
  • font-glyph-count returns how many glyphs the font has: its glyph IDs run from 0 below it. Every function that takes a glyph ID draws any other ID as the font's .notdef, glyph 0, as a font draws a character it lacks: its advance, outline, image and field, never an error. font-glyph-or-notdef returns the ID that is drawn, for a renderer that keeps what it makes by glyph ID.

Draw such a glyph like any other, from the font, ID and a position you choose: glyph-outline gives its outline, and the raster and atlas backends take a font and glyph ID too.

What a face is made of

font-face returns a font's face. face-source, face-index and face-variations return the arguments the face was made with, face-units-per-em its design units per em, and face-glyph-count how many glyphs it has. Backends that open the font themselves use them: the raster backend opens the face with FreeType.

Lifetimes

Faces and fonts hold foreign objects, HarfBuzz's, which are freed once nothing holds them, as Godot frees a font once nothing refers to it. A font holds its face, a face the fonts it gave, and a layout its fonts, so no font is freed while text laid out with it is around. You need not destroy anything.

To free them at once instead:

  • destroy-face frees a face and every font it gave.
  • destroy-font frees a font, and its face too if make-font made it. A font that face-font gave goes alone, and the face gives a new one for that size the next time it is asked.
  • with-face and with-font destroy what they made when their body is left.

Destroying twice does nothing, and face-live-p and font-live-p tell whether it has happened. Afterwards:

  • layouts made with the font keep their glyphs and positions, and hit testing them still works, but they cannot be laid out or drawn again;

  • layout-text, face-font, font-glyph-id, font-glyph-advance, glyph-outline and the backends signal textrender-error with it:

    The font has been destroyed with DESTROY-FONT.
    Make it again with MAKE-FONT or FACE-FONT, and destroy it only when nothing uses
    it any more.
    

Destroy a face or font only when no thread uses it. See Threads and Lifetimes.

What renderers keep with a face or font

A renderer keeps what it makes from a face's or font's glyphs with the face or font, as Godot's fonts keep their glyph caches, so that it goes with them, under a cache key of its own, which make-cache-key makes:

(defvar *key* (tr:make-cache-key))   ; one for each renderer
(tr:face-cache face *key* make)      ; for what serves every size of the face
(tr:font-cache font *key* make)      ; for what belongs to one size

Each returns what the face or font keeps under the key: what make, a function of no arguments, returned the first time. Finding it again allocates nothing, so a renderer can look it up for every glyph of every frame.

make may return a second value: a function of no arguments that releases the foreign or GPU parts of the first. It runs when the face or font is destroyed or collected, or the key forgotten, on whatever thread that happens on, so it must be safe there, and it must not refer to the face, its fonts or anything that holds them, or they are never collected.

(tr:font-cache font *key*
               (lambda ()
                 (let ((texture (make-glyph-texture)))
                   (values texture (lambda () (free-texture texture))))))

A renderer that goes before the faces and fonts it drew, as one whose window closes while the program's fonts live on, forgets its key as it goes:

(tr:forget-cache-key *key*)

Every face and font that kept something under the key forgets it at once, running its release functions, so they keep nothing the renderer made, nor the renderer, which those functions may refer to. A key knows its faces and fonts through weak pointers, so it keeps none of them alive, and it may be used again afterwards. (tr:make-cache-key :for object) makes a key that is forgotten once the collector takes object, for what needs no releasing: the raster backend, the glyph atlas and the distance field atlas keep their glyphs under such keys. A font keeps the outlines of Glyph Outlines itself.

Threads

Faces and fonts may be shared between threads: several threads may lay out text with one font at once, and ask a face for fonts. Destroy them only when no thread uses them.

Errors

make-face and make-font signal textrender-error when the file cannot be read, is not a font (WOFF and WOFF2 must be converted first), has no face at index, or lacks an axis given in variations. Each report says what to do. A size that is not a positive real signals a type-error. See Errors.