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.
Cookbook
Complete, tested recipes. They assume lib is a live library and face a face with a size set.
- Drawing a line of text
- Measuring a string
- Saving a bitmap as an image
- A glyph atlas
- Outlined, bordered and bold text
- Distance fields
- Subpixel positioning
- Hinted or unhinted layout
- Font fallback
- Fonts from memory
- Font collections
- Variable fonts
- Rendering shaped text
Drawing a line of text
Draws a string into an 8-bit canvas, using legacy kerning:
(defun render-line (face string)
"Return a 2D octet array holding STRING drawn in FACE at its current size."
(let* ((metrics (ft2:face-size-metrics face))
(ascender (ceiling (ft2:size-metrics-ascender metrics)))
(descender (floor (ft2:size-metrics-descender metrics)))
(glyphs (map 'list (lambda (ch) (ft2:char-index face ch)) string))
;; First pass: pen positions, with legacy kerning.
(pens (loop with pen = 0
for (left right) on glyphs
collect pen
do (incf pen (ft2:glyph-index-advance face left))
(when right
(incf pen (ft2:kerning face left right)))))
(width (ceiling (+ (car (last pens)) (ft2:glyph-index-advance face (car (last glyphs))))))
(canvas (make-array (list (- ascender descender) width)
:element-type '(unsigned-byte 8) :initial-element 0)))
;; Second pass: render each glyph and blend it in at its pen position.
(loop for glyph in glyphs
for pen in pens
for bitmap = (ft2:glyph-index-bitmap face glyph)
for x0 = (+ (round pen) (ft2:bitmap-left bitmap))
for y0 = (- ascender (ft2:bitmap-top bitmap))
do (dotimes (r (ft2:bitmap-height bitmap))
(dotimes (c (ft2:bitmap-width bitmap))
(let ((x (+ x0 c)) (y (+ y0 r)))
(when (array-in-bounds-p canvas y x)
(setf (aref canvas y x)
(max (aref canvas y x)
(aref (ft2:bitmap-data bitmap) r c))))))))
canvas))
At 16 px, (render-line face "AVATAR") gives a 19 × 61 canvas. Here it is thresholded at 128, with the blank rows above and below trimmed:
....###...##.......##...###...##########...###......#####....
....###....#.......#....###.......##.......###......#....##..
....#.#....##.....##....#.#.......##.......#.#......#....##..
...##.##...##.....##...##.##......##......##.##.....#....##..
...##.##....#.....#....##.##......##......##.##.....#....##..
...#...#....##...##....#...#......##......#...#.....#....##..
..##...##....#...#....##...##.....##.....##...##....######...
..#....##....##.##....#....##.....##.....#....##....#...##...
.#########...##.##...#########....##....#########...#....##..
.##.....##....###....##.....##....##....##.....##...#.....#..
.#.......#....###....#.......#....##....#.......#...#.....##.
##.......##...###...##.......##...##...##.......##..#......#.
The max blend keeps overlapping glyphs, such as a kerned AV, from erasing each other. For colored text, use the coverage as alpha.
Measuring a string
(defun text-width (face string &key (kerning t))
(loop for (left right) on (map 'list (lambda (ch) (ft2:char-index face ch)) string)
sum (+ (ft2:glyph-index-advance face left)
(if (and kerning right) (ft2:kerning face left right) 0))))
(text-width face "AVATAR") ; => 90 at 24 px
(text-width face "AVATAR" :kerning nil) ; => 96
glyph-index-advance does not touch the glyph slot, so measuring is cheap. The line height is (ft2:size-metrics-height (ft2:face-size-metrics face)).
Saving a bitmap as an image
A binary PGM needs no image library and opens in most viewers:
(defun write-pgm (array path)
"Write the 2D octet ARRAY (rows × columns) to PATH as a binary PGM."
(with-open-file (out path :direction :output :if-exists :supersede
:element-type '(unsigned-byte 8))
(let ((header (format nil "P5 ~D ~D 255~%"
(array-dimension array 1) (array-dimension array 0))))
(write-sequence (map 'vector #'char-code header) out))
(dotimes (y (array-dimension array 0))
(dotimes (x (array-dimension array 1))
(write-byte (aref array y x) out)))))
(write-pgm (ft2:bitmap-data (ft2:char-bitmap face #\g)) "g.pgm")
(write-pgm (render-line face "Hello, world") "hello.pgm")
A glyph atlas
A simple shelf packer that bakes a character set into one texture and records where each glyph is:
(defun build-atlas (face chars &key (size 256) (padding 1))
"Pack CHARS, rendered from FACE, into a SIZE × SIZE octet array. Return
the array and a hash table from character to its placement and metrics."
(let ((atlas (make-array (list size size) :element-type '(unsigned-byte 8)
:initial-element 0))
(entries (make-hash-table))
(x 0) (y 0) (shelf 0))
(loop for ch across chars
for bitmap = (ft2:char-bitmap face ch)
for w = (ft2:bitmap-width bitmap)
for h = (ft2:bitmap-height bitmap)
do (when (> (+ x w) size) ; next shelf
(setf x 0 y (+ y shelf padding) shelf 0))
(when (> (+ y h) size)
(error "Atlas of ~D × ~D is full at ~S." size size ch))
(dotimes (r h)
(dotimes (c w)
(setf (aref atlas (+ y r) (+ x c))
(aref (ft2:bitmap-data bitmap) r c))))
(setf (gethash ch entries)
(list :x x :y y :width w :height h
:left (ft2:bitmap-left bitmap) :top (ft2:bitmap-top bitmap)
:advance (ft2:glyph-linear-advance face)))
(incf x (+ w padding))
(setf shelf (max shelf h)))
(values atlas entries)))
(setf (ft2:face-size face) 24)
(multiple-value-bind (atlas entries)
(build-atlas face (coerce (loop for c from 32 below 127 collect (code-char c)) 'string))
(write-pgm atlas "atlas.pgm")
(gethash #\g entries))
;; => (:X 213 :Y 69 :WIDTH 13 :HEIGHT 18 :LEFT 1 :TOP 13 :ADVANCE 975/64)
char-bitmap leaves the glyph in the slot, so glyph-linear-advance right after it reads that glyph's advance. For a GPU, upload atlas as a single-channel texture. Each quad is drawn at (pen + left, baseline - top) with size width × height and texture coordinates x, y / size. Pack shaped glyph indexes instead of characters with glyph-index-bitmap.
Outlined, bordered and bold text
See Glyph objects and strokers for outlines, halos and synthetic bold. For a synthetic oblique, shear with face-transform-matrix:
(setf (ft2:face-transform-matrix face) '(1 1/5 0 1)) ; about 11°
(ft2:char-bitmap face #\A)
(setf (ft2:face-transform-matrix face) nil)
Distance fields
FreeType renders single-channel SDFs directly:
(setf (ft2:face-size face) 48)
(ft2:char-bitmap face #\A :mode :sdf) ; 128 = edge, more = inside
Pass the data to a shader that thresholds at 0.5 with smoothstep. Store left and top as usual: the bitmap includes the field's spread, 8 pixels by default, on every side. For multichannel SDF or GPU curve rendering, use glyph-outline with :no-scale as the input, since it is resolution-independent.
Subpixel positioning
Rasterising at fractional pen positions gives more even spacing in unhinted text. Render each glyph at a few phases and cache them:
(defun glyph-at-phase (face glyph-index phase)
"Render GLYPH-INDEX shifted right by PHASE, a fraction of a pixel."
(setf (ft2:face-transform-delta face) (list phase 0))
(unwind-protect (ft2:glyph-index-bitmap face glyph-index :target :light)
(setf (ft2:face-transform-delta face) nil)))
;; Draw at pen x = 10.3: use phase 1/4 (nearest quarter) at integer x 10.
Alternatively, copy the glyph with copy-glyph and pass :origin to render-glyph.
Hinted or unhinted layout
| Want | Load with | Advance with |
|---|---|---|
| Crisp UI text at small sizes | default (:target :normal) |
glyph-advance / glyph-index-advance (whole pixels) |
| Positions matching a shaper or another platform | :target :light, or :flags '(:no-hinting) |
glyph-linear-advance (exact) |
| Size-independent geometry | :flags '(:no-scale) |
glyph-advance, in font units |
Mixing hinted advances with unhinted glyphs, or the reverse, makes spacing uneven. Pick one row and stay on it.
Font fallback
(defun find-face-for (char faces)
"Return the first of FACES that has a glyph for CHAR, or the first face."
(or (find-if (lambda (face) (plusp (ft2:char-index face char))) faces)
(first faces)))
Give every face in the list the same size. For per-run fallback with shaping, see cl-textrender.
Fonts from memory
Fonts embedded in an executable or read from an archive:
(defparameter *font-octets*
(with-open-file (in "DejaVuSans.ttf" :element-type '(unsigned-byte 8))
(let ((v (make-array (file-length in) :element-type '(unsigned-byte 8))))
(read-sequence v in)
v)))
(ft2:with-face (face lib *font-octets*)
…)
The octets are copied into foreign memory that the face owns. You may reuse or discard the vector right away. Each open-face makes its own copy.
Font collections
(defun collection-faces (lib path)
"Return descriptions of every face in the font file at PATH."
(let ((n (ft2:with-face (f lib path) (ft2:face-num-faces f))))
(loop for i below n
collect (ft2:with-face (f lib path :index i)
(list i (ft2:face-family-name f) (ft2:face-style-name f))))))
Variable fonts
;; Every named instance of a variable font, rendered.
(ft2:with-face (face lib "Inter.ttf")
(setf (ft2:face-size face) 32)
(loop for instance in (ft2:face-named-instances face)
collect (progn (setf (ft2:face-named-instance face) (ft2:named-instance-index instance))
(cons (ft2:named-instance-name instance)
(ft2:char-bitmap face #\a)))))
;; Optical size follows the pixel size, when the font has an opsz axis.
(defun set-size-with-opsz (face pixels)
(setf (ft2:face-size face) pixels)
(when (find "opsz" (ft2:face-variation-axes face) :key #'ft2:var-axis-tag :test #'string=)
(setf (ft2:face-axis-coordinate face "opsz") pixels)))
See Font variations for the details.
Rendering shaped text
For real text, with GPOS kerning, ligatures, marks, Arabic, Indic and so on, shape with HarfBuzz, then render each output glyph by index:
;; GLYPHS is a list of (glyph-index x-offset y-offset x-advance) in pixels,
;; produced by a shaper whose font is at the same size as FACE.
(let ((pen-x 0) (pen-y 0))
(dolist (g glyphs)
(destructuring-bind (index dx dy advance) g
(let ((bitmap (ft2:glyph-index-bitmap face index :target :light)))
(blit bitmap (+ pen-x dx (ft2:bitmap-left bitmap))
(- baseline pen-y dy (ft2:bitmap-top bitmap))))
(incf pen-x advance))))
Use :target :light, or no hinting, so the rasterised shapes match the shaper's unhinted positions. With cl-harfbuzz/ft2, the HarfBuzz font is built on the same cl-ft2 face and follows its size and variations; see Face dependents.