6 Core Concepts
dxtr edited this page 2026-10-18 15:36:10 +02:00

Core concepts

Objects and ownership

cl-ft2 has four kinds of object. Each wraps a FreeType handle:

Class Wraps Made by Destroyed by
library FT_Library make-library, with-library destroy-library
face FT_Face open-face, with-face destroy-face
glyph FT_Glyph copy-glyph, with-glyph destroy-glyph
stroker FT_Stroker make-stroker, with-stroker destroy-stroker

A library owns everything created in it. destroy-library first destroys every face, glyph and stroker that is still live, then the library itself. Destroying a library therefore never leaves another object holding a dangling pointer.

A glyph does not depend on the face it was copied from. It belongs to the face's library, so it outlives destroy-face but not destroy-library.

Destruction is explicit. cl-ft2 does not use finalizers. Use the with- macros, which destroy in an unwind-protect, or call the destroy- functions yourself. Each destroy- function is idempotent, so destroying an object twice is harmless.

Using a destroyed object

Every operation checks that its object is still live. A destroyed object signals a freetype-error and never touches freed memory:

Object Error name
library :invalid-library-handle
face :invalid-face-handle
glyph, stroker :invalid-handle

library-live-p, face-live-p, glyph-live-p and stroker-live-p test liveness without signalling. A destroyed object prints as #<FACE destroyed>, #<GLYPH destroyed>, and so on.

The glyph slot

FreeType gives each face one glyph slot. load-glyph and load-char overwrite it, and these readers report on the glyph loaded last:

glyph-index, glyph-format, glyph-metrics, glyph-advance, glyph-linear-advance, glyph-outline, glyph-control-box, render-glyph, glyph-bitmap

So the slot holds one glyph at a time. To keep a glyph, either copy out what you need (glyph-bitmap and glyph-outline return fresh Lisp data), or make a standalone glyph object with copy-glyph.

Naming: glyph-… and glyph-index-…

The names follow one rule:

  • glyph-… functions take a glyph source: either a face, meaning the glyph in its slot, or a glyph object. These are all of the readers above, plus copy-glyph. They are generic functions.
  • glyph-index-… functions take a face and a glyph index, and leave the slot alone: glyph-index-bitmap, glyph-index-advance and glyph-index-name. (glyph-index-bitmap does reuse the slot to render.)

Lookups that produce a glyph index are char-index (from a character) and name-index (from a glyph name).

Other code that uses the face's FT_Face also loads into the same slot. For example, shaping with a cl-harfbuzz/ft2 font replaces whatever glyph cl-ft2 loaded last.

Units

cl-ft2 converts FreeType's fixed-point numbers into exact rationals, so no precision is lost to floating point:

FreeType Meaning cl-ft2
FT_F26Dot6, scaled FT_Pos 1/64 pixel rational pixels: 807/64 = 12.609375
FT_Fixed (16.16) scale factors, linear advances, variation coordinates rationals: 1/2, 650
unscaled FT_Pos, FT_Short font units integers

Use (float x) when you need a float. Rounding happens only on input: cl-ft2 rounds the reals you pass to the nearest 1/64 (positions, char sizes) or 1/65536 (matrices, coordinates).

Which unit you get depends on what you ask for:

Value Unit
Face-wide metrics: face-ascender, face-descender, face-height, face-bounding-box, face-max-advance-…, face-underline-… font units (see face-units-per-em)
face-size-metrics: ascender, descender, height, max-advance pixels at the current size
size-metrics-x-scale, -y-scale pixels per font unit: multiply a font-unit distance by it
Glyph metrics, glyph-advance, outlines, control boxes pixels, or font units if the glyph was loaded with :no-scale
glyph-linear-advance pixels, or font units if loaded with :linear-design
glyph-index-advance pixels, or font units with :no-scale in :flags
kerning pixels (:default, :unfitted), or font units (:unscaled)
Bitmap width, height, left, top whole pixels

cl-ft2 remembers the flags of the most recent load on each face, so glyph-slot readers always use the right unit. A glyph object remembers whether it was copied from a :no-scale load, and its outline, control box, advance, transform-glyph delta and stroker radius use the same unit.

Coordinate directions

  • Outlines, metrics and control boxes are Y-up. The baseline is y = 0, ascent is positive and descent is negative, as in the font itself.
  • Bitmaps are stored top row first. (aref data row column) with row 0 at the top. cl-ft2 normalises FreeType's pitch, so you never see bottom-up rows.
  • bitmap-top counts up from the baseline to the top row. To place a bitmap on a Y-down canvas with the pen at (pen-x, baseline-y), its top-left pixel goes at (pen-x + left, baseline-y - top).

Sizes and the current state of a face

A face carries mutable state that affects every later load:

  • the size (face-size, or face-strike for a bitmap strike)
  • the transform (face-transform-matrix, face-transform-delta)
  • the variation coordinates, for variable fonts (see Font variations)
  • the current charmap ((setf face-charmap))

To render one font at two sizes at once, open the font twice. Two faces are independent, and opening the same file twice is cheap.

Threads

FreeType objects are not thread-safe. Use one library per thread, with its own faces, or serialise all access to a library and everything made in it. Glyphs and bitmaps that have been copied into Lisp (bitmap, outline lists, metric structs) are plain Lisp data and can be shared freely, as are outline objects of your own; stroke-outline and render-outline use a stroker and a library, under the same rules.

Input validation

Bad arguments are reported before they reach FreeType:

  • An unknown keyword (load flag, render mode, encoding, kerning mode, line cap, …) signals a standard type-error that lists the valid keywords.
  • An argument of the wrong type, such as a negative size or a glyph where a face belongs, signals a type-error. Every exported function declares the types of its arguments and results, so SBCL also warns at compile time about a constant argument of the wrong type.
  • A glyph index out of range, a strike that does not exist, an outline asked of a bitmap, or a variation on a font without axes signals a freetype-error with freetype-error-function set to nil. See Errors.