matplotlib: Can I use a secondary font for missing glyphs? matplotlib: Can I use a secondary font for missing glyphs? python python

matplotlib: Can I use a secondary font for missing glyphs?


Here's my thinking:

Create a function taking x - the starting x position, y - the y position, text - the text to draw, and fallbackList - a list of fonts, ordered like font-family in CSS.

  1. When rendering the text, use fontTools.ttLib.TTFont to check if a certain character is contained within the primary font (fallbackList[0]), by parsing the font table, looping through it, and checking if the given character is inside the map (see this question).
  2. If the result of step 1 is False (i.e it's not contained within the font pack), go through fallbackList, repeating step 1, until you find a font that does contain it. If you find a character with no font containing it, just use the first font. We'll call this font we just found foundFont.
  3. Draw that character with textpath.TextPath((xPosNow, y) ...stuff... prop=foundFont).getextents() (matplotlib > 1.0.0). This draws that character, but also gets the bounding box of the text, from which you can extract the width (see this question). Do xPosNow += textWidth, where textWidth is extracted from getextents().

This would essentially keep a tally of the total distance from the origin (by adding together the width of each bit of text you add), and then when you need to add another bit of text in a different font, simply set the x value to be this tally + a little bit for kerning, and this way, you can just work out where you want each character to go (but do each character separately).

Here's a code example:

import matplotlib.pyplot as pltfrom matplotlib.textpath import TextPathfrom fontTools.ttLib import TTFontfig = plt.figure()plt.axis([0, 8, 0, 6])t = u'abcde♥'plt.text(4.5, 4, 'DejaVu Sans:', horizontalalignment='right')plt.text(5, 4, t, {'family':'DejaVu Sans'})plt.text(4.5, 3, 'Noto Sans:', horizontalalignment='right')plt.text(5, 3, t, {'family':'Noto Sans'})plt.text(4.5, 2, 'Noto Sans Symbols2:', horizontalalignment='right')plt.text(5, 2, t, {'family':'Noto Sans Symbols2'})def doesContain(fontPath, unicode_char):  # Helper function, the only issue being it takes font paths instead of names    font = TTFont(fontPath)  # Use helper library to go through all characters    for cmap in font['cmap'].tables:        if cmap.isUnicode():            if ord(unicode_char) in cmap.cmap:  # If the character table contains our character return True                return True    # Otherwise, return False.    return Falsedef renderText(x, y, text, fontSize, fallback_list, spacingSize):    xPosNow = x    for char in text:  # For each character...        fontId = 0        while not doesContain(fallback_list[fontId]['path'], char):  # find a font that works            if fontId < len(fallback_list) - 1:                fontId += 1            else:  # Or just go with the first font, if nothing seems to match                fontId = 0                break        print(fontId)        t = plt.text(xPosNow, y, char, {'family':fallback_list[fontId]['name']})        r = fig.canvas.get_renderer()        xPosNow += t.get_window_extent(renderer=r).width/100 + spacingSize

We call it with:

renderText(3, 5, t, 9, [        {            'path': 'C:\\Users\\User\\Downloads\\NotoSans-hinted\\NotoSans-Regular.ttf',  # Font path            'name': 'Noto Sans'  # Font name        },        {            'path': 'C:\\Users\\User\\Downloads\\NotoSansSymbols2-unhinted\\NotoSansSymbols2-Regular.ttf',            'name': 'Noto Sans Symbols2'        }    ], 0.08)  # The distance between the lettersplt.show()

And the output is:

image