3 Face Dependents
dxtr edited this page 2026-10-11 18:31:03 +02:00

Face dependents

Other C libraries can build objects that point into a face's FT_Face. The motivating case is HarfBuzz's hb_ft_font_create_referenced, which cl-harfbuzz/ft2 uses. Such an object has two needs:

  1. It must be destroyed before the FT_Face it points into, whether that happens through destroy-face or through destroy-library.
  2. It may need to know when the face's scaling changes: size, transform or variation coordinates.

cl-ft2 provides both through a small protocol.

Protocol

register-face-dependent face dependent → dependent

Registers dependent, any Lisp object, on face. From then on, cl-ft2 calls the two generic functions below on it. Registering the same object twice has no extra effect. The face must be live.

unregister-face-dependent face dependent

Stops the notifications, for example because the dependent was released first. Unregistering an object that is not registered is harmless.

release-face-dependent dependent face (generic, required)

Called by destroy-face, and so also by destroy-library, before the FT_Face is destroyed, while face-handle is still valid. Release the foreign object here. Every dependent class must have a method. There is no default, so a missing method is an error when the face is destroyed rather than a silent dangling pointer.

After the dependents are released, the face clears its list, so you do not need to call unregister-face-dependent from inside this method.

face-changed dependent face (generic, optional)

Called right after the face's size, transform or variation coordinates change, by setting face-size, face-strike, face-transform-matrix, face-transform-delta, face-design-coordinates, face-axis-coordinate, face-normalized-coordinates or face-named-instance. The default method does nothing.

Example: the HarfBuzz bridge

This is how cl-harfbuzz/ft2 ties a HarfBuzz font to a cl-ft2 face, abridged:

(defclass ft2-font (cl-harfbuzz:font)
  ((ft2-face :initarg :ft2-face :reader ft2-font-face)))

(defun make-ft2-font (face)
  (let ((handle (cl-harfbuzz/ffi:hb-ft-font-create-referenced (cl-ft2:face-handle face))))
    (cl-ft2:register-face-dependent
     face (make-instance 'ft2-font :handle handle :ft2-face face :scale-divisor 64))))

;; Destroyed explicitly first?  Stop listening.
(defmethod cl-harfbuzz:destroy-font :before ((font ft2-font))
  (cl-ft2:unregister-face-dependent (ft2-font-face font) font))

;; Face closing?  Destroy the hb_font_t while the FT_Face still exists.
(defmethod cl-ft2:release-face-dependent ((font ft2-font) face)
  (declare (ignore face))
  (cl-harfbuzz:destroy-font font))

;; Size, transform or variations changed?  Resynchronise HarfBuzz.
(defmethod cl-ft2:face-changed ((font ft2-font) face)
  (declare (ignore face))
  (when (cl-harfbuzz:font-live-p font)
    (cl-harfbuzz/ffi:hb-ft-font-changed (cl-harfbuzz:font-handle font))))

With this in place:

(ft2:with-library (lib)
  (ft2:with-face (face lib path)
    (setf (ft2:face-size face) 32)
    (cl-harfbuzz/ft2:with-ft2-font (font face)
      ;; shape with FONT; glyph indexes go to ft2:glyph-index-bitmap
      (setf (ft2:face-axis-coordinate face "wght") 700)   ; FONT follows automatically
      …)))

Shaping through hb-ft loads glyphs into the face's glyph slot, so do not hold slot state across a shaping call.

Writing your own

A minimal dependent that caches per-size data and drops it when the face changes:

(defclass metrics-cache ()
  ((table :initform (make-hash-table) :reader cache-table)))

(defmethod ft2:release-face-dependent ((cache metrics-cache) face)
  (declare (ignore face))
  (clrhash (cache-table cache)))

(defmethod ft2:face-changed ((cache metrics-cache) face)
  (declare (ignore face))
  (clrhash (cache-table cache)))

(defun attach-cache (face)
  (ft2:register-face-dependent face (make-instance 'metrics-cache)))