X11 fonts

Dear Lazyweb,

  1. In This Modern World, is it possible for a non-privileged X11 app to hand a TTF file to libXft and then have XftDrawString* work?

  2. Failing that, is there any portable way for "sudo make install" to install the file in such a way that XftFontOpen will immediately be able to find it?

I'm pretty sure the answer to #2 is "Hahahahahaha no".

Tags: , , ,

33 Responses:

  1. Eli the Bearded says:

    I see:

    2.1.1. Installing fonts in Xft

    Fontconfig looks for fonts in a set of well-known directories that include all of X11R6.8.2's standard font directories (`/usr/X11R6/lib/X11/lib/fonts/*') by default) as well as a directory called `.fonts/' in the user's home directory. Installing a font for use by Xft applications is as simple as copying a font file into one of these directories.

    Is that not working for you? It "works" for me in the sense that I can use those fonts in various X11 programs, but I haven't XftFontOpen()ed it myself.

    • jwz says:

      I believe this answer is incomplete, and doesn't work until you've also bled a chicken with runes like "fonts.dir" and "fonts.cache" carved into its skin, and probably also restarted the X server. So this falls into the category of "Have you tried it? You haven't tried it, have you?"

  2. Narr says:

    Don’t forget to check that the install dir is listed in /usr/X11R6/lib/X11/XftConfig, and reload the cache with xftcache. When is the cache read, though?

  3. Chipaca says:

    I was pretty sure this should work, so I just munged together the examples from libcurl and xft to confirm it, and indeed it does, so here goes:
    First, determine the right current-user fonts directory: if "$HOME/.fonts" exists, use that. Otherwise check for or create "${XDG_DATA_HOME:-$HOME/.local/share}/fonts/".

    In that directory, check for and otherwise download the font.
    Then XftFontOpenName will work. It's supposed to just be a convenient wrapper to more precise calls, but I didn't try them.

    This is "portable" in the sense that it will work on any linux that has xft, as long as you aren't running in a sandbox (and in most cases can be made to work if you are).

    I can clean up the munged code if you want to see it, but there's not a lot of value to it :-)

    • jwz says:

      I'd like to see your example, thanks. By "sandbox" do you mean seccomp or something else, and how would that affect it?

        • Simon says:

          You can use a font file that resides anywhere by making use of fontconfig.

          I basically used the gist from above and just dropped all of the ensurefontfile() stuff (including curl and directory poking).

          Then it is just:


          #include

          [...]

          int main() {
          FcConfigAppFontAddFile (FcConfigGetCurrent (), (FcChar8 *) "/tmp/hanalei.ttf");
          win();
          return 0;
          }

          adjust the path to your font file accordingly.

          for compiling add "fontconfig" to the list of packages for pkgconfig.

          • Simon says:

            gah.


            #include <fontconfig/fontconfig.h>

            of course.

          • Simon says:

            and a small addendum:

            There also is FcConfigAppFontAddDir() which scans a directory for all the font files.

            Also you can pass NULL to the first argument, in that case it uses the current configuration by default.

          • Chipaca says:

            Excellent.

            I'll add one to the ‘easiest way to get the right answer is by stating the wrong one’ tally.

          • Chipaca says:

            Dunno if you saw jwz's comment below where he shows the code he's trying to work with; his main problem seems to be due to the fallback mechanism. Is there a way to turn that off, for an app?
            (if there is, reply over there... :)

            • jwz says:

              Well it's really two different problems. "How do I use a custom font" and "how can get the font I actually asked for and not some wildly inappropriate substitution".

              • Simon says:

                Well, yeah. That is a different problem than you stated originally...

                I am not really that familiar with fontconfig - it was just too easy to solve with that kind-of-obvious function call.

                My gut recommendation is, that you should try to avoid the by now pretty much obsolete xlfd's and use the fontconfig pattern matching instead. But I am very much in have-not-tried-it-myself territory here and while there is a documentation on the functions of fontconfig on their site, the "What do I have to do to get this result?"-aspect is not really there...

                I still have scars from trying to improve the font selector in GIMP, trying to work with PangoFontDescriptions. That was also quite a shit show with multiple fonts mapping to the same PFD. I can only hope that using fontconfig directly works better...

                • jwz says:

                  Well it turns out that XLFDs and FC patterns have exactly the same behavior -- once you get down into FcFontSetMatch it silently substitutes for missing fonts. So if you asked for a fixed-width font, enjoy your variable-width substitution!

                  This doesn't happen for Courier because they special-case that one. Sigh.

                  My best guess at this point is: load the font, and then see if its name matches the family we passed in, and if not, unload and reject it. Which is some bullshit.

                  • Simon says:

                    I found https://unix.stackexchange.com/questions/363365/command-to-list-all-monospace-fonts-known-to-fontconfig and indeed "fc-list :spacing=100" lists just the monospaced fonts in my system.

                    Maybe it helps to add this "spacing=100" to your pattern. And if that fails list all fonts with spacing=100 and pick a random monospaced font...

                  • jwz says:

                    Huh, this seems like XftXlfdParse ought to be translating *-m-* to that, but is not. However that just means that now you have to remember the difference between "monospace" and "charcell" fonts. One of those has the magic number 100 and the other has the magic number 110. They both mean fixed-width but differ in whether any ink escapes the box.

                  • jwz says:

                    Wait, I'm wrong. XftXlfdParse does correctly translate *-m-* to :spacing=100 but it doesn't matter because FcFontSetMatch ignores it: "No Such Font-48:spacing=100" turns into "DejaVu Sans" instead of "Liberation Mono".

                  • jwz says:

                    Correction again, XftXlfdParse does not translate *-m-* to :spacing=100, but it doesn't matter because XftFontMatch ignores spacing when deciding whether to return "DejaVu Sans" or "Liberation Mono".

      • Chipaca says:

        ... and by 'sandbox' i was thinking of either flatpack or snaps, which use a combination of mount namespaces, seccomp, and either apparmor or selinux. Things can get messy.

      • Chipaca says:

        You should disregard my suggestion and do what Simon says (in https://www.jwz.org/blog/2021/02/x11-fonts/#comment-215580), using fontconfig, if you can. It is superior in at least two ways: the fiddly bits are taken care of by a library somebody else maintains, and you don't need to plonk your fonts in the user's font dir (which they might object to, knowing them).

  4. What do the various X11-based web browsers do when they fetch a .ttf from the net?

    • swiley says:

      I’d sure hope browsers aren’t handing untrusted otf/ttf files to X11!
      They’re probably using freetype or so to render text client side and painting that into a shared memory buffer.

  5. jwz says:

    Here's a problem I'm trying to solve: I would like to use the "OCR" font if it is available, otherwise, I would like to use some fixed-width font. But I can't figure out how to ask Xft whether a font actually exists without it silently doing terrible, inappropriate substitutions on me first.

    When OCR-A is installed:

    XftXlfdParse("-*-ocr a std-medium-r-*-*-*-480-*-*-m-*-*-*") -> "ocr a std-48:slant=0:weight=100"
    XftFontOpenXlfd("-*-ocr a std-medium-r-*-*-*-480-*-*-m-*-*-*") -> "OCR A Std-48:familylang=en:style=Regular: [...]"
    XftFontMatch(...) -> Match "OCR A Std-48:familylang=en:style=Regular: [...]"

    XftNameParse("OCR A Std-48") -> "OCR A Std-48"
    XftFontOpenName("OCR A Std-48") -> "OCR A Std-48:familylang=en:style=Regular: [...]"
    XftFontMatch(...) -> Match "OCR A Std-48:familylang=en:style=Regular: [...]"

    XftNameParse("OCR A Std-48:style=Bold Italic") -> "OCR A Std-48:style=Bold Italic"
    XftFontOpenName("OCR A Std-48:style=Bold Italic") -> "OCR A Std-48:familylang=en:style=Regular: [...]"
    XftFontMatch(...) -> Match "OCR A Std-48:familylang=en:style=Regular: [...]"

    When OCR-A is not installed, no matter what I do I get "DejaVu Sans", which is a terrible substitution because it is a variable-width font:

    XftXlfdParse("-*-ocr a std-medium-r-*-*-*-480-*-*-m-*-*-*") -> "ocr a std-48:slant=0:weight=100"
    XftFontOpenXlfd("-*-ocr a std-medium-r-*-*-*-480-*-*-m-*-*-*") -> "DejaVu Sans-48:familylang=en:style=Book: [...]"
    XftFontMatch(...) -> Match "DejaVu Sans-48:familylang=en:style=Book: [...]"

    XftNameParse("OCR A Std-48") -> "OCR A Std-48"
    XftFontOpenName("OCR A Std-48") -> "DejaVu Sans-48:familylang=en:style=Book: [...]"
    XftFontMatch(...) -> Match "DejaVu Sans-48:familylang=en:style=Book: [...]"

    XftNameParse("OCR A Std-48:style=Bold Italic") -> "OCR A Std-48:style=Bold Italic"
    XftFontOpenName("OCR A Std-48:style=Bold Italic") -> "DejaVu Sans-48:familylang=en:style=Book: [...]"
    XftFontMatch(...) -> Match "DejaVu Sans-48:familylang=en:style=Book: [...]"

    Test code:

    int i;
    const char *tests[] = {
      "-*-ocr a std-medium-r-*-*-*-480-*-*-m-*-*-*",
      "OCR A Std-48",
      "OCR A Std-48:style=Bold Italic",
      "-*-courier-bold-o-*-*-*-480-*-*-m-*-*-*",
      "-*-helvetica-bold-o-*-*-*-480-*-*-m-*-*-*",
      "Courier-48:style=Bold Italic",
      "Helvetica-48:style=Bold Italic",
      "Liberation Mono-48:style=Bold Italic",
      "Liberation Sans-48:style=Bold Italic",
    };
    for (i = 0; i < countof(tests); i++) {
      const char *name1 = tests[i];
      XftResult ret;
      XftPattern *pat;
      char name2[1024];
      XftFont *ff;

    # define TRUNC() do {
        char *s = strstr (name2, ":style=")
        char *s1 = (s ? strstr (s+1, ",") : 0);
        char *s2 = (s ? strstr (s+1, ":") : 0)
        char *s3 = (s1 && s1 < s2 ? s1 : s2);
        if (s3) strcpy (s3+1, " [...]");
      } while(0)
      *name2 = 0;
      if (*name1 == '-') {
        pat = XftXlfdParse (name1, True, True);
        XftNameUnparse (pat, name2, sizeof(name2)-1);
        TRUNC();
        fprintf (stderr, "XftXlfdParse("%s") -> "%s"n", name1, name2);

        ff = XftFontOpenXlfd (dpy, screen, name1);
        if (ff)
          XftNameUnparse (ff->pattern, name2, sizeof(name2)-1);
        TRUNC();
        fprintf (stderr, "XftFontOpenXlfd("%s") -> "%s"n", name1, name2);

      } else {
        pat = XftNameParse (name1);
        XftNameUnparse (pat, name2, sizeof(name2)-1);
        TRUNC();
        fprintf (stderr, "XftNameParse("%s") -> "%s"n", name1, name2);

        ff = XftFontOpenName (dpy, screen, name1);
        if (ff)
          XftNameUnparse (ff->pattern, name2, sizeof(name2)-1);
        TRUNC();
        fprintf (stderr, "XftFontOpenName("%s") -> "%s"n", name1, name2);
      }

      pat = XftFontMatch (dpy, screen, pat, &ret);
      XftNameUnparse (pat, name2, sizeof(name2)-1);
      TRUNC();
      fprintf (stderr, "XftFontMatch(...) -> %s "%s"n",
              (ret == XftResultMatch ? "Match" :
                ret == XftResultNoMatch ? "NoMatch" :
                ret == XftResultTypeMismatch ? "TypeMismatch" :
                ret == XftResultNoId ? "NoId" : "???"),
              name2);
      fprintf (stderr, "n");
    }

    • jwz says:

      Here is what I have learned:

      • XftFontOpenXlfd is defined as: XftFontOpenPattern (XftFontMatch (XftXlfdParse (xlfd)))
      • XftFontOpenName is defined as: XftFontOpenPattern (XftFontMatch (XftNameParse (name)))
      • Calling XftFontOpenPattern with a pattern that has not been filtered through XftFontMatch does not work.
      • XftFontMatch substitutes another font if the pattern doesn't match.
          • If the pattern has family "Courier", it substitutes a fixed width font, e.g. "Liberation Mono".
          • Otherwise it substitutes a variable width font, e.g. "DejaVu Sans".
            It does this even if the pattern contained "spacing=100" or "110", indicating a monospace or charcell font.
      • XftXlfdParse does not translate "spacing" from XLFD to XftPattern, but that doesn't matter since XftFontMatch ignores it anyway.

  6. Nick says:

    Hi Jamie, why do you put yourself through this. If it were me I’d have given up on this nonsense long ago

    • Nibby says:

      I'm sure the software in question wouldn't even exist from the beginning 1000x over if taking "No" for an answer was on the table. Even if you have to shove that font bit by bit through the X server's socket, and craft an non-conformant packet to get it to wharfle it back up, or just patch it into memory, that font will damn well show up. If you consider it ran on an iphone, police car, among many other improbable places, you can start to grasp that this is a cultural artefact that will be defiantly made to run on even more impossible things long after we're all gone.

    • margaret says:

      maybe try something easier, like running a nightclub in sf during a pandemic.

    • dcapacitor says:

      The razor-thin distinction between "BS jwz is willing to put up with" vs "BS jwz is absolutely not willing to put up with" is endlessly fascinating to me.

  7. jwz says:

    It looks like just dropping a TTF or OTF into any subdirectory of /usr/share/fonts/ causes it to immediately become visible to XftFontOpenName and even to XftFontOpenXlfd, which is so eminently reasonable that I keep waiting for the other shoe to drop.

    That might not make the fonts visible to XLoadFont, but I think I'm coming around to the idea that, these days, trying to do anything with Xlib fonts instead of Xft fonts is just self-abuse.

  8. Glynn says:

    Xft uses FontConfig for everything related to loading fonts. It uses the "current" config which can be changed with FcConfigSetCurrent(). If you want more control over the loading process, you can create an empty FcConfig, add only the font(s) you want, make that current, call XftFontOpen() (etc), then (optionally) restore the original config.

    Most of this has no meaningful documentation beyond the source code.

Leave a Reply

Your email address will not be published. But if you provide a fake email address, I will likely assume that you are a troll, and not publish your comment.

You may use these HTML tags and attributes: <a href="" title=""> <b> <blockquote cite=""> <code> <em> <i> <s> <strike> <strong> <img src="" width="" height="" style=""> <iframe src="" class=""> <video src="" class="" controls="" loop="" muted="" autoplay="" playsinline=""> <div class=""> <blink> <tt> <u>, or *italics*.