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

Getting started

A guided tour. All output below is real, from DejaVu Sans (fonts-dejavu-core, 2048 units per EM) and HarfBuzz 12.3. It runs in a package of its own, which gives cl-harfbuzz the local nickname hb, and cl-ft2, which section 6 renders with, the nickname ft2:

(asdf:load-system :cl-harfbuzz)
(asdf:load-system :cl-harfbuzz/ft2)    ; for section 6; loads cl-ft2

(defpackage #:tour
  (:use #:cl)
  (:local-nicknames (#:hb #:cl-harfbuzz) (#:ft2 #:cl-ft2)))
(in-package #:tour)

1. Open a face and make a font

A face is one typeface of a font file. A font is a face at a scale (and, for variable fonts, a variation). Shaping and glyph queries take a font.

(defparameter *face* (hb:make-face "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"))
*face*                    ; => #<HB:FACE 6253 glyphs {...}>
(hb:face-units-per-em *face*)     ; => 2048

(defparameter *font* (hb:make-font *face*))
*font*                    ; => #<HB:FONT scale 2048x2048 {...}>

A new font's scale is the face's units per EM, so everything it reports is in font units.

In real code use the with- macros, which destroy the objects on exit:

(hb:with-face (face "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
  (hb:with-font (font face)
    ...))

2. Shape text

(hb:shape-text *font* "office")
;; => #(#S(SHAPED-GLYPH :INDEX 82   :CLUSTER 0 :X-ADVANCE 1253 :Y-ADVANCE 0 :X-OFFSET 0 :Y-OFFSET 0 :FLAGS NIL)
;;      #S(SHAPED-GLYPH :INDEX 5044 :CLUSTER 1 :X-ADVANCE 1980 ...)    ; "ffi" ligature
;;      #S(SHAPED-GLYPH :INDEX 70   :CLUSTER 4 :X-ADVANCE 1126 ...)
;;      #S(SHAPED-GLYPH :INDEX 72   :CLUSTER 5 :X-ADVANCE 1260 ...))

Six characters became four glyphs. The ligature's cluster is 1, the index of its first character; the next glyph's cluster jumps to 4. See Core concepts.

Features are switched with HarfBuzz's feature strings:

(map 'list #'hb:shaped-glyph-index (hb:shape-text *font* "office" :features '("-liga")))
;; => (82 73 73 76 70 72)       ; o f f i c e, no ligature

Kerning is on by default:

(map 'list #'hb:shaped-glyph-x-advance (hb:shape-text *font* "AV"))
;; => (1270 1401)   ; A's advance is 1401 unkerned; GPOS pulls V 131 units closer

Right-to-left text needs nothing special; direction and script are guessed:

(map 'list #'hb:shaped-glyph-cluster (hb:shape-text *font* "سلام"))
;; => (3 1 0)   ; visual order, left to right: the last character comes first

3. Place the glyphs

Advances and offsets have y up. The pen starts at 0 and moves by each advance; each glyph is drawn at pen + offset:

(let ((x 0) (y 0))
  (loop for g across (hb:shape-text *font* "Hello")
        collect (list (hb:shaped-glyph-index g)
                      (+ x (hb:shaped-glyph-x-offset g))
                      (+ y (hb:shaped-glyph-y-offset g)))
        do (incf x (hb:shaped-glyph-x-advance g))
           (incf y (hb:shaped-glyph-y-advance g))))

To convert font units to pixels at size s px, multiply by s / face-units-per-em. Or set the font's scale (see Fonts and metrics), or use an ft2-font that reports pixels directly (step 6).

4. Get outlines

(hb:glyph-outline *font* (hb:font-char-glyph *font* #\L))
;; => (((:MOVE-TO 201 1493) (:LINE-TO 403 1493) (:LINE-TO 403 170)
;;      (:LINE-TO 1130 170) (:LINE-TO 1130 0) (:LINE-TO 201 0) (:LINE-TO 201 1493)))

A list of contours, each a list of segments, in the same form as cl-ft2:glyph-outline. See Outlines.

5. Font-wide metrics

(hb:font-extents *font*)                     ; => 1901, -483, 0   (ascender, descender, line gap)
(hb:font-metric *font* :underline-offset)    ; => -40
(hb:face-name *face* :font-family)           ; => "DejaVu Sans"

6. Render with FreeType

With cl-harfbuzz/ft2, a font built on a cl-ft2 face shapes at the face's pixel size and reports pixels, so positions and bitmaps line up:

(ft2:with-library (lib)
  (ft2:with-face (face lib "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
    (setf (ft2:face-size face) 32)
    (cl-harfbuzz/ft2:with-ft2-font (font face)
      (let ((x 0))
        (loop for g across (hb:shape-text font "Hello")
              collect (list (+ x (hb:shaped-glyph-x-offset g))
                            (ft2:glyph-index-bitmap face (hb:shaped-glyph-index g)))
              do (incf x (hb:shaped-glyph-x-advance g)))))))
;; => ((0 #S(BITMAP :WIDTH 18 ...)) (385/16 #S(BITMAP ...)) (175/4 ...) ...)

Positions are exact rationals in pixels. See FreeType bridge.

7. Clean up

(hb:destroy-font *font*)
(hb:destroy-face *face*)

Where next

  • Shaping for buffers, buffer properties, cluster levels and flags.
  • Cookbook for complete recipes.
  • Scope and limitations: shaping takes one run of one direction and script. Splitting a paragraph into runs (bidi, script itemization) and breaking lines is cl-icu's job.