goose121.clx-truetype/clx-truetype.lisp
Morgan Hager 4ac0f524a4
Add *allow-fixed-pitch-p*
Add a defvar to control optimization for fixed-pitch fonts, which can
break CJK.
2025-02-11 23:08:48 -07:00

706 lines
34 KiB
Common Lisp

;;;; clx-truetype.lisp
(in-package #:clx-truetype)
(defclass font ()
((family :type string :initarg :family :accessor font-family :documentation "Font family.")
(subfamily :type string :initarg :subfamily :accessor font-subfamily :documentation "Font subfamily. For e.g. regular, italic, bold, bold italib.")
(size :type real :initarg :size :accessor font-size :initform 12 :documentation "Font size in points.")
(underline :type boolean :initarg :underline :initform nil :accessor font-underline :documentation "Draw line under text string.")
(strikethrough :type boolean :initarg :strikethrough :initform nil :accessor font-strikethrough :documentation "Draw strike through text string.")
(overline :type boolean :initarg :overline :initform nil :accessor font-overline :documentation "Draw line over text string.")
(background :initarg :background :initform nil :accessor font-background :documentation "Background color.")
(foreground :initarg :foreground :initform nil :accessor font-foreground :documentation "Foreground color.")
(overwrite-gcontext :type boolean :initarg :overwrite-gcontext :initform nil
:accessor font-overwrite-gcontext :documentation "Use font values for background and foreground colors.")
(antialias :type boolean :initarg :antialias :initform t :accessor font-antialias :documentation "Antialias text string.")
(string-bboxes :type cacle:cache :accessor font-string-bboxes
:documentation "Cache for text bboxes")
(string-line-bboxes :type cacle:cache :accessor font-string-line-bboxes
:documentation "Cache for text line bboxes")
(string-alpha-maps :type cacle:cache :accessor font-string-alpha-maps
:documentation "Cache for text alpha maps")
(string-line-alpha-maps :type cacle:cache :accessor font-string-line-alpha-maps
:documentation "Cache for text line alpha maps"))
(:documentation "Class for representing font information."))
(defun check-valid-font-families (family subfamily)
(when (or (null (gethash family *font-cache*))
(null (gethash subfamily (gethash family *font-cache*))))
(error "Font is not found: ~A ~A" family subfamily)))
(defmethod initialize-instance :before
((instance font) &rest initargs &key family subfamily &allow-other-keys)
(declare (ignorable initargs))
(check-valid-font-families family subfamily))
(defun make-font-cache (font dpi-cache-size string-cache-size inner-provider)
(flet ((outer-provider (dpi-cons)
(values
(cacle:make-cache
string-cache-size
(lambda (string)
(values
(funcall inner-provider (car dpi-cons) (cdr dpi-cons) font string)
(length string)))
:test #'equal
:policy :lfu)
1)))
(cacle:make-cache
dpi-cache-size
#'outer-provider
:test #'equal
:policy :lfu)))
(defun font-cache-fetch (cache dpi string)
(cacle:cache-fetch (cacle:cache-fetch cache dpi) string))
(defmethod initialize-instance :after
((font font) &key (dpi-cache-size 10) (string-cache-size 1000) &allow-other-keys)
(setf
(font-string-bboxes font)
(make-font-cache font dpi-cache-size string-cache-size 'text-bounding-box-provider)
(font-string-line-bboxes font)
(make-font-cache font dpi-cache-size string-cache-size 'text-line-bounding-box-provider)
(font-string-alpha-maps font)
(make-font-cache font dpi-cache-size string-cache-size 'text-pixarray-provider)
(font-string-line-alpha-maps font)
(make-font-cache font dpi-cache-size string-cache-size 'text-line-pixarray-provider)))
(defmethod (setf font-family) :before
(family (instance font))
(check-valid-font-families family (font-subfamily instance)))
(defmethod (setf font-subfamily) :before
(subfamily (instance font))
(check-valid-font-families (font-family instance) subfamily))
(defmethod (setf font-family) :after
(family (font font))
(cacle:cache-flush (font-string-bboxes font))
(cacle:cache-flush (font-string-line-bboxes font)))
(defmethod (setf font-subfamily) :after
(subfamily (font font))
(cacle:cache-flush (font-string-bboxes font))
(cacle:cache-flush (font-string-line-bboxes font)))
(defmethod (setf font-size) :after (value (font font))
(cacle:cache-flush (font-string-bboxes font))
(cacle:cache-flush (font-string-line-bboxes font)))
(defmethod (setf font-underline) :after (value (font font))
(cacle:cache-flush (font-string-bboxes font)))
(defmethod (setf font-overline) :after (value (font font))
(cacle:cache-flush (font-string-bboxes font)))
(defgeneric font-equal (font1 font2)
(:documentation "Returns t if two font objects are equal, else returns nil.")
(:method ((font1 font) (font2 font))
(and (string-equal (font-family font1)
(font-family font2))
(string-equal (font-subfamily font1)
(font-subfamily font2))
(= (font-size font1) (font-size font2))
(eql (font-underline font1) (font-underline font2))
(eql (font-strikethrough font1) (font-strikethrough font2))
(eql (font-overline font1) (font-overline font2))
(equal (font-background font1) (font-background font2))
(equal (font-foreground font1) (font-foreground font2))
(eql (font-overwrite-gcontext font1) (font-overwrite-gcontext font2))
(eql (font-antialias font1) (font-antialias font2)))))
(defmethod print-object ((instance font) stream)
"Pretty printing font object"
(with-slots (family subfamily underline strikethrough
overline background foreground overwrite-gcontext
antialias)
instance
(if *print-readably*
(format stream
"#.(~S '~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S)"
'cl:make-instance 'font
:family family :subfamily subfamily :underline underline
:strikethrough strikethrough
:overline overline :background background :foreground foreground
:overwrite-gcontext overwrite-gcontext
:antialias antialias)
(format stream
"#<'~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S ~S>"
'font
:family family :subfamily subfamily :underline underline
:strikethrough strikethrough
:overline overline :background background :foreground foreground
:overwrite-gcontext overwrite-gcontext
:antialias antialias))))
;;; ZPB-TTF font objects cache
(defun get-font-pathname (font)
(gethash (font-subfamily font) (gethash (font-family font) *font-cache*)))
(defvar *font-loader-cache* (make-hash-table :test 'equal))
(defmacro with-font-loader ((loader font) &body body)
(let ((exists-p (gensym))
(font-path (gensym)))
`(let ((,font-path (get-font-pathname ,font)))
(multiple-value-bind (,loader ,exists-p)
(gethash ,font-path *font-loader-cache*)
(unless ,exists-p
(setf ,loader (setf (gethash ,font-path *font-loader-cache*)
(zpb-ttf:open-font-loader ,font-path))))
,@body))))
;;; Screen DPI
(defun screen-default-dpi (screen)
"Returns default dpi for @var{screen}. pixel width * 25.4/millimeters width"
(values (floor (* (xlib:screen-width screen) 25.4)
(xlib:screen-width-in-millimeters screen))
(floor (* (xlib:screen-height screen) 25.4)
(xlib:screen-height-in-millimeters screen))))
(defun screen-dpi (screen)
"Returns current dpi for @var{screen}."
(values (getf (xlib:screen-plist screen) :dpi-x
(floor (* (xlib:screen-width screen) 25.4)
(xlib:screen-width-in-millimeters screen)))
(getf (xlib:screen-plist screen) :dpi-y
(floor (* (xlib:screen-height screen) 25.4)
(xlib:screen-height-in-millimeters screen)))))
(defun (setf screen-dpi) (value screen)
"Sets current dpi for @var{screen}."
(setf (getf (xlib:screen-plist screen) :dpi-x) value
(getf (xlib:screen-plist screen) :dpi-y) value))
;;; Font metrics
(defun font-units->pixels-x (dpi-x font)
"px = funits*coeff. Function returns coeff."
(with-font-loader (loader font)
(with-slots (size) font
(let* ((units/em (zpb-ttf:units/em loader))
(pixel-size-x (* size (/ dpi-x 72))))
(* pixel-size-x (/ units/em))))))
(defun font-units->pixels-y (dpi-y font)
"px = funits*coeff. Function returns coeff."
(with-font-loader (loader font)
(with-slots (size) font
(let* ((units/em (zpb-ttf:units/em loader))
(pixel-size-y (* size (/ dpi-y 72))))
(* pixel-size-y (/ units/em))))))
(defun font-ascent-for-dpi (dpi-y font)
(with-font-loader (loader font)
(ceiling (* (font-units->pixels-y dpi-y font) (zpb-ttf:ascender loader)))))
(defun font-descent-for-dpi (dpi-y font)
(with-font-loader (loader font)
(floor (* (font-units->pixels-y dpi-y font) (zpb-ttf:descender loader)))))
(defun font-ascent (drawable font)
"Returns ascent of @var{font}. @var{drawable} must be window, pixmap or screen."
(font-ascent-for-dpi (nth-value 1 (screen-dpi (drawable-screen drawable))) font))
(defun font-descent (drawable font)
"Returns descent of @var{font}. @var{drawable} must be window, pixmap or screen."
(font-descent-for-dpi (nth-value 1 (screen-dpi (drawable-screen drawable))) font))
(defun font-line-gap (drawable font)
"Returns line gap of @var{font}. @var{drawable} must be window, pixmap or screen."
(with-font-loader (loader font)
(ceiling (* (font-units->pixels-y drawable font) (zpb-ttf:line-gap loader)))))
;;; baseline-to-baseline = ascent - descent + line gap
(defun baseline-to-baseline (drawable font)
"Returns distance between baselines of @var{font}. @var{drawable} must be window, pixmap or screen. ascent - descent + line gap"
(+ (font-ascent drawable font) (- (font-descent drawable font))
(font-line-gap drawable font)))
(defun text-bounding-box-provider (dpi-x dpi-y font string)
(with-font-loader (loader font)
(let* ((bbox
(zpb-ttf:string-bounding-box string loader))
(units->pixels-x (font-units->pixels-x dpi-x font))
(units->pixels-y (font-units->pixels-y dpi-y font))
(xmin (zpb-ttf:xmin bbox))
(ymin (zpb-ttf:ymin bbox))
(xmax (zpb-ttf:xmax bbox))
(ymax (zpb-ttf:ymax bbox)))
(when (font-underline font)
(setf ymin (min ymin (- (zpb-ttf:underline-position loader)
(zpb-ttf:underline-thickness loader)))))
(when (font-overline font)
(setf ymax (max ymax (+ (zpb-ttf:ascender loader)
(zpb-ttf:underline-position loader)
(+ (zpb-ttf:underline-thickness loader))))))
(vector (floor (* xmin
units->pixels-x))
(floor (* ymin
units->pixels-y))
(ceiling (* xmax
units->pixels-x))
(ceiling (* ymax
units->pixels-y))))))
(defun text-bounding-box (drawable font string &key start end)
"Returns text bounding box. @var{drawable} must be window, pixmap or screen. Text bounding box is only for contours. Bounding box for space (#x20) is zero."
(when (and start end)
(setf string (subseq string start end)))
(font-cache-fetch (font-string-bboxes font)
(multiple-value-call #'cons (screen-dpi (drawable-screen drawable)))
string))
(defun text-width (drawable font string &key start end)
"Returns width of text bounding box. @var{drawable} must be window, pixmap or screen."
(when (and start end)
(setf string (subseq string start end)))
(let ((bbox (text-bounding-box drawable font string)))
(- (xmax bbox) (xmin bbox))))
(defun text-height (drawable font string &key start end)
"Returns height of text bounding box. @var{drawable} must be window, pixmap or screen."
(when (and start end)
(setf string (subseq string start end)))
(let ((bbox (text-bounding-box drawable font string)))
(- (ymax bbox) (ymin bbox))))
(defvar *allow-fixed-pitch-p* t
"Allow shortcutting fixed-pitch fonts. Somewhat faster, but breaks CJK
in fixed-pitch fonts.")
(defun text-line-bounding-box-provider (dpi-x dpi-y font string)
(with-font-loader (loader font)
(let* ((units->pixels-x (font-units->pixels-x dpi-x font))
(xmin 0)
(ymin (font-descent-for-dpi dpi-y font))
(ymax (font-ascent-for-dpi dpi-y font))
(string-length (length string))
(xmax (if (> string-length 0)
(zpb-ttf:advance-width (zpb-ttf:find-glyph (elt string 0) loader))
0)))
(if (and *allow-fixed-pitch-p* (zpb-ttf:fixed-pitch-p loader))
(setf xmax (* xmax string-length))
(do ((i 1 (1+ i)))
((>= i string-length))
(incf xmax
(+ (zpb-ttf:advance-width (zpb-ttf:find-glyph (elt string i) loader))
(zpb-ttf:kerning-offset (elt string (1- i)) (elt string i) loader)))))
(vector (floor (* xmin units->pixels-x))
ymin
(ceiling (* xmax
units->pixels-x))
ymax))))
(defun text-line-bounding-box (drawable font string &key start end)
"Returns text line bounding box. @var{drawable} must be window, pixmap or screen. Text line bounding box is bigger than text bounding box. It's height is ascent + descent, width is sum of advance widths minus sum of kernings."
(when (and start end)
(setf string (subseq string start end)))
(font-cache-fetch (font-string-line-bboxes font)
(multiple-value-call #'cons (screen-dpi (drawable-screen drawable)))
string))
(defun text-line-width (drawable font string &key start end)
"Returns width of text line bounding box. @var{drawable} must be window, pixmap or screen. It is sum of advance widths minus sum of kernings."
(when (and start end)
(setf string (subseq string start end)))
(let ((bbox (text-line-bounding-box drawable font string)))
(- (xmax bbox) (xmin bbox))))
(defun text-line-height (drawable font string &key start end)
"Returns height of text line bounding box. @var{drawable} must be window, pixmap or screen."
(when (and start end)
(setf string (subseq string start end)))
(let ((bbox (text-line-bounding-box drawable font string)))
(- (ymax bbox) (ymin bbox))))
(defun xmin (bounding-box)
"Returns left side x of @var{bounding-box}"
(typecase bounding-box
(vector (elt bounding-box 0))))
(defun ymin (bounding-box)
"Returns bottom side y of @var{bounding-box}"
(typecase bounding-box
(vector (elt bounding-box 1))))
(defun xmax (bounding-box)
"Returns right side x of @var{bounding-box}"
(typecase bounding-box
(vector (elt bounding-box 2))))
(defun ymax (bounding-box)
"Returns top side y of @var{bounding-box}"
(typecase bounding-box
(vector (elt bounding-box 3))))
;;; Font rendering
(defun clamp (value min max)
"Clamps the value 'value' into the range [min,max]."
(max min (min max value)))
(defun make-state (font)
"Wrapper around antialising and not antialiasing renderers."
(if (font-antialias font)
(aa:make-state)
(aa-bin:make-state)))
(defun aa-bin/update-state (state paths)
"Update state for not antialiasing renderer."
(if (listp paths)
(dolist (path paths)
(aa-bin/update-state state path))
(let ((iterator (paths:path-iterator-segmented paths)))
(multiple-value-bind (i1 k1 e1) (paths:path-iterator-next iterator)
(declare (ignore i1))
(when (and k1 (not e1))
;; at least 2 knots
(let ((first-knot k1))
(loop
(multiple-value-bind (i2 k2 e2) (paths:path-iterator-next iterator)
(declare (ignore i2))
(aa-bin:line-f state
(paths:point-x k1) (paths:point-y k1)
(paths:point-x k2) (paths:point-y k2))
(setf k1 k2)
(when e2
(return))))
(aa-bin:line-f state
(paths:point-x k1) (paths:point-y k1)
(paths:point-x first-knot) (paths:point-y first-knot)))))))
state)
(defun update-state (font state paths)
"Wrapper around antialising and not antialiasing renderers."
(if (font-antialias font)
(vectors:update-state state paths)
(aa-bin/update-state state paths)))
(defun cells-sweep (font state function &optional function-span)
"Wrapper around antialising and not antialiasig renderers."
(if (font-antialias font)
(aa:cells-sweep state function function-span)
(aa-bin:cells-sweep state function function-span)))
(defun text-pixarray-provider (dpi-x dpi-y font string)
(with-font-loader (font-loader font)
(let* ((bbox (font-cache-fetch (font-string-bboxes font) (cons dpi-x dpi-y) string))
(min-x (xmin bbox))
(min-y (ymin bbox))
(max-x (xmax bbox))
(max-y (ymax bbox))
(width (- max-x min-x))
(height (- max-y min-y)))
(if (or (= 0 width) (= 0 height))
(list nil 0 0 0 0)
(let* ((units->pixels-x (font-units->pixels-x dpi-x font))
(units->pixels-y (font-units->pixels-y dpi-y font))
(array (make-array (list height width)
:initial-element 0
:element-type '(unsigned-byte 8)))
(state (make-state font))
(paths (paths-ttf:paths-from-string font-loader string
:offset (paths:make-point (- min-x)
max-y)
:scale-x units->pixels-x
:scale-y (- units->pixels-y))))
(when (font-underline font)
(let* ((thickness (* units->pixels-y (zpb-ttf:underline-thickness font-loader)))
(underline-offset (* units->pixels-y (zpb-ttf:underline-position font-loader)))
(underline-path (paths:make-rectangle-path 0 (+ max-y (- underline-offset))
max-x (+ max-y (- underline-offset) thickness))))
(push underline-path paths)))
(when (font-strikethrough font)
(let* ((thickness (* units->pixels-y (zpb-ttf:underline-thickness font-loader)))
(underline-offset (* 2 units->pixels-y (zpb-ttf:underline-position font-loader)))
(line-path (paths:make-rectangle-path 0 (+ max-y underline-offset) max-x (+ max-y underline-offset thickness))))
(push line-path paths)))
(when (font-overline font)
(let* ((thickness (* units->pixels-y (zpb-ttf:underline-thickness font-loader)))
(underline-offset (* units->pixels-y (zpb-ttf:underline-position font-loader)))
(ascend (* units->pixels-y (zpb-ttf:ascender font-loader)))
(overline-path (paths:make-rectangle-path 0 (- max-y ascend underline-offset)
max-x
(- max-y ascend underline-offset thickness))))
(push overline-path paths)))
(update-state font state paths)
(cells-sweep font state
(lambda (x y alpha)
(when (and (<= 0 x (1- width))
(<= 0 y (1- height)))
(setf alpha (min 255 (abs alpha))
(aref array y x) (clamp
(floor (+ (* (- 256 alpha) (aref array y x))
(* alpha 255))
256)
0 255)))))
(list array
min-x
max-y
width
height))))))
(defun text-pixarray (drawable font string)
"Render a text string of 'face', returning a 2D (unsigned-byte 8) array
suitable as an alpha mask, and dimensions. This function returns five
values: alpha mask byte array, x-origin, y-origin (subtracted from
position before rendering), horizontal and vertical advances.
@var{drawable} must be window or pixmap."
(values-list
(font-cache-fetch
(font-string-alpha-maps font)
(multiple-value-call #'cons (screen-dpi (drawable-screen drawable)))
string)))
(defun text-line-pixarray-provider (dpi-x dpi-y font string)
(with-font-loader (font-loader font)
(let* ((bbox (font-cache-fetch (font-string-line-bboxes font) (cons dpi-x dpi-y) string))
(min-x (xmin bbox))
(min-y (ymin bbox))
(max-x (xmax bbox))
(max-y (ymax bbox))
(width (- max-x min-x))
(height (- max-y min-y)))
(if (or (= 0 width) (= 0 height))
(list nil 0 0 0 0)
(let* ((units->pixels-x (font-units->pixels-x dpi-x font))
(units->pixels-y (font-units->pixels-y dpi-y font))
(array (make-array (list height width)
:initial-element 0
:element-type '(unsigned-byte 8)))
(state (make-state font))
(paths (paths-ttf:paths-from-string font-loader string
:offset (paths:make-point (- min-x)
max-y)
:scale-x units->pixels-x
:scale-y (- units->pixels-y))))
(when (font-underline font)
(let* ((thickness (* units->pixels-y (zpb-ttf:underline-thickness font-loader)))
(underline-offset (* units->pixels-y (zpb-ttf:underline-position font-loader)))
(underline-path (paths:make-rectangle-path 0 (+ max-y (- underline-offset))
max-x (+ max-y (- underline-offset) thickness))))
(push underline-path paths)))
(when (font-strikethrough font)
(let* ((thickness (* units->pixels-y (zpb-ttf:underline-thickness font-loader)))
(underline-offset (* 2 units->pixels-y (zpb-ttf:underline-position font-loader)))
(line-path (paths:make-rectangle-path 0 (+ max-y underline-offset) max-x (+ max-y underline-offset thickness))))
(push line-path paths)))
(when (font-overline font)
(let* ((thickness (* units->pixels-y (zpb-ttf:underline-thickness font-loader)))
(underline-offset (* units->pixels-y (zpb-ttf:underline-position font-loader)))
(ascend (* units->pixels-y (zpb-ttf:ascender font-loader)))
(overline-path (paths:make-rectangle-path 0 (- max-y ascend underline-offset)
max-x
(- max-y ascend underline-offset thickness))))
(push overline-path paths)))
(update-state font state paths)
(cells-sweep font state
(lambda (x y alpha)
(when (and (<= 0 x (1- width))
(<= 0 y (1- height)))
(setf alpha (min 255 (abs alpha))
(aref array y x) (clamp
(floor (+ (* (- 256 alpha) (aref array y x))
(* alpha 255))
256)
0 255)))))
(list array
min-x
max-y
width
height))))))
(defun text-line-pixarray (drawable font string)
"Render a text line of 'face', returning a 2D (unsigned-byte 8) array
suitable as an alpha mask, and dimensions. This function returns five
values: alpha mask byte array, x-origin, y-origin (subtracted from
position before rendering), horizontal and vertical advances.
@var{drawable} must be window or pixmap."
(values-list
(font-cache-fetch
(font-string-line-alpha-maps font)
(multiple-value-call #'cons (screen-dpi (drawable-screen drawable)))
string)))
(defun update-foreground (drawable gcontext font)
"Lazy updates foreground for drawable. @var{drawable} must be window or pixmap."
(let ((pixmap (or (getf (xlib:drawable-plist drawable) :ttf-pen-surface)
(setf (getf (xlib:drawable-plist drawable) :ttf-pen-surface)
(xlib:create-pixmap
:drawable drawable
:depth (xlib:drawable-depth drawable)
:width 1 :height 1)))))
(let ((color (the xlib:card32
(if (font-overwrite-gcontext font)
(font-foreground font)
(xlib:gcontext-foreground gcontext)))))
(when (or (null (getf (xlib:drawable-plist drawable) :ttf-foreground))
(/= (getf (xlib:drawable-plist drawable) :ttf-foreground)
color))
(let ((previous-color (xlib:gcontext-foreground gcontext)))
(setf (xlib:gcontext-foreground gcontext) color)
(xlib:draw-point pixmap gcontext 0 0)
(setf (xlib:gcontext-foreground gcontext) previous-color)
(setf (getf (xlib:drawable-plist drawable) :ttf-foreground) color))))))
(defun update-background (drawable gcontext font x y width height)
"Lazy updates background for drawable. @var{drawable} must be window or pixmap."
(let ((previous-color (xlib:gcontext-foreground gcontext))
(color (the xlib:card32
(if (font-overwrite-gcontext font)
(font-background font)
(xlib:gcontext-background gcontext)))))
(setf (xlib:gcontext-foreground gcontext) color)
(xlib:draw-rectangle drawable gcontext x y width height t)
(setf (xlib:gcontext-foreground gcontext) previous-color)))
;;; Caching X11 objects
(defun get-drawable-picture (drawable)
(or (getf (xlib:drawable-plist drawable) :ttf-surface)
(setf (getf (xlib:drawable-plist drawable) :ttf-surface)
(xlib:render-create-picture
drawable
:format (first (xlib::find-matching-picture-formats (xlib:drawable-display drawable)
:depth (xlib:drawable-depth drawable)))))))
(defun get-drawable-pen-picture (drawable)
(or (getf (xlib:drawable-plist drawable) :ttf-pen)
(setf (getf (xlib:drawable-plist drawable) :ttf-pen)
(xlib:render-create-picture
(or (getf (xlib:drawable-plist drawable) :ttf-pen-surface)
(setf (getf (xlib:drawable-plist drawable) :ttf-pen-surface)
(xlib:create-pixmap
:drawable drawable
:depth (xlib:drawable-depth drawable)
:width 1 :height 1)))
:format (first (xlib::find-matching-picture-formats (xlib:drawable-display drawable)
:depth (xlib:drawable-depth drawable)))
:repeat :on))))
(defun display-alpha-picture-format (display)
(or (getf (xlib:display-plist display) :ttf-alpha-format)
(setf (getf (xlib:display-plist display) :ttf-alpha-format)
(first
(xlib:find-matching-picture-formats
display
:depth 8 :alpha 8 :red 0 :blue 0 :green 0)))))
;;; Drawing text
(defun draw-text (drawable gcontext font string x y &key start end
draw-background-p)
"Draws text string using @var{font} on @var{drawable} with graphic context @var{gcontext}. @var{x}, @var{y} are the left point of base line. @var{start} and @var{end} are used for substring rendering.
If @var{gcontext} has background color, text bounding box will be filled with it. Text line bounding box is bigger than text bounding box. @var{drawable} must be window or pixmap."
(when (and start end)
(when (>= start end)
(return-from draw-text))
(setf string (subseq string start end)))
(multiple-value-bind (alpha-data min-x max-y width height)
(text-pixarray drawable font string)
(when (or (= 0 width) (= 0 height))
(return-from draw-text))
(let* ((display (xlib:drawable-display drawable))
(image (xlib:create-image :width width :height height :depth 8 :data alpha-data))
(alpha-pixmap (xlib:create-pixmap :width width :height height :depth 8 :drawable drawable))
(alpha-gc (xlib:create-gcontext :drawable alpha-pixmap))
(alpha-picture
(progn
(xlib:put-image alpha-pixmap alpha-gc image :x 0 :y 0)
(xlib:render-create-picture alpha-pixmap
:format (display-alpha-picture-format display))))
(source-picture (get-drawable-pen-picture drawable))
(destination-picture (get-drawable-picture drawable)))
(xlib:free-gcontext alpha-gc)
(update-foreground drawable gcontext font)
(when draw-background-p
(update-background drawable gcontext font (+ x min-x) (- y max-y) width height))
;; Sync the destination picture with the gcontext
(setf (xlib:picture-clip-x-origin destination-picture) (xlib:gcontext-clip-x gcontext))
(setf (xlib:picture-clip-y-origin destination-picture) (xlib:gcontext-clip-y gcontext))
(setf (xlib:picture-subwindow-mode destination-picture) (xlib:gcontext-subwindow-mode gcontext))
(setf (xlib::picture-clip-mask destination-picture)
(xlib::gcontext-clip-mask gcontext))
(xlib:render-composite :over source-picture alpha-picture destination-picture 0 0 0 0 (+ x min-x) (- y max-y) width height)
(xlib:render-free-picture alpha-picture)
(xlib:free-pixmap alpha-pixmap)
nil)))
(defun draw-text-line (drawable gcontext font string x y &key start end draw-background-p)
"Draws text string using @var{font} on @var{drawable} with graphic context @var{gcontext}. @var{x}, @var{y} are the left point of base line. @var{start} and @var{end} are used for substring rendering.
If @var{gcontext} has background color, text line bounding box will be filled with it. Text line bounding box is bigger than text bounding box. @var{drawable} must be window or pixmap."
(when (and start end)
(when (>= start end)
(return-from draw-text-line))
(setf string (subseq string start end)))
(multiple-value-bind (alpha-data min-x max-y width height)
(text-line-pixarray drawable font string)
(when (or (= 0 width) (= 0 height))
(return-from draw-text-line))
(let* ((display (xlib:drawable-display drawable))
(image (xlib:create-image :width width :height height :depth 8 :data alpha-data))
(alpha-pixmap (xlib:create-pixmap :width width :height height :depth 8 :drawable drawable))
(alpha-gc (xlib:create-gcontext :drawable alpha-pixmap))
(alpha-picture
(progn
(xlib:put-image alpha-pixmap alpha-gc image :x 0 :y 0)
(xlib:render-create-picture alpha-pixmap
:format (display-alpha-picture-format display))))
(source-picture (get-drawable-pen-picture drawable))
(destination-picture (get-drawable-picture drawable)))
(xlib:free-gcontext alpha-gc)
(update-foreground drawable gcontext font)
(when draw-background-p
(update-background drawable gcontext font (+ x min-x) (- y max-y) width height))
;; Sync the destination picture with the gcontext
(setf (xlib:picture-clip-x-origin destination-picture) (xlib:gcontext-clip-x gcontext))
(setf (xlib:picture-clip-y-origin destination-picture) (xlib:gcontext-clip-y gcontext))
(setf (xlib:picture-subwindow-mode destination-picture) (xlib:gcontext-subwindow-mode gcontext))
(setf (xlib::picture-clip-mask destination-picture)
(xlib::gcontext-clip-mask gcontext))
(xlib:render-composite :over source-picture alpha-picture destination-picture 0 0 0 0 (+ x min-x) (- y max-y) width height)
(xlib:render-free-picture alpha-picture)
(xlib:free-pixmap alpha-pixmap)
nil)))
;;; Test utils
(defun trgrey (i)
"Visualize alpha mask using graphic characters"
(cond
((> i 200) "██")
((> i 150) "▓▓")
((> i 100) "▒▒")
((> i 50) "░░")
(t " ")))
(defun print-pixarray (array)
"Print 2d array of alpha mask using graphic characters."
(do ((i 0 (1+ i)))
((>= i (array-dimension array 0)) nil)
(do ((j 0 (1+ j)))
((>= j (array-dimension array 1)) nil)
(format t "~A" (trgrey (aref array i j))))
(format t "~%")))
(defun font-lines-height (drawable font lines-count)
"Returns text lines height in pixels. For one line height is ascender+descender. For more than one line height is ascender+descender+linegap."
(if (> lines-count 0)
(+ (+ (xft:font-ascent drawable font)
(- (xft:font-descent drawable font)))
(* (1- lines-count) (+ (xft:font-ascent drawable font)
(- (xft:font-descent drawable font))
(xft:font-line-gap drawable font))))
0))
;;; "clx-truetype" goes here. Hacks and glory await!