Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

The font I want to use doesn't have all the symbols I need. Is it possible to have matplotlib use a different font if a symbol is missing?

Here is a minimal example:

import matplotlib.pyplot as plt

fig = 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'})

plt.show()

And the output:

And the output.

Noto Sans is missing the heart symbol while Noto Sans Symbols2 is missing the letters. I'm trying to get something like the DejaVu Sans example but with letters from Noto Sans and the heart from Noto Sans Symbols2.

like image 238
user10735027 Avatar asked Dec 02 '18 15:12

user10735027


People also ask

How do I add fonts to Matplotlib?

Usually, double-click on the . ttf file and then click on the Install button in the window that pops up. Note that Matplotlib handles fonts in True Type Format (. ttf) , so make sure you install fonts ending in .

Where is Matplotlib font cache?

To map font names to font files, matplotlib has a dictionary (or json file) located in its cache directory. Note, this file is not always in the same place, but usually sits at the home directory. If you are on mac (windows), it usually sits at whereever your HOME (%HOME%) environmental variable is set to.

What fonts are available in Matplotlib?

Matplotlib can use font families installed on the user's computer, i.e. Helvetica, Times, etc. Font families can also be specified with generic-family aliases like ( {'cursive', 'fantasy', 'monospace', 'sans', 'sans serif', 'sans-serif', 'serif'} ).

How do I change the font of my Matplotlib title?

In Matplotlib, to set the title of a plot you have to use the title() method and pass the fontsize argument to change its font size.


1 Answers

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 plt
from matplotlib.textpath import TextPath
from fontTools.ttLib import TTFont

fig = 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 False

def 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 letters

plt.show()

And the output is:

image

like image 123
Geza Kerecsenyi Avatar answered Oct 27 '22 01:10

Geza Kerecsenyi