Search code examples
pythonpython-3.xpython-imaging-library

Writing different text fonts on the same line using PIL


I'm trying to write prices on an Image using two different fonts on the same line, one for the numbers, and one for the currency symbol (in this case, €), so I am wondering what is the proper way to do it using PIL. What I'm currently trying is to use font.getsize() on the first font, and add the X axis result of the getsize to the coordinates of the text in order to get the offset needed to write the symbol:

d_price = "248"

d_price_font = ImageFont.truetype("Gotham Black Regular.ttf", 58)
symbol_font = ImageFont.truetype("Myriad Pro Regular.ttf", 70)

d_price_size = d_price_font.getsize(d_price)
d_price_coords = (677, 519)

draw.text(d_price_coords, d_price, (0, 255, 255), font=d_price_font)
symbol_coords = (d_price_coords[0] + d_price_size[0], d_price_coords[1])
draw.text(symbol_coords, "€", (255, 255, 255), font=symbol_font)

This seem to work, however this would require me to calculate the size of the text everytime I want to write a symbol next to a price, and also adjust the font size, which is different to the one I'm using to the prices.

enter image description here


Solution

  • I think your approach is OK, but there's a nicer way to write the code that will avoid the repetition and make it easier to add more text in varied font:

    from PIL import ImageFont, Image, ImageDraw
    
    
    def draw_text_in_font(d: ImageDraw, coords: tuple[int, int], 
                          content: list[tuple[str, tuple[int, int, int], str, int]]):
        fonts = {}
        for text, color, font_name, font_size in content:
            font = fonts.setdefault(font_name, ImageFont.truetype(font_name, font_size))
            d.text(coords, text, color, font)
            coords = (coords[0] + font.getsize(text)[0], coords[1])
    
    
    with Image.open("white.png") as im:
        draw = ImageDraw.Draw(im)
    
        draw_text_in_font(draw, (10, 10), [
            ('248', (255, 255, 0), 'C:/Windows/Fonts/Consola.ttf', 70),
            ('€', (255, 0, 255), 'C:/Windows/Fonts/Arial.ttf', 56)
        ])
    
        im.show()
    

    This just bundles all you were doing in a single, fairly readable function, while calling it is pretty clear as well. You can string on more text in other fonts, and it will reuse a font once it's created it.

    It might be nicer to use a list[dict[str, Any]] for the content, or even define a dataclass for it, so you could make it clearer in your code what the values are that you're passing to draw_text_in_font, but the idea would be the same.

    Also, if you wrote a class instead, you could cache the fonts you use in class attributes, to avoid creating them every time you write a text, but that's all just niceties.

    Note that I like making the types explicit, as it helps whoever is using the code to provide the right arguments in a decent editor or IDE. But this could would work the same, just not as nice to the coder:

    def draw_text_in_font(d, coords, content):
        fonts = {}
        for text, color, font_name, font_size in content:
            font = fonts.setdefault(font_name, ImageFont.truetype(font_name, font_size))
            d.text(coords, text, color, font)
            coords = (coords[0] + font.getsize(text)[0], coords[1])