|
1
|
+;;;; -*- Mode: Lisp ; Package: Toolkit -*-
|
|
2
|
+
|
|
3
|
+(ext:file-comment "$Header: src/motif/lisp/fonts.lisp $")
|
|
4
|
+
|
|
5
|
+;;; fonts.lisp -- some machinery for unifying the naming of
|
|
6
|
+;;; traditional Core X11 Fonts with Xft2 fonts. Conceptually almost
|
|
7
|
+;;; all of this this file is not specific to CLM (it's mostly parsing
|
|
8
|
+;;; and some invented heuristics/conventions that could be useful in
|
|
9
|
+;;; X11 context), but it currently only models the minimum properties
|
|
10
|
+;;; of font names necessary to generate the resource specifications
|
|
11
|
+;;; OpenMotif uses to configure fonts. However, the interfaces in this
|
|
12
|
+;;; file were designed to permit retrofitting in a richer model
|
|
13
|
+;;; non-disruptively.
|
|
14
|
+
|
|
15
|
+(in-package "TOOLKIT")
|
|
16
|
+
|
|
17
|
+;; For reasons that'll be explained as we go, we need to parse (or at
|
|
18
|
+;; least validate) font name strings. Here's the base class for
|
|
19
|
+;; parsing errors.
|
|
20
|
+(define-condition font-name-parse-error (parse-error)
|
|
21
|
+ ((kind :initarg :kind :reader font-name-parse-error-kind)
|
|
22
|
+ (string :initarg :string :reader font-name-parse-error-string)
|
|
23
|
+ (index :initarg :index :reader font-name-parse-error-index)
|
|
24
|
+ (description :initarg :description
|
|
25
|
+ :reader font-name-parse-error-description))
|
|
26
|
+ (:default-initargs :kind nil :description nil)
|
|
27
|
+ (:documentation "Class of error signaled when a string can't be parsed as a font name.")
|
|
28
|
+ (:report
|
|
29
|
+ (lambda (error stream)
|
|
30
|
+ (format stream
|
|
31
|
+ "Parsing ~S as a font-name~@[ according to ~A syntax~] ended at ~D~@[ ~A~]."
|
|
32
|
+ (font-name-parse-error-string error)
|
|
33
|
+ (font-name-parse-error-kind error)
|
|
34
|
+ (font-name-parse-error-index error)
|
|
35
|
+ (font-name-parse-error-description error)))))
|
|
36
|
+
|
|
37
|
+;; As mentioned, this file currently only offers a trivial model of
|
|
38
|
+;; font specifications. The representation of parsed font names is
|
|
39
|
+;; *not* part of the interface, and subject to change. To insulate
|
|
40
|
+;; prospective clients from that detail, here are some types.
|
|
41
|
+(deftype core-font-name ()
|
|
42
|
+ "Instances of this type are for use with the Core X11 Font system."
|
|
43
|
+ '(satisfies core-font-name-p))
|
|
44
|
+(deftype xlfd-name ()
|
|
45
|
+ "Subtype of CORE-FONT-NAME for XLFD names."
|
|
46
|
+ '(satisfies xlfd-name-p))
|
|
47
|
+(deftype fontconfig-name ()
|
|
48
|
+ "Instances of this type are for use with the Xft2 font system."
|
|
49
|
+ '(satisfies fontconfig-name-p))
|
|
50
|
+
|
|
51
|
+;; Core X11 Font names are just strings, ultimately transmitted to the
|
|
52
|
+;; X server for resolution. In general core fonts' names are strings
|
|
53
|
+;; that are opaque to clients. We'll wrap them in an object for
|
|
54
|
+;; discrimination, and let DEFSTRUCT define the predicate we use for
|
|
55
|
+;; the DEFTYPE above.
|
|
56
|
+(defstruct (core-font-name (:type vector) :named (:copier nil))
|
|
57
|
+ (string "" :type string :read-only t))
|
|
58
|
+
|
|
59
|
+;; Core X11 Font names can be in XLFD format, but they might
|
|
60
|
+;; not be (e.g., aliases are unlikely to be in XLFD format). Here's
|
|
61
|
+;; the XLFD spec:
|
|
62
|
+
|
|
63
|
+;; https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/XLFD/xlfd.html
|
|
64
|
+
|
|
65
|
+;; For now, we don't really need a detailed parse of an XLFD, but
|
|
66
|
+;; we'll pretend as if we've got one. In fact our parser will merely
|
|
67
|
+;; validate the string and then cons up an object for which we've got
|
|
68
|
+;; a predicate.
|
|
69
|
+(defstruct (xlfd-name (:type vector) :named (:copier nil)
|
|
70
|
+ (:include core-font-name)))
|
|
71
|
+
|
|
72
|
+(define-condition xlfd-name-parse-error
|
|
73
|
+ (font-name-parse-error)
|
|
74
|
+ ()
|
|
75
|
+ (:default-initargs :kind "XLFD"))
|
|
76
|
+
|
|
77
|
+;; A proper XLFD has 14 hyphens, so 15 fields (inclusive of the
|
|
78
|
+;; registry, which must be the empty string).
|
|
79
|
+(defconstant +xlfd-field-count+ 15)
|
|
80
|
+
|
|
81
|
+;; Even though we don't really need a structured XLFD parse, our
|
|
82
|
+;; heuristics require code for validating well-formedness of an XLFD
|
|
83
|
+;; (14 hyphens, optionally excluding wildcards). JUNK-ALLOWED follows
|
|
84
|
+;; the ANSI CL convention. WILDCARD-ALLOWED is just a convenience. It
|
|
85
|
+;; seems that Xorg and Xquartz treat subsets of well-formed XLFDs as
|
|
86
|
+;; usable font names, so this also supports a keyword to make it okay
|
|
87
|
+;; to have fewer than 15 fields).
|
|
88
|
+(defun parse-xlfd-name (string &key (start 0) end
|
|
89
|
+ junk-allowed subsequence-allowed
|
|
90
|
+ wildcard-allowed)
|
|
91
|
+ "Parse STRING bounded by START and END as an X Logical Font
|
|
92
|
+Description. If parsing succeeds, return an object for which
|
|
93
|
+XLFD-NAME-P returns true and the index at which parsing
|
|
94
|
+ended. Exceptional conditions: if STRING has a registry but doesn't
|
|
95
|
+have enough fields (13), then signal an error if SUBSEQUENCE-ALLOWED
|
|
96
|
+is false (the default); if string contains a delimiter after the 13th
|
|
97
|
+field, signal an error if JUNK-ALLOWED is false (the default). If
|
|
98
|
+SUBSEQUENCE-ALLOWED is true or JUNK-ALLOWED is true, then return NIL
|
|
99
|
+and the index at which parsing stopped. If WILDCARD-ALLOWED is
|
|
100
|
+false (the default), wildcard characters will cause parsing to end at
|
|
101
|
+the first wildcard character (and so the consequences will depend on
|
|
102
|
+JUNK-ALLOWED); otherwise, wildcard characters will be treated as field
|
|
103
|
+contents."
|
|
104
|
+ (setq end (or end (length string)))
|
|
105
|
+ (let ((index start) (field-count 0))
|
|
106
|
+ (labels
|
|
107
|
+ (;; This is the only way out of PARSE-XLFD-NAME. It
|
|
108
|
+ ;; implements all the SUBSEQUENCE-ALLOWED and JUNK-ALLOWED
|
|
109
|
+ ;; logic. Callers can supply arguments to enrich the error
|
|
110
|
+ ;; report, though it's not the caller's job to decide whether
|
|
111
|
+ ;; we've succeeded or not.
|
|
112
|
+ (finish-parsing (&rest error-description)
|
|
113
|
+ (if (and (or (= field-count +xlfd-field-count+)
|
|
114
|
+ (and (plusp field-count) subsequence-allowed))
|
|
115
|
+ (or (= index end) junk-allowed))
|
|
116
|
+ (return-from parse-xlfd-name
|
|
117
|
+ (values
|
|
118
|
+ (make-xlfd-name
|
|
119
|
+ :string (subseq string start index))
|
|
120
|
+ index))
|
|
121
|
+ (error 'xlfd-name-parse-error
|
|
122
|
+ :string (subseq string start end)
|
|
123
|
+ :index (- index start)
|
|
124
|
+ :description
|
|
125
|
+ (apply #'format nil
|
|
126
|
+ (if error-description
|
|
127
|
+ error-description
|
|
128
|
+ (if (< field-count
|
|
129
|
+ +xlfd-field-count+)
|
|
130
|
+ (list "with too few fields (~D)"
|
|
131
|
+ field-count)
|
|
132
|
+ '("with trailing junk")))))))
|
|
133
|
+ (next-token ()
|
|
134
|
+ (loop
|
|
135
|
+ (when (= index end)
|
|
136
|
+ (return))
|
|
137
|
+ (when (> (- index start) 255)
|
|
138
|
+ (finish-parsing "due to length limits"))
|
|
139
|
+ (let ((char (char string index)))
|
|
140
|
+ (cond
|
|
141
|
+ ;; Fields must be ISO-8859-1 strings.
|
|
142
|
+ ((> (char-code char) 255)
|
|
143
|
+ (finish-parsing "due to non-ISO-8859-1 character, ~@C" char))
|
|
144
|
+ ;; Explicitly disallowed in field values.
|
|
145
|
+ ((char= #\" char)
|
|
146
|
+ (finish-parsing "due to a double-quote"))
|
|
147
|
+ ;; Conditionally allowed.
|
|
148
|
+ ((and (find (char string index) '(#\? #\*))
|
|
149
|
+ (not wildcard-allowed))
|
|
150
|
+ (finish-parsing "due to wildcard character, ~@C" char))
|
|
151
|
+ ;; Field delimiter character, unescapable.
|
|
152
|
+ ((char= #\- char)
|
|
153
|
+ (return))
|
|
154
|
+ (t (incf index)))))
|
|
155
|
+ (progn (incf field-count)
|
|
156
|
+ (values index
|
|
157
|
+ ;; Leave INDEX at END when we're at end of string
|
|
158
|
+ (when (< index end)
|
|
159
|
+ (prog1 (char string index)
|
|
160
|
+ (incf index)))))))
|
|
161
|
+ (unless (< index end)
|
|
162
|
+ (finish-parsing "because the bounded string was empty"))
|
|
163
|
+ (let ((registry-end (next-token)))
|
|
164
|
+ (when (> registry-end start)
|
|
165
|
+ (finish-parsing "due to unsupported font name registry ~S"
|
|
166
|
+ (subseq string start registry-end))))
|
|
167
|
+ (loop
|
|
168
|
+ (let ((delimiter (nth-value 1 (next-token))))
|
|
169
|
+ (when (= field-count +xlfd-field-count+)
|
|
170
|
+ (when delimiter
|
|
171
|
+ (decf index))
|
|
172
|
+ (finish-parsing))
|
|
173
|
+ (when (null delimiter)
|
|
174
|
+ (finish-parsing)))))))
|
|
175
|
+
|
|
176
|
+;; Several test cases for PARSE-XLFD-NAME.
|
|
177
|
+#+(or)
|
|
178
|
+(macrolet
|
|
179
|
+ ((test-okay (results string &rest args)
|
|
180
|
+ `(assert (equalp (ignore-errors
|
|
181
|
+ (multiple-value-list
|
|
182
|
+ (parse-xlfd-name ,string ,@args)))
|
|
183
|
+ ',(if results
|
|
184
|
+ results
|
|
185
|
+ (list (vector 'core-font-name string 'xlfd-name)
|
|
186
|
+ (length string))))))
|
|
187
|
+ (test-fail (msg string &rest args &aux (result (gensym)) (error (gensym)))
|
|
188
|
+ `(multiple-value-bind (,result ,error)
|
|
189
|
+ (ignore-errors
|
|
190
|
+ (multiple-value-list
|
|
191
|
+ (parse-xlfd-name ,string ,@args)))
|
|
192
|
+ (assert (null ,result))
|
|
193
|
+ (assert (typep ,error 'xlfd-name-parse-error))
|
|
194
|
+ ,(when msg
|
|
195
|
+ `(assert (search ',msg (princ-to-string ,error)))))))
|
|
196
|
+ (test-okay nil "--------------")
|
|
197
|
+ ;; By default, an XLFD with fewer than 14 hyphens is an error.
|
|
198
|
+ (test-fail nil "--------")
|
|
199
|
+ ;; But :SUBSEQUENCE-ALLOWED T will make it allowed.
|
|
200
|
+ (test-okay nil "--------" :subsequence-allowed t)
|
|
201
|
+ ;; By default, a string that has more than 14 fields is an error
|
|
202
|
+ (test-fail nil "--------------nope-")
|
|
203
|
+ ;; But :JUNK-ALLOWED T will make it allowed.
|
|
204
|
+ (test-okay (#(core-font-name "--------------nope" xlfd-name)
|
|
205
|
+ 18)
|
|
206
|
+ "--------------nope" :junk-allowed t)
|
|
207
|
+ ;; By default, wildcards are disallowed.
|
|
208
|
+ (test-fail nil "-*-------------")
|
|
209
|
+ (test-okay nil "-*-------------" :wildcard-allowed t)
|
|
210
|
+ (test-fail nil "--------------*")
|
|
211
|
+ (test-okay nil "--------------*" :wildcard-allowed t))
|
|
212
|
+
|
|
213
|
+;; Xft2 doesn't strictly have its own font names; it uses fontconfig
|
|
214
|
+;; for naming. Fontconfig has a syntax for specifying fonts; here's
|
|
215
|
+;; the spec for that:
|
|
216
|
+
|
|
217
|
+;; https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
|
|
218
|
+
|
|
219
|
+;; For Motif-y reasons explained below, we must parse a few properties
|
|
220
|
+;; out of fontconfig names. We'll ignore properties we don't care
|
|
221
|
+;; about. We'll use the same basic idea as above: a lightweight
|
|
222
|
+;; representation of the stuff we need, a PARSE-ERROR subclass, and a
|
|
223
|
+;; parsing function.
|
|
224
|
+(defstruct (fontconfig-name (:type vector) :named (:copier nil))
|
|
225
|
+ (foundry nil :type (or null string) :read-only t)
|
|
226
|
+ (family "" :type string :read-only t)
|
|
227
|
+ ;; TODO: SIZE is really a number, but the fontconfig spec doesn't
|
|
228
|
+ ;; document the number format, so for the moment it's a string.
|
|
229
|
+ ;; Probably this ought to get fixed before contemplating exporting
|
|
230
|
+ ;; the accessor name.
|
|
231
|
+ (size nil :type (or null string) :read-only t)
|
|
232
|
+ (weight nil :type (or null string) :read-only t)
|
|
233
|
+ (slant nil :type (or null string) :read-only t)
|
|
234
|
+ ;; This isn't a proper part of a model of a fontconfig name, just an
|
|
235
|
+ ;; internal trick for the heuristics that follow.
|
|
236
|
+ (has-properties-p nil :type boolean :read-only t))
|
|
237
|
+
|
|
238
|
+(define-condition fontconfig-name-parse-error
|
|
239
|
+ (font-name-parse-error)
|
|
240
|
+ ()
|
|
241
|
+ (:default-initargs :kind "fontconfig"))
|
|
242
|
+
|
|
243
|
+;; This routine attempts to implement a fairly strict idea of
|
|
244
|
+;; well-formedness for fontconfig specs. Any functional disagreement
|
|
245
|
+;; with fontconfig over the domain of well-formed fontconfig names is
|
|
246
|
+;; a bug. (fontconfig's matching of strings that aren't well-formed
|
|
247
|
+;; fontconfig names is none of our business.)
|
|
248
|
+(defun parse-fontconfig-name (string &key (start 0) end junk-allowed)
|
|
249
|
+ (setq end (or end (length string)))
|
|
250
|
+ (let (foundry family size weight slant has-properties-p
|
|
251
|
+ (index start) part-end)
|
|
252
|
+ (labels
|
|
253
|
+ (;; This is the only way out of
|
|
254
|
+ ;; PARSE-FONTCONFIG-NAME.
|
|
255
|
+ (finish-parsing (&rest error-description)
|
|
256
|
+ (if (or (= part-end end) junk-allowed)
|
|
257
|
+ (return-from parse-fontconfig-name
|
|
258
|
+ (values
|
|
259
|
+ (when family
|
|
260
|
+ (make-fontconfig-name
|
|
261
|
+ :family family :foundry foundry :size size
|
|
262
|
+ :weight weight :slant slant
|
|
263
|
+ :has-properties-p
|
|
264
|
+ (or has-properties-p slant weight foundry)))
|
|
265
|
+ ;; Parsing always ends at the index of the end of
|
|
266
|
+ ;; the part of the name that parsed, even if there's
|
|
267
|
+ ;; junk after.
|
|
268
|
+ part-end))
|
|
269
|
+ (error 'fontconfig-name-parse-error
|
|
270
|
+ :string (subseq string start end)
|
|
271
|
+ :index (- index start)
|
|
272
|
+ :description (when error-description
|
|
273
|
+ (apply #'format nil error-description)))))
|
|
274
|
+ ;; Parse the next token starting at INDEX, delimited by any
|
|
275
|
+ ;; character in DELIMITERS. Note that the family and any
|
|
276
|
+ ;; property value use backslash to escape the delimiter, but
|
|
277
|
+ ;; the size and property name are not documented as allowing
|
|
278
|
+ ;; an escape character. Returns a non-empty token, the
|
|
279
|
+ ;; delimiter that ended the token, and the delimiter's index.
|
|
280
|
+ (next-token (delimiters &optional (escapep t))
|
|
281
|
+ (do ((chars)
|
|
282
|
+ (char (and (< index end) (char string index))
|
|
283
|
+ (and (< index end) (char string index))))
|
|
284
|
+ ((or (null char) (find char delimiters))
|
|
285
|
+ (multiple-value-prog1
|
|
286
|
+ (values (when chars
|
|
287
|
+ (coerce (nreverse chars) 'string))
|
|
288
|
+ char
|
|
289
|
+ index)
|
|
290
|
+ (incf index)))
|
|
291
|
+ (when (and escapep (char= #\\ char))
|
|
292
|
+ (when (= index end)
|
|
293
|
+ (finish-parsing "after the escape character"))
|
|
294
|
+ (incf index)
|
|
295
|
+ (setq char (char string index)))
|
|
296
|
+ (push char chars)
|
|
297
|
+ (incf index)))
|
|
298
|
+ ;; The fontconfig spec doesn't say whether names & their
|
|
299
|
+ ;; components are matched case-sensitively or
|
|
300
|
+ ;; case-insensitively. It seems as if it's insensitive, but
|
|
301
|
+ ;; let's factor it here just in case.
|
|
302
|
+ (string-equiv (s1 s2)
|
|
303
|
+ (string-equal s1 s2)))
|
|
304
|
+ (let (delimiter token-end)
|
|
305
|
+ (multiple-value-setq (family delimiter token-end)
|
|
306
|
+ (next-token '(#\- #\:)))
|
|
307
|
+ (when (null family)
|
|
308
|
+ (finish-parsing "without any family"))
|
|
309
|
+ ;; fontconfig names allow for a comma-separated list of
|
|
310
|
+ ;; families. TODO: check if Motif can handle such lists.
|
|
311
|
+ ;; Pending that, make it an error to find a comma in the name.
|
|
312
|
+ ;; This is a defect in this parser.
|
|
313
|
+ (when (find #\, family)
|
|
314
|
+ (finish-parsing "with an unsupported syntax (list of families))"))
|
|
315
|
+ ;; If we're here, the family is acceptable, so we've reached
|
|
316
|
+ ;; the end of this part. Save it for FINISH-PARSING.
|
|
317
|
+ (setq part-end token-end)
|
|
318
|
+ (when (eql #\- delimiter)
|
|
319
|
+ (multiple-value-setq (size delimiter token-end)
|
|
320
|
+ (next-token '(#\:) nil))
|
|
321
|
+ ;; TODO, maybe: validate that SIZE parses as a number. (But
|
|
322
|
+ ;; first figure out what the number syntax is; the
|
|
323
|
+ ;; fontconfig spec doesn't say.)
|
|
324
|
+ (unless size
|
|
325
|
+ (finish-parsing "with a hyphen")))
|
|
326
|
+ ;; TODO: check if Motif supports lists of sizes.
|
|
327
|
+ ;; This is a defect in this parser.
|
|
328
|
+ (when (find #\, size)
|
|
329
|
+ (finish-parsing "with an unsupported syntax (list of sizes))"))
|
|
330
|
+ (setq part-end token-end)
|
|
331
|
+ (when (eql #\: delimiter) ;There are properties to parse.
|
|
332
|
+ (let (name value tmp-end)
|
|
333
|
+ (loop
|
|
334
|
+ (setq part-end token-end)
|
|
335
|
+ ;; We must not set TOKEN-END until we know we've parsed
|
|
336
|
+ ;; a whole property. So we'll use TMP-END.
|
|
337
|
+ (multiple-value-setq (name delimiter tmp-end)
|
|
338
|
+ (next-token '(#\= #\:) nil))
|
|
339
|
+ (if (null name)
|
|
340
|
+ (ecase delimiter
|
|
341
|
+ (#\=
|
|
342
|
+ (finish-parsing "with an empty property name"))
|
|
343
|
+ (#\:
|
|
344
|
+ (finish-parsing "with an empty property"))
|
|
345
|
+ ((nil)
|
|
346
|
+ (finish-parsing "with a colon")))
|
|
347
|
+ (ecase delimiter
|
|
348
|
+ (#\=
|
|
349
|
+ (multiple-value-setq (value delimiter tmp-end)
|
|
350
|
+ (next-token '(#\:)))
|
|
351
|
+ (when (null value)
|
|
352
|
+ (finish-parsing "with an empty property value"))
|
|
353
|
+ (setq token-end tmp-end
|
|
354
|
+ has-properties-p t)
|
|
355
|
+ ;; These are the only properties we care about.
|
|
356
|
+ (cond ((string-equiv name "weight")
|
|
357
|
+ (setq weight value))
|
|
358
|
+ ((string-equiv name "slant")
|
|
359
|
+ (setq slant value))
|
|
360
|
+ ((string-equiv name "foundry")
|
|
361
|
+ (setq foundry value))))
|
|
362
|
+ ((#\: nil)
|
|
363
|
+ ;; In this case, the property might be a
|
|
364
|
+ ;; "symbolic constant" The fontconfig spec says
|
|
365
|
+ ;; "there are symbolic constants that
|
|
366
|
+ ;; simultaneously indicate both a name and a
|
|
367
|
+ ;; value", but it's not clear what those
|
|
368
|
+ ;; constants are. We'll assume that any
|
|
369
|
+ ;; construct is both syntactically valid here.
|
|
370
|
+ (setq token-end tmp-end
|
|
371
|
+ has-properties-p t)
|
|
372
|
+ ;; We need to recognize whatever symbolic
|
|
373
|
+ ;; constants are defined for the weight and
|
|
374
|
+ ;; slant properties. These are taken from the
|
|
375
|
+ ;; description of the <const> element of the
|
|
376
|
+ ;; configuration file format, in case that's
|
|
377
|
+ ;; what's intended in the fontconfig spec.
|
|
378
|
+ (cond ((member name
|
|
379
|
+ '("thin"
|
|
380
|
+ "extralight"
|
|
381
|
+ "ultralight"
|
|
382
|
+ "light"
|
|
383
|
+ "demilight"
|
|
384
|
+ "semilight"
|
|
385
|
+ "book"
|
|
386
|
+ "regular"
|
|
387
|
+ "normal"
|
|
388
|
+ "medium"
|
|
389
|
+ "demibold"
|
|
390
|
+ "semibold"
|
|
391
|
+ "bold"
|
|
392
|
+ "extrabold"
|
|
393
|
+ "black"
|
|
394
|
+ "heavy")
|
|
395
|
+ :test #'string-equiv)
|
|
396
|
+ (setq weight name))
|
|
397
|
+ ((member name
|
|
398
|
+ '("roman"
|
|
399
|
+ "italic"
|
|
400
|
+ "oblique"
|
|
401
|
+ "ultracondensed"
|
|
402
|
+ "extracondensed"
|
|
403
|
+ "condensed"
|
|
404
|
+ "semicondensed"
|
|
405
|
+ "normal"
|
|
406
|
+ "semiexpanded"
|
|
407
|
+ "expanded"
|
|
408
|
+ "extraexpanded"
|
|
409
|
+ "ultraexpanded")
|
|
410
|
+ :test #'string-equiv)
|
|
411
|
+ (setq slant name))))))))))
|
|
412
|
+ (finish-parsing))))
|
|
413
|
+
|
|
414
|
+;; Some test cases for PARSE-FONTCONFIG-NAME.
|
|
415
|
+#+(or)
|
|
416
|
+(macrolet
|
|
417
|
+ ((test-okay (results string &rest args)
|
|
418
|
+ `(assert (equalp (ignore-errors
|
|
419
|
+ (multiple-value-list
|
|
420
|
+ (parse-fontconfig-name ,string ,@args)))
|
|
421
|
+ ',(if (listp results)
|
|
422
|
+ results
|
|
423
|
+ (list results (length string))))))
|
|
424
|
+ (test-fail (msg string &rest args &aux (result (gensym)) (error (gensym)))
|
|
425
|
+ `(multiple-value-bind (,result ,error)
|
|
426
|
+ (ignore-errors
|
|
427
|
+ (multiple-value-list
|
|
428
|
+ (parse-fontconfig-name ,string ,@args)))
|
|
429
|
+ (assert (null ,result))
|
|
430
|
+ (assert (typep ,error 'fontconfig-name-parse-error))
|
|
431
|
+ ,(when msg
|
|
432
|
+ `(assert (search ',msg (princ-to-string ,error)))))))
|
|
433
|
+ ;; Just a name
|
|
434
|
+ (test-okay #(fontconfig-name nil "Foo" nil nil nil nil) "Foo")
|
|
435
|
+ ;; Name and size
|
|
436
|
+ (test-okay #(fontconfig-name nil "Foo" "12" nil nil nil) "Foo-12")
|
|
437
|
+ ;; This fully specifies everything we care about.
|
|
438
|
+ (test-okay #(fontconfig-name "Bar" "Foo" "12" "bold" "italic" t)
|
|
439
|
+ "Foo-12:foundry=Bar:slant=italic:weight=bold")
|
|
440
|
+ ;; Same as previous, but with extra junk (which should be ignored).
|
|
441
|
+ (test-okay #(fontconfig-name "Bar" "Foo" "12" "bold" "italic" t)
|
|
442
|
+ "Foo-12:abc=def:foundry=Bar:xyz=123:slant=italic:weight=bold")
|
|
443
|
+ ;; Test recognition of symbolic constants for weight and slant.
|
|
444
|
+ (test-okay #(fontconfig-name nil "Foo" "12" "bold" "italic" t)
|
|
445
|
+ "Foo-12:italic:bold")
|
|
446
|
+ ;; Test recognition that a font has properties (even if we don't
|
|
447
|
+ ;; know what they are).
|
|
448
|
+ (test-okay #(fontconfig-name nil "Foo" "12" nil nil t)
|
|
449
|
+ "Foo-12:bar=baz")
|
|
450
|
+ ;; Test various invalid (I think) things.
|
|
451
|
+ (test-fail "with a hyphen" "Foo-")
|
|
452
|
+ (test-fail "with a colon" "Foo:")
|
|
453
|
+ (test-fail "with a colon" "Foo-12:")
|
|
454
|
+ (test-fail "empty property" "Foo::")
|
|
455
|
+ (test-fail "empty property" "Foo-12::")
|
|
456
|
+ (test-fail "empty property name" "Foo:=bar")
|
|
457
|
+ (test-fail "empty property value" "Foo:bar="))
|
|
458
|
+
|
|
459
|
+;; Now that we have font name parsers, let's build a convention for
|
|
460
|
+;; figuring out when to apply them. Every octet string up to length
|
|
461
|
+;; 255 is a syntactically valid Core X11 Font name; and fontconfig
|
|
462
|
+;; appears not to care whether its input strings are well-formed
|
|
463
|
+;; fontconfig names. So in principle all strings (modulo length and
|
|
464
|
+;; encoding) might be "usable" as a font names in either system.
|
|
465
|
+;;
|
|
466
|
+;; However, in practice, most Core X11 Fonts have XLFD names, and
|
|
467
|
+;; fontconfig's behavior is more predictable when its inputs are
|
|
468
|
+;; well-formed and detailed fontconfig names. Therefore, it seems
|
|
469
|
+;; reasonable to build up some heuristics:
|
|
470
|
+;;
|
|
471
|
+;; 1. a string that starts with a hyphen is an XLFD (fontconfig name
|
|
472
|
+;; can't start with hyphens).
|
|
473
|
+;;
|
|
474
|
+;; 2. a string that's a well-formed fontconfig name containing a colon
|
|
475
|
+;; is a fontconfig name (colons don't seem much used in Core X11 Font
|
|
476
|
+;; names).
|
|
477
|
+;;
|
|
478
|
+;; Here are two helper routines that implement those heuristics. Note
|
|
479
|
+;; that these two don't partition all strings, e.g., "Times" or
|
|
480
|
+;; "Helvetica-12" won't satisfy either predicate. We'll address those
|
|
481
|
+;; "ambiguous" cases below.
|
|
482
|
+(defun xlfdp (thing)
|
|
483
|
+ "Returns true if THING represents an X Logical Font Description, either
|
|
484
|
+as an XLFD string or the parse of one."
|
|
485
|
+ (etypecase thing
|
|
486
|
+ (xlfd-name
|
|
487
|
+ ;; Note that objects that satisfy this predicate might have been
|
|
488
|
+ ;; created by PARSE-XLFD-NAME calls with non-default
|
|
489
|
+ ;; flags, and so may not be well-formed XLFDs on their own. If
|
|
490
|
+ ;; the user had the context to do that, then we're not going to
|
|
491
|
+ ;; overrule the decision.
|
|
492
|
+ thing)
|
|
493
|
+ (string (nth-value
|
|
494
|
+ 0
|
|
495
|
+ (ignore-errors
|
|
496
|
+ (parse-xlfd-name
|
|
497
|
+ thing
|
|
498
|
+ ;; These initargs are arbitrary, but appear to agree
|
|
499
|
+ ;; with what my X server seems to consider acceptable
|
|
500
|
+ ;; arguments to XOpenFont.
|
|
501
|
+ :junk-allowed nil :subsequence-allowed t
|
|
502
|
+ :wildcard-allowed t))))))
|
|
503
|
+
|
|
504
|
+(defun fontconfigp (thing)
|
|
505
|
+ "Returns true in case THING is probably a fontconfig name:
|
|
506
|
+either a parsed fontconfig name, or a string that parses to a
|
|
507
|
+fontconfig name having explicit properties."
|
|
508
|
+ (etypecase thing
|
|
509
|
+ (fontconfig-name
|
|
510
|
+ thing)
|
|
511
|
+ (string
|
|
512
|
+ (let ((thing (ignore-errors
|
|
513
|
+ (parse-fontconfig-name thing))))
|
|
514
|
+ (when (and thing (fontconfig-name-has-properties-p thing))
|
|
515
|
+ thing)))))
|
|
516
|
+
|
|
517
|
+;; So now we've got heuristic detection of XLFD and fontconfig
|
|
518
|
+;; names. Disambiguating other strings in isolation is inherently
|
|
519
|
+;; arbitrary. However, when we've got an opportunity to look at a set
|
|
520
|
+;; of strings, we can disambiguate using context: let's assume that if
|
|
521
|
+;; any string is an XLFD, then all ambiguous strings are meant as Core
|
|
522
|
+;; X11 Font names; that if any string is a well-formed fontconfig name
|
|
523
|
+;; with properties, then all ambiguous strings are also for Xft2; that
|
|
524
|
+;; if all strings are ambiguous, we'll fallthru to consulting a
|
|
525
|
+;; variable.
|
|
526
|
+(defvar *ambiguous-font-disposition* :xft2)
|
|
527
|
+(declaim (type (member :xft2 :core) *ambiguous-font-disposition*))
|
|
528
|
+
|
|
529
|
+;; Finally, it seems that using Core X11 Fonts with Xft2 fonts within
|
|
530
|
+;; a single RenderTable that doesn't work in OpenMotif circa 2021. (I
|
|
531
|
+;; couldn't figure it out, anyhow.) And maybe nobody would want to do
|
|
532
|
+;; so anyway. So for now we'll rule out mix-and-match scenarios.
|
|
533
|
+(defun heuristicate-font-name-types (names)
|
|
534
|
+ (assert (every #'stringp names))
|
|
535
|
+ (flet ((parse-as-core-fonts ()
|
|
536
|
+ (mapcar #'(lambda (spec)
|
|
537
|
+ (or (xlfdp spec) (make-core-font-name :string spec)))
|
|
538
|
+ names))
|
|
539
|
+ (parse-as-xft2-fonts ()
|
|
540
|
+ (mapcar #'parse-fontconfig-name names)))
|
|
541
|
+ (cond ((some #'xlfdp names)
|
|
542
|
+ (when (some #'fontconfigp names)
|
|
543
|
+ (error "Can't mix fontconfig and Core X11 font names."))
|
|
544
|
+ (parse-as-core-fonts))
|
|
545
|
+ ((some #'fontconfigp names)
|
|
546
|
+ (when (some #'xlfdp names)
|
|
547
|
+ (error "Can't mix fontconfig and Core X11 font names."))
|
|
548
|
+ (parse-as-xft2-fonts))
|
|
549
|
+ (t (ecase *ambiguous-font-disposition*
|
|
550
|
+ (:core (parse-as-core-fonts))
|
|
551
|
+ (:xft2 (parse-as-xft2-fonts)))))))
|
|
552
|
+
|
|
553
|
+;; Here's the OpenMotif-specific bit. Now that we can heuristically
|
|
554
|
+;; classify a list of fonts, we can pair up tags with heuristicated
|
|
555
|
+;; font names in order to generate OpenMotif resource strings suitable
|
|
556
|
+;; for either fallback resources or writing into X resource files.
|
|
557
|
+(defun generate-heuristicated-font-resources
|
|
558
|
+ (tags fonts &key application-name application-class)
|
|
559
|
+ "Generate a list of OpenMotif RenderTable & Rendition resources
|
|
560
|
+associating FONTS with TAGS. If APPLICATION-NAME or APPLICATION-CLASS
|
|
561
|
+is supplied, the resource keys will be prefixed by that string;
|
|
562
|
+otherwise, the resource key will start with the loose binding
|
|
563
|
+operator, asterisk."
|
|
564
|
+ (declare (type list tags fonts)
|
|
565
|
+ (type (or null string) application-name application-class))
|
|
566
|
+ (let ((ntags (length tags))
|
|
567
|
+ (nfonts (length fonts)))
|
|
568
|
+ (assert (= ntags nfonts) (tags fonts)
|
|
569
|
+ "Too ~:[many~;few~] tags (~A) for fonts (~A)."
|
|
570
|
+ (> ntags nfonts) tags fonts))
|
|
571
|
+ (let ((name/class (or application-name application-class)))
|
|
572
|
+ (nconc
|
|
573
|
+ (mapcan
|
|
574
|
+ (lambda (tag spec)
|
|
575
|
+ (let* ((rendition (if (or (string= "" tag) (null tag))
|
|
576
|
+ ;; Accept NIL or "" as a the default
|
|
577
|
+ ;; tag. Default tags' resources are
|
|
578
|
+ ;; resources of the RenderTable itself.
|
|
579
|
+ "renderTable"
|
|
580
|
+ ;; Non-default tags get used as resource
|
|
581
|
+ ;; names.
|
|
582
|
+ tag)))
|
|
583
|
+ ((lambda (resources)
|
|
584
|
+ (loop for (resname resval) on resources by #'cddr
|
|
585
|
+ collect (format nil "~@[~A~]*~A.~A: ~A"
|
|
586
|
+ name/class rendition resname resval)))
|
|
587
|
+ ;; Core fonts are specified by 2 resource name/value pairs.
|
|
588
|
+ ;; Xft2 fonts are specified by 4 such pairs.
|
|
589
|
+ (if (core-font-name-p spec)
|
|
590
|
+ (list "fontName" (core-font-name-string spec)
|
|
591
|
+ "fontType" "FONT_IS_FONT")
|
|
592
|
+ (list* "fontName" (fontconfig-name-family spec)
|
|
593
|
+ "fontType" "FONT_IS_XFT"
|
|
594
|
+ (nconc
|
|
595
|
+ (when (fontconfig-name-foundry spec)
|
|
596
|
+ (list "foundryName" (fontconfig-name-foundry spec)))
|
|
597
|
+ (when (fontconfig-name-size spec)
|
|
598
|
+ (list "fontSize" (fontconfig-name-size spec)))
|
|
599
|
+ (when (or (fontconfig-name-weight spec)
|
|
600
|
+ (fontconfig-name-slant spec))
|
|
601
|
+ (list "fontStyle"
|
|
602
|
+ (format nil "~:[~@[~A~]~;~:*~A~@[ ~A~]~]"
|
|
603
|
+ (fontconfig-name-weight spec)
|
|
604
|
+ (fontconfig-name-slant spec))))))))))
|
|
605
|
+ tags (heuristicate-font-name-types fonts))
|
|
606
|
+ (list (format nil "~@[~A~]*renderTable: ~{~A~^ ~}"
|
|
607
|
+ name/class
|
|
608
|
+ (remove "" tags))))))
|
|
609
|
+
|
|
610
|
+;; Test cases for GENERATE-HEURISTICATED-FONT-RESOURCES.
|
|
611
|
+#+(or)
|
|
612
|
+(assert
|
|
613
|
+ (equal
|
|
614
|
+ ;; These are the Core X11 Fonts that the CLM Debugger/Inspector have
|
|
615
|
+ ;; used.
|
|
616
|
+ (let ((fonts '("-adobe-helvetica-medium-r-normal--*-120-75-*"
|
|
617
|
+ "-adobe-helvetica-bold-r-normal--*-120-75-*"
|
|
618
|
+ "-adobe-helvetica-medium-o-normal--*-120-75-*"))
|
|
619
|
+ (tags '("" "header" "italic")))
|
|
620
|
+ (generate-heuristicated-font-resources tags fonts))
|
|
621
|
+ '("*renderTable.fontName: -adobe-helvetica-medium-r-normal--*-120-75-*"
|
|
622
|
+ "*renderTable.fontType: FONT_IS_FONT"
|
|
623
|
+ "*header.fontName: -adobe-helvetica-bold-r-normal--*-120-75-*"
|
|
624
|
+ "*header.fontType: FONT_IS_FONT"
|
|
625
|
+ "*italic.fontName: -adobe-helvetica-medium-o-normal--*-120-75-*"
|
|
626
|
+ "*italic.fontType: FONT_IS_FONT" "*renderTable: header italic")))
|
|
627
|
+
|
|
628
|
+#+(or)
|
|
629
|
+(assert
|
|
630
|
+ (equal
|
|
631
|
+ ;; Here are some fontconfig names.
|
|
632
|
+ (let ((fonts '("Sans-12:regular"
|
|
633
|
+ "Sans-12:bold"
|
|
634
|
+ "Sans-12:italic"
|
|
635
|
+ "Sans-12:bold:italic"))
|
|
636
|
+ (tags '("" "header" "italic" "foo")))
|
|
637
|
+ (generate-heuristicated-font-resources tags fonts))
|
|
638
|
+ '("*renderTable.fontName: Sans" "*renderTable.fontType: FONT_IS_XFT"
|
|
639
|
+ "*renderTable.fontSize: 12" "*renderTable.fontStyle: regular"
|
|
640
|
+ "*header.fontName: Sans" "*header.fontType: FONT_IS_XFT"
|
|
641
|
+ "*header.fontSize: 12" "*header.fontStyle: bold"
|
|
642
|
+ "*italic.fontName: Sans" "*italic.fontType: FONT_IS_XFT"
|
|
643
|
+ "*italic.fontSize: 12" "*italic.fontStyle: italic"
|
|
644
|
+ "*foo.fontName: Sans" "*foo.fontType: FONT_IS_XFT"
|
|
645
|
+ "*foo.fontSize: 12" "*foo.fontStyle: bold italic"
|
|
646
|
+ "*renderTable: header italic foo")))
|