5 Getting Started
dxtr edited this page 2026-10-18 15:36:10 +02:00

Getting started

This tour uses DejaVu Sans (fonts-dejavu-core on Debian and Ubuntu). Every result shown is real output from FreeType 2.14.3.

(asdf:load-system :cl-ft2)

(defpackage #:example
  (:use #:cl)
  (:local-nicknames (#:ft2 #:cl-ft2)))
(in-package #:example)

(defparameter *font* "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")

The examples in this wiki call cl-ft2 by the local nickname ft2, given in a package's :local-nicknames as here. cl-ft2 defines no global nickname.

1. A library and a face

All FreeType work happens inside a library. A face is one typeface from a font file:

(defparameter *lib* (ft2:make-library))
(defparameter *face* (ft2:open-face *lib* *font*))

*face*                               ; => #<FACE "DejaVu Sans" "Book" {…}>
(ft2:face-num-glyphs *face*)         ; => 6253
(ft2:face-units-per-em *face*)       ; => 2048
(ft2:face-flags *face*)
;; => (:SCALABLE :SFNT :HORIZONTAL :KERNING :GLYPH-NAMES :HINTER)

In real code, prefer with-library and with-face. They release the objects when the body exits, even through a non-local exit:

(ft2:with-library (lib)
  (ft2:with-face (face lib *font*)
    …))

2. Choose a size

A face has no size until you give it one. Set it in pixels per em:

(setf (ft2:face-size *face*) 32)
(ft2:face-size-metrics *face*)
;; => #S(SIZE-METRICS :X-PPEM 32 :Y-PPEM 32 :X-SCALE 1/64 :Y-SCALE 1/64
;;                    :ASCENDER 30 :DESCENDER -8 :HEIGHT 37 :MAX-ADVANCE 60)

For a size in points, convert: 12 pt at 96 dpi is (setf (ft2:face-size *face*) (* 12 96/72)), 16 pixels per em. Sizes may be fractional.

3. Render a character

char-bitmap loads, renders and copies the glyph in one call:

(setf (ft2:face-size *face*) 8)
(ft2:char-bitmap *face* #\A)
;; => #S(BITMAP :WIDTH 6 :HEIGHT 6 :PIXEL-MODE :GRAY :NUM-GRAYS 256 :LEFT 0 :TOP 6
;;              :DATA #2A((0   1 223  94   0  0)
;;                        (0  67 148 183   0  0)
;;                        (0 163  45 177  30  0)
;;                        (12 204  0  92 122  0)
;;                        (97 255 255 255 217 0)
;;                        (189 42  0   0 172 56)))

data is indexed (row column), with the top row first. left and top place the bitmap relative to the pen position on the baseline. Here the bitmap starts 0 pixels right of the pen, and its top row is 6 pixels above the baseline. For a 1-bit bitmap, pass :mode :mono:

(ft2:bitmap-data (ft2:char-bitmap *face* #\A :mode :mono))
;; => #2A((0 0 1 0 0) (0 0 1 0 0) (0 1 0 1 0) (0 1 0 1 0) (0 1 1 1 0) (1 0 0 0 1))

4. Load a glyph and inspect it

Each face has a single glyph slot. load-char and load-glyph load a glyph into it, and the glyph-… readers report on the glyph in the slot:

(setf (ft2:face-size *face*) 32)
(ft2:load-char *face* #\A)           ; => 36, the glyph index
(ft2:glyph-format *face*)            ; => :OUTLINE
(ft2:glyph-metrics *face*)
;; => #S(GLYPH-METRICS :WIDTH 22 :HEIGHT 23 :HORI-BEARING-X 0 :HORI-BEARING-Y 23
;;                     :HORI-ADVANCE 22 :VERT-BEARING-X -11 :VERT-BEARING-Y 4
;;                     :VERT-ADVANCE 32)
(ft2:glyph-advance *face*)           ; => 22, 0      (hinted)
(ft2:glyph-linear-advance *face*)    ; => 1401/64, 32 (unhinted: 21.890625 px)

Distances are exact rationals in pixels. FreeType's 26.6 fixed point (1/64 pixel) becomes 1401/64 rather than a float. Whole-pixel values come back as integers.

5. Outlines

Before it is rendered, a glyph from a scalable font is a vector outline:

(ft2:load-char *face* #\l)
(ft2:glyph-outline *face*)
;; => (((:MOVE-TO 193/64 24) (:LINE-TO 377/64 24) (:LINE-TO 377/64 0)
;;      (:LINE-TO 193/64 0) (:LINE-TO 193/64 24)))

The result is a list of contours, and each contour is a list of segments. Curved glyphs use :conic-to (quadratic) and :cubic-to segments. See Outlines.

6. Kerning

(ft2:kerning *face* (ft2:char-index *face* #\A) (ft2:char-index *face* #\V))
;; => -2, 0

This reads only the legacy TrueType kern table. Real text layout, with GPOS kerning, ligatures and complex scripts, needs a shaper such as HarfBuzz. See Scope and limitations.

7. Clean up

(ft2:destroy-library *lib*)   ; also closes *face* and anything else made in *lib*

After that, using *face* signals a freetype-error named :invalid-face-handle. It never touches freed memory:

(ft2:face-family-name *face*)
;; error: The face has been destroyed, by DESTROY-FACE or by DESTROY-LIBRARY on its library.
;;        Open the font again with OPEN-FACE, and keep the face open while
;;        you use it.

Where next

  • Core concepts explains units, coordinate directions and lifetimes in detail.
  • Cookbook shows how to draw a whole line of text into a buffer, build a glyph atlas, and draw outlined text.