Table of Contents
- Font variations
- Inspecting
- face-variable-p face → boolean
- face-variation-axes face → list of var-axis
- face-named-instances face → list of named-instance
- face-default-named-instance face → index or nil
- Coordinates
- face-design-coordinates face → list · setf
- face-axis-coordinate face tag → real · setf
- face-normalized-coordinates face → list · setf
- face-named-instance face → index or nil · setf
- Coordinates in effect
- A FreeType bug: an instance selected again after a reset
- Effects
- Example: a weight ramp
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Font variations
cl-ft2 supports OpenType variable fonts (fvar/gvar, CFF2), and Adobe Multiple Masters fonts, which FreeType presents through the same interface. A variable font has one or more axes, such as weight (wght) or width (wdth), and usually a set of named instances, such as "Thin", "Regular" and "Black", at fixed points on those axes.
The examples use tests/fonts/cl-ft2-test-variable.ttf from the repository. It has wght 100–900 (default 400), wdth 75–125 (default 100), and the instances Thin, Regular and Black.
Inspecting
face-variable-p face → boolean
True if the face has variation axes (:multiple-masters in face-flags).
face-variation-axes face → list of var-axis
The axes in axis order. The list is empty for a face without variations.
(ft2:face-variation-axes face)
;; => (#S(VAR-AXIS :TAG "wght" :NAME "Weight" :MINIMUM 100 :DEFAULT 400 :MAXIMUM 900
;; :HIDDEN-P NIL :INDEX 0)
;; #S(VAR-AXIS :TAG "wdth" :NAME "Width" :MINIMUM 75 :DEFAULT 100 :MAXIMUM 125
;; :HIDDEN-P NIL :INDEX 1))
| Accessor | Meaning |
|---|---|
var-axis-tag |
four-character OpenType tag: "wght", "wdth", "slnt", "ital", "opsz", or a custom uppercase tag |
var-axis-name |
display name, or nil |
var-axis-minimum, var-axis-default, var-axis-maximum |
range, in design units (rationals) |
var-axis-hidden-p |
true if the font asks not to show the axis in user interfaces |
var-axis-index |
position in coordinate lists |
face-named-instances face → list of named-instance
(ft2:face-named-instances face)
;; => (#S(NAMED-INSTANCE :NAME "Thin" :POSTSCRIPT-NAME NIL :COORDINATES (100 100) :INDEX 0)
;; #S(NAMED-INSTANCE :NAME "Regular" :POSTSCRIPT-NAME NIL :COORDINATES (400 100) :INDEX 1)
;; #S(NAMED-INSTANCE :NAME "Black" :POSTSCRIPT-NAME NIL :COORDINATES (900 100) :INDEX 2))
| Accessor | Meaning |
|---|---|
named-instance-name |
the instance's subfamily name, from the name table, preferring US English Windows names |
named-instance-postscript-name |
its PostScript name, or nil if the font gives none |
named-instance-coordinates |
its design coordinates, in axis order |
named-instance-index |
from 0, the value for face-named-instance and for open-face's :instance |
Named instances are numbered from 0, with nil for none. FreeType numbers them from 1, with 0 for none; cl-ft2 converts. cl-harfbuzz uses the same convention as cl-ft2.
face-default-named-instance face → index or nil
The instance whose coordinates are the font's defaults (here 1, "Regular"), or nil if there is none.
Coordinates
Design coordinates are in each axis's own units: wght 100–900, wdth as a percentage, and so on. Normalized coordinates run from −1 (minimum) through 0 (default) to 1 (maximum).
Each place below is read with its function and set with setf. As usual for setf, setting returns the value you gave, not the clamped or padded result.
face-design-coordinates face → list · setf
The design coordinates in effect, one per axis in axis order.
Setting it takes a list of reals in axis order:
- Axes past the end of the list take the font's default, not their current value.
- Values outside an axis's range are clamped to it.
- More values than axes signals
:invalid-argument. nildiscards any coordinates set and returns the face to its named instance, or to the font's defaults if none is selected.
(ft2:face-design-coordinates face) ; => (400 100) the defaults
(setf (ft2:face-design-coordinates face) '(900)) ; wght 900, wdth 100
(setf (ft2:face-design-coordinates face) '(2000 50)) ; clamped to (900 75)
(setf (ft2:face-design-coordinates face) '()) ; reset
Setting coordinates adds :variation to face-flags, unless the result is the font's default position.
face-axis-coordinate face tag → real · setf
The design coordinate of one axis, named by its tag. Setting it changes that axis, clamped to its range, and leaves the others as they are:
(setf (ft2:face-axis-coordinate face "wght") 650)
(ft2:face-axis-coordinate face "wght") ; => 650
(ft2:face-design-coordinates face) ; => (650 100)
(ft2:face-normalized-coordinates face) ; => (1/2 0)
(member :variation (ft2:face-flags face)) ; => (:VARIATION)
A tag the face does not have signals :invalid-argument and lists the tags it does have. A tag is four characters: a string of another length is a type-error, which SBCL reports at compile time for a constant.
face-normalized-coordinates face → list · setf
The normalized coordinates in effect. Setting it works like face-design-coordinates: missing axes take 0, values are clamped to −1..1, and nil resets.
face-named-instance face → index or nil · setf
The instance last selected, by setting this place or by open-face :instance, or nil. Setting coordinates later moves the face away from that instance without deselecting it. :variation in face-flags tells you whether they have.
Setting it to an index (from 0) selects that instance. Setting it to nil selects none, which puts every axis at its default. Either way, any coordinates set since are discarded and :variation is cleared. An index that does not exist signals :invalid-argument.
(setf (ft2:face-named-instance face) 2) ; Black
(ft2:face-design-coordinates face) ; => (900 100)
(setf (ft2:face-named-instance face) nil) ; defaults
You can also choose an instance when opening the face, which also sets the face's style name:
(ft2:open-face lib "Inter.ttf" :instance 2)
face-index reports only the face's index in its file. FreeType packs the instance into the same field, and face-named-instance is how you read it.
Coordinates in effect
The readers always report the coordinates actually in effect. FreeType's own getters can keep answering with the old values after a reset to defaults. cl-ft2 detects that state (no :variation flag and no named instance) and answers with the defaults itself. This FreeType quirk, and FreeType's two setters, which disagree on short and out-of-range lists, are handled in src/variation.lisp, where a comment explains them.
A FreeType bug: an instance selected again after a reset
cl-ft2 does not work around this one. Once a face has gone from a named instance whose coordinates were changed back to the font's defaults, selecting that named instance again, or setting its design coordinates, does nothing: glyphs go on loading at the defaults, and the readers report the defaults.
;; With tests/fonts/cl-ft2-test-variable.ttf
(setf (ft2:face-named-instance face) 2) ; Black, wght 900
(setf (ft2:face-axis-coordinate face "wdth") 125) ; changed
(setf (ft2:face-named-instance face) nil) ; back to the defaults
(setf (ft2:face-named-instance face) 2) ; Black again: no effect
(ft2:face-design-coordinates face) ; => (400 100), not (900 100)
Setting (setf (ft2:face-design-coordinates face) '(900 100)) in place of the last selection has no effect either. Selecting another named instance, or setting other coordinates, before going back to the instance avoids the bug, as does opening the face again.
It was seen with FreeType 2.14.3, and the code responsible is the same in 2.13.1, the oldest FreeType cl-ft2 takes, and in FreeType's development sources of October 2026. Asked for no instance, FT_Set_Named_Instance fills its copy of the design coordinates from the named instance the face still records, while the coordinates it applies become the defaults. A later request for that instance's coordinates then matches the copy, so FreeType finds nothing to change and the defaults stay in force.
Effects
The coordinates in effect apply to everything loaded afterwards: outlines, metrics, advances, bitmaps and glyph-index-advance. Glyphs already copied into glyph objects or bitmaps keep the shape they had. Setting any of these places notifies face dependents, so a cl-harfbuzz/ft2 font follows the face's variations.
Reading or setting any of these places on a face without axes signals :invalid-argument (with freetype-error-function nil), except face-variable-p, face-variation-axes and face-named-instances, which return false or empty:
DejaVu Sans is not a variable font: it has no variation axes.
Check FACE-VARIABLE-P before reading or setting variations.
Example: a weight ramp
(ft2:with-face (face lib "Inter[opsz,wght].ttf")
(setf (ft2:face-size face) 48)
(loop for weight from 100 to 900 by 100
collect (progn (setf (ft2:face-axis-coordinate face "wght") weight)
(ft2:char-bitmap face #\g))))