]> git.hungrycats.org Git - xscreensaver/blob - utils/font-retry.c
From https://www.jwz.org/xscreensaver/xscreensaver-6.09.tar.gz
[xscreensaver] / utils / font-retry.c
1 /* xscreensaver, Copyright © 2018-2022 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  */
11
12 /* The value of XScreenSaver's font resource strings is a comma-separated
13    list of font names that look like this:
14
15        Helvetica Bold Italic 12
16
17    We parse that into an XLFD and pass that along to the underlying systems:
18    XftFontOpenXlfd or jwxyz.
19
20    Xft does aggressive substitution of unknown fonts, so if we don't get an
21    exact match on the font name, we move on to the next element in the list.
22    For the last element, we take whatever substitution we got.
23
24    XftNameParse uses this syntax:
25
26       "Helvetica Neue-12:style=Bold"
27       "Helvetica Neue-12:style=Bold Italic"
28       "Helvetica Neue-12:slant=110:weight=200"
29
30    Cocoa uses PostScript names, with hyphens:
31
32      [NSFont fontWithName:@"Helvetica-BoldOblique" size:12];
33      [NSFont fontWithName:@"Times-Roman" size:12];
34
35      Alternately, with separated styles:
36
37      [[NSFontManager sharedFontManager]
38       fontWithFamily:@"Helvetica" traits:NSBoldFontMask weight:5 size:12];
39
40    Android separates names from styles:
41
42      Paint.setTypeface (Typeface.create ("Helvetica", Typeface.BOLD));
43      Paint.setTextSize (12);
44
45      See android/xscreensaver/src/org/jwz/xscreensaver/jwxyz.java
46
47
48    In XScreenSaver 6, USE_XFT is always true, as all programs now use Xft.
49
50    In XScreenSaver 5, this file was compiled in two different ways:
51    - As font-retry.o for programs that did not link with libXft;
52    - As font-retry-xft.o for programs that did.
53
54    #ifdef USE_XFT is whether Xft code should be called.
55    #ifdef HAVE_XFT is whether libXft is natively available.
56
57    The reason there are two defines is that if HAVE_XFT is not defined,
58    the Xft API is still available through emulation provided by "xft.h".
59  */
60
61 #define _GNU_SOURCE  /* Why is this here? */
62
63 #include "utils.h"
64 #include "visual.h"
65 #include "xft.h"
66 #include "font-retry.h"
67
68 extern const char *progname;
69
70 #undef countof
71 #define countof(x) (sizeof((x))/sizeof((*x)))
72
73 #undef DEBUG
74 #undef SELFTEST
75
76 #ifdef HAVE_JWXYZ
77 # define USE_XFT 1
78 #endif
79
80 #ifdef SELFTEST
81 static void xft_selftest (Display *dpy, int screen);
82 #endif
83
84
85 /* Parse font names of the form "Helvetica Neue Bold Italic 12".
86  */
87 static char *
88 font_name_to_xlfd (const char *font_name)
89 {
90   char *name = strdup (font_name);
91   char *xlfd = 0;
92   char *s, *s2, *b, *i, *o, c;
93   float size;
94
95   if (name[0] == '-' || name[0] == '*')
96     return name;  /* Already an XLFD */
97
98   /* Downcase ASCII; dash to space */
99   for (s = name; *s; s++)
100     if (*s >= 'A' && *s <= 'Z')
101       *s += 'a'-'A';
102     else if (*s == '-')
103       *s = ' ';
104
105   /* "Family NN" or "Family-NN" */
106   s  = strrchr (name, ' ');
107   s2 = strrchr (name, '-');
108   if (s && s2 && s2 > s) s = s2;
109   if (!s) goto FAIL;
110   if (1 != sscanf (s+1, " %f %c", &size, &c)) goto FAIL;
111   if (size <= 0) goto FAIL;
112   *s = 0;
113
114   /* "Family Bold", etc. */
115   b = strstr (name, " bold");
116   i = strstr (name, " italic");
117   o = strstr (name, " oblique");
118   if (b) *b = 0;
119   if (i) *i = 0;
120   if (o) *o = 0;
121
122   xlfd = (char *) malloc (strlen(name) + 80);
123   sprintf (xlfd, "-*-%s-%s-%s-*-*-*-%d-*-*-*-*-*-*",
124            name,
125            (b ? "bold" : "medium"),
126            (i ? "i" : o ? "o" : "r"),
127            (int) (size * 10));
128   free (name);
129   return xlfd;
130
131  FAIL:
132   fprintf (stderr, "%s: XFT: unparsable: \"%s\"\n", progname, font_name);
133   if (name) free (name);
134   if (xlfd) free (xlfd);
135   return 0;
136 }
137
138
139 #ifdef USE_XFT
140
141 /* Xft silently substitutes fonts if the one you requested wasn't available.
142    This leads to the deplorable situation where we ask for a fixed width font
143    and are given a variable width font instead.  This doesn't happen with
144    Courier, since Xft special-cases that one, but it happens with any other
145    fixed width font that is not similarly privileged.
146
147    Even worse: when Xft substitutes fonts, it makes no attempt to return fonts
148    that are capable of displaying the language of the current locale.  For
149    example: if your locale is set to Japanese and you request "Helvetica", Xft
150    silently substitutes "Nimbus Sans" -- a font which is not capable of
151    displaying Japanese characters.  If instead you requested "asdfasdfghjkl",
152    you get "Noto Sans CJK JP", which does work.  So that's just spectacular.
153
154    Since there does not appear to be a way to ask Xft whether a particular
155    font exists, we load the font and then check to see if the name we got
156    is the name we requested.
157
158    In more detail:
159
160      - XftFontOpenXlfd is defined as:
161        XftFontOpenPattern (XftFontMatch (XftXlfdParse (xlfd)))
162      - XftFontOpenName is defined as:
163        XftFontOpenPattern (XftFontMatch (XftNameParse (name)))
164      - Calling XftFontOpenPattern with a pattern that has not been filtered
165        through XftFontMatch does not work.
166      - XftFontMatch substitutes another font if the pattern doesn't match.
167          - If the pattern has family "Courier", it substitutes a fixed width
168            font, e.g. "Liberation Mono".
169          - Otherwise it substitutes a variable width font, e.g. "DejaVu Sans".
170            It does this even if the pattern contained "spacing=100" or "110",
171            indicating a monospace or charcell font.
172      - XftXlfdParse does not translate "spacing" from XLFD to XftPattern,
173        but that doesn't matter since XftFontMatch ignores it anyway.
174
175    Xft source is here:
176    https://opensource.apple.com/source/X11ForMacOSXSource/X11ForMacOSXSource-1.0/xc/lib/Xft1/
177
178    Courier, Helvetica and the other historical PostScript font names seem to
179    be special-cased in /etc/fonts/conf.d/ in the files 30-metric-aliases.conf,
180    45-latin.conf and/or 60-latin.conf, which uses an idiosyncratic scripting
181    language implemented as XML!  Some incomplete documentation on that baroque
182    mess is here:
183    https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
184
185    The Xft configuration files seem to special-case the names "monospace",
186    "serif" and "sans-serif" as generic fallback fonts.
187
188    However, "sans-serif" is problematic because it does not survive the trip
189    through XftXlfdParse and XftFontOpenPattern -- XLFD font families cannot
190    include hyphens.  So we have no choice but to turn it into "sans serif",
191    which is *not* special-cased by the Xft config files.
192
193    In summary, everything is terrible, and it's a wonder anything works at all.
194  */
195 static Bool
196 xlfd_substituted_p (XftFont *f, const char *xlfd)
197 {
198 # ifndef HAVE_XFT
199   return False;         /* No substitutions in the Xft emulator. */
200 # else /* HAVE_XFT */
201
202   Bool ret = True;
203   const char *oxlfd = xlfd;
204   char *s = 0;
205   char *xname = 0;
206   char fname[1024];
207
208   if (*xlfd == '-' || strchr (xlfd, '*'))       /* it's an xlfd */
209     {
210       if (*xlfd != '-') goto FAIL;
211       xlfd++;
212       s = strchr (xlfd, '-'); if (!s) goto FAIL;        /* skip foundry */
213       xlfd = s+1;
214       s = strchr (xlfd, '-'); if (!s) goto FAIL;        /* skip family */
215
216       {
217         char buf[1024];
218         XftPattern *pat = XftXlfdParse (oxlfd, True, True);
219         XftNameUnparse (pat, buf, sizeof(buf)-1);
220       }
221     }
222   else                                          /* It's an Xft name */
223     {
224       s = strchr (xlfd, '-'); if (!s) goto FAIL;        /* skip family */
225     }
226
227   xname = strdup (xlfd);
228   xname[s - xlfd] = 0;
229
230   *fname = 0;
231   XftNameUnparse (f->pattern, fname, sizeof(fname)-1);
232   s = strchr (fname, ':');   /* Strip to "Family-NN" */
233   if (s) *s = 0;
234   s = strrchr (fname, '-');  /* Strip to family */
235   if (s) *s = 0;
236
237   ret = !strcasestr (fname, xname);
238
239 #  ifdef DEBUG
240   if (ret)
241     fprintf (stderr, "%s: XFT: requested \"%s\" but got \"%s\"\n",
242              progname, xname, fname);
243 #  endif
244
245  FAIL:
246
247   if (!s)
248     fprintf (stderr, "%s: unparsable XLFD: %s\n", progname, oxlfd);
249   if (xname) free (xname);
250   return ret;
251 # endif /* HAVE_XFT */
252 }
253 #endif /* USE_XFT */
254
255
256 static void *
257 load_font_retry_1 (Display *dpy, int screen, const char *font_list, Bool xft_p)
258 {
259
260 # ifdef USE_XFT
261 #  define LOADFONT(F) (xft_p \
262                       ? (void *) XftFontOpenXlfd (dpy, screen, (F))  \
263                       : (void *) XLoadQueryFont (dpy, (F)))
264 #  define UNLOADFONT(F) (xft_p \
265                          ? (void) XftFontClose (dpy, (F)) \
266                          : (void) XFreeFont (dpy, (F)))
267 # else
268 #  define LOADFONT(F) ((void *) XLoadQueryFont (dpy, (F)))
269 #  define UNLOADFONT(F) XFreeFont (dpy, (F))
270 # endif
271
272   char *font_name = 0;
273   void *f = 0;
274   void *fallback = 0;
275
276 # ifndef USE_XFT
277   if (xft_p) abort();
278 # endif
279
280 # ifdef SELFTEST
281   xft_selftest (dpy, screen);
282 # endif
283
284   if (! font_list) font_list = "<null>";
285
286   /* Treat the string as a comma-separated list of font names.
287      Names are XLFDs or the XScreenSaver syntax described above.
288      Try to load each of them in order.
289      If a substitution was made, keep going, unless this is the last.
290    */
291   if (font_list)
292     {
293       char *token = strdup (font_list);
294       char *otoken = token;
295       char *name2, *lasts;
296       if (!token) abort();
297       while ((name2 = strtok_r (token, ",", &lasts)))
298          {
299           int L;
300           token = 0;
301
302           /* Strip leading and trailing whitespace */
303           while (*name2 == ' ' || *name2 == '\t' || *name2 == '\n')
304             name2++;
305           L = strlen(name2);
306           while (L && (name2[L-1] == ' ' || name2[L-1] == '\t' ||
307                        name2[L-1] == '\n'))
308             name2[--L] = 0;
309
310           if (font_name) free (font_name);
311           font_name = font_name_to_xlfd (name2);
312
313 # ifdef DEBUG
314           fprintf (stderr, "%s: trying \"%s\" as \"%s\"\n", progname,
315                    name2, font_name);
316 # endif
317
318           f = LOADFONT (font_name);
319
320 # ifdef USE_XFT
321           /* If we did not get an exact match for the font family we requested,
322              reject this font and try the next one in the list. */
323           if (f && xft_p && xlfd_substituted_p (f, font_name))
324             {
325               if (fallback)
326                 UNLOADFONT (fallback);
327               fallback = f;
328               f = 0;
329             }
330 #  ifdef DEBUG
331           else if (!f)
332             fprintf (stderr, "%s: no match for \"%s\"\n", progname, font_name);
333 #  endif
334 # endif /* USE_XFT */
335
336           if (f) break;
337         }
338       free (otoken);
339       if (!font_name) abort();
340
341       /* If the last font in the list was an Xft pattern that matched but
342          was inexact, use it. */
343       if (!f)
344         {
345           f = fallback;
346           fallback = 0;
347         }
348     }
349
350   if (!font_name) abort();
351
352 # if !defined(HAVE_XFT) && !defined(HAVE_JWXYZ)
353   /* Xft is now REQUIRED. All of XScreenSaver's resource settings use
354      font names that assume that it is available. However, this lets you
355      limp along without it. But, really, don't do that to yourself. */
356 #   define FALLBACK2(F) do {    \
357       free (font_name);         \
358       font_name = (strdup(F));  \
359       f = LOADFONT (font_name); \
360       if (f) fprintf (stderr, "%s: non-XFT fallback \"%s\"\n", \
361                       progname, font_name);                    \
362     } while (0)
363     if (!f && !strcasestr (font_name, "mono")) {
364       if (!f) FALLBACK2 ("-*-sans serif-medium-r-*-*-*-180-*-*-*-*-*-*");
365       if (!f) FALLBACK2 ("-*-helvetica-medium-r-*-*-*-180-*-*-*-*-*-*");
366       if (!f) FALLBACK2 ("-*-nimbus sans l-medium-r-*-*-*-180-*-*-*-*-*-*");
367     }
368     if (!f) FALLBACK2 ("-*-courier-medium-r-*-*-*-180-*-*-*-*-*-*");
369     if (!f) FALLBACK2 ("fixed");
370 # endif
371
372   if (!f) abort();
373
374 # ifdef DEBUG
375   if (f && font_name)
376     fprintf (stderr, "%s:%s loaded %s\n", progname,
377              (xft_p ? " XFT:" : ""), font_name);
378 #  if defined(USE_XFT) && defined(HAVE_XFT)
379    if (xft_p && f)
380      {
381        XftPattern *p = ((XftFont *) f)->pattern;
382        char name[1024];
383        char *s, *s1, *s2, *s3;
384        XftNameUnparse (p, name, sizeof(name)-1);
385        s = strstr (name, ":style=");
386        s1 = (s ? strstr (s+1, ",") : 0);
387        s2 = (s ? strstr (s+1, ":") : 0);
388        s3 = (s1 && s1 < s2 ? s1 : s2);
389        if (s3) strcpy (s3+1, " [...]");
390        fprintf (stderr, "%s: XFT name: %s\n", progname, name);
391      }
392 #  endif /* USE_XFT && HAVE_XFT */
393 # endif /* DEBUG */
394
395    if (fallback) UNLOADFONT (fallback);
396    if (font_name) free (font_name);
397   return f;
398 }
399
400
401 #if 1  /* No longer used in XScreenSaver 6.
402           (Used by retired flag, juggle, xsublim) */
403 XFontStruct *
404 load_font_retry (Display *dpy, const char *font_list)
405 {
406   return (XFontStruct *) load_font_retry_1 (dpy, 0, font_list, 0);
407 }
408 #endif
409
410 #if defined(USE_XFT) || defined(HAVE_JWXYZ)
411 XftFont *
412 load_xft_font_retry (Display *dpy, int screen, const char *font_list)
413 {
414   return (XftFont *) load_font_retry_1 (dpy, screen, font_list, 1);
415 }
416 #endif
417
418
419
420 #ifdef SELFTEST
421 static void
422 xft_selftest (Display *dpy, int screen)
423 {
424   int i;
425   const char *tests[] = {
426     "-*-ocr a std-medium-r-*-*-*-480-*-*-m-*-*-*",
427     "OCR A Std-48",
428     "OCR A Std-48:style=Bold Italic",
429     "OCR A Std-48:spacing=100",
430     "OCR A Std-48:spacing=110",
431     "OCR A-48",
432     "OCR A-48:style=Bold Italic",
433     "OCR A-48:spacing=100",
434     "OCR A-48:spacing=110",
435     "-*-courier-medium-r-*-*-*-480-*-*-m-*-*-*",
436     "-*-courier-bold-o-*-*-*-480-*-*-m-*-*-*",
437     "Courier-48:style=Bold Italic",
438     "Courier-48:style=Italic Bold",  /* seems to be illegal */
439     "Courier-48:spacing=100",
440     "Courier-48:spacing=110",
441     "-*-helvetica-bold-o-*-*-*-480-*-*-m-*-*-*",
442     "Helvetica-48:style=Bold Italic",
443     "Liberation Mono-48:style=Bold Italic",
444     "Liberation Sans-48:style=Bold Italic",
445     "-*-sans serif-bold-o-*-*-*-480-*-*-m-*-*-*",
446     "-*-sans-serif-bold-o-*-*-*-480-*-*-m-*-*-*",
447     "-*-sans\\-serif-bold-o-*-*-*-480-*-*-m-*-*-*",
448   };
449
450   const char *tests2[] = { "Helvetica 10",
451                            "Helvetica Bold 10",
452                            "Helvetica Bold Italic 10",
453                            "Helvetica Oblique Bold-10.5",
454                            "Times New Roman-10",
455                            "Times New Roman Bold-10",
456                            "Times New Roman-Bold Oblique Italic 10",
457                            "Times New Roman-Oblique Italic Bold 10",
458                            "Times-20:style=Bold",
459                            "Times-Oblique-20:style=Bold",
460                            "sans serif-20:style=Bold",
461                            "sans-serif-20:style=Bold",
462                            "sans\\-serif-20:style=Bold",
463   };
464
465   fprintf (stderr, "\n");
466   for (i = 0; i < countof(tests2); i++)
467     fprintf (stderr, "%s\n%s\n\n", tests2[i], font_name_to_xlfd (tests2[i]));
468
469   fprintf (stderr, "\n");
470   for (i = 0; i < countof(tests); i++) {
471     const char *name1 = tests[i];
472     XftResult ret;
473     XftPattern *pat1 = 0, *pat2 = 0, *pat3 = 0;
474     char name2[1024], name3[1024];
475     XftFont *ff;
476     Bool xlfd_p = (*name1 == '-');
477
478 # define TRUNC(V) do {                          \
479       char *s = strstr (V, ":style=");          \
480       char *s1 = (s ? strstr (s+1, ",") : 0);   \
481       char *s2 = (s ? strstr (s+1, ":") : 0);   \
482       char *s3 = (s1 && s1 < s2 ? s1 : s2);     \
483       if (s3) strcpy (s3+1, " [...]");          \
484     } while(0)
485
486     *name2 = 0;
487
488     /* Name to Parse */
489
490     if (xlfd_p)
491       pat1 = XftXlfdParse (name1, True, True);
492     else
493       pat1 = XftNameParse (name1);
494     XftNameUnparse (pat1, name2, sizeof(name2)-1);
495     TRUNC(name2);
496     fprintf (stderr, "%s (\"%s\")\n\t-> \"%s\"\n",
497              (xlfd_p ? "XftXlfdParse" : "XftNameParse"),
498              name1, name2);
499
500
501     /* Name to pattern to Open */
502
503     ff = XftFontOpenPattern (dpy, pat1);
504     if (ff) {
505       pat2 = ff->pattern;
506       XftNameUnparse (pat2, name3, sizeof(name3)-1);
507     } else {
508       pat2 = 0;
509       strcpy (name3, "NULL");
510     }
511     TRUNC(name3);
512     fprintf (stderr, "XftFontOpenPattern (\"%s\")\n\t-> \"%s\"\n",
513              name2, name3);
514
515
516     /* Name to pattern to Match */
517
518     pat2 = XftFontMatch (dpy, screen, pat1, &ret);
519     XftNameUnparse (pat2, name3, sizeof(name3)-1);
520     TRUNC(name3);
521     fprintf (stderr, "XftFontMatch (\"%s\")\n\t-> \"%s\", %s\n",
522              name2, name3,
523              (ret == XftResultMatch ? "Match" :
524               ret == XftResultNoMatch ? "NoMatch" :
525               ret == XftResultTypeMismatch ? "TypeMismatch" :
526               ret == XftResultNoId ? "NoId" : "???"));
527
528
529     /* Name to pattern to Match to Open */
530
531     strcpy (name2, name3);
532     ff = XftFontOpenPattern (dpy, pat2);
533     if (ff) {
534       pat3 = ff->pattern;
535       XftNameUnparse (pat3, name3, sizeof(name3)-1);
536     } else {
537       pat3 = 0;
538       strcpy (name3, "NULL");
539     }
540     TRUNC(name3);
541     fprintf (stderr, "XftFontOpenPattern (\"%s\")\n\t-> \"%s\"\n",
542              name2, name3);
543
544
545     /* Name to Open */
546
547     ff = (xlfd_p
548           ? XftFontOpenXlfd (dpy, screen, name1)
549           : XftFontOpenName (dpy, screen, name1));
550     *name2 = 0;
551     if (ff) {
552       pat1 = ff->pattern;
553       XftNameUnparse (pat1, name2, sizeof(name2)-1);
554     } else {
555       strcpy (name2, "NULL");
556     }
557     TRUNC(name2);
558     fprintf (stderr, "%s (\"%s\")\n\t-> \"%s\"\n",
559              (xlfd_p ? "XftFontOpenXlfd" : "XftFontOpenName"),
560              name1, name2);
561     fprintf (stderr, "\n");
562   }
563
564   exit (0);
565 }
566 #endif /* SELFTEST */