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.