10

Similar to this question, I'm not asking how to find the number of characters in a string. I would like to determine the visual length of a string as rendered or compare it to another string.

For example, both 'iiii' and 'WWWW' have four characters. However, 'iiii' is shorter visually. I'm aware that this is determined by font, and I'm not working with monospaced fonts. So, for the purposes of this problem, I'll be using Arial 10pt.

Are there any built-in modules which will provide the visual dimensions of a string given a font?

Fezter
  • 221
  • 1
  • 3
  • 12
  • I reworded your question to say ‘visual’ rather than ‘physical’ length of the string, as I think this is clearer—if you disagree, feel free to revert my change. – icktoofay Sep 13 '15 at 22:09
  • 1
    The term *physical length* of a string isn't really a thing; you are talking about the *display* or visual length, when using a proportional font. What GUI or image generation system are we talking about here? Measuring the font metrics depends on the framework used. – Martijn Pieters Sep 13 '15 at 22:11
  • The only "real" way to know how something renders on the screen where your code is run is to calculate it either on the screen and then get the pixels, or pre-render it in a virtual rendering environment (like how headless browsers work). –  Sep 13 '15 at 22:11
  • You can print the string and then get the size of the image using the library of your choice such as image font: http://effbot.org/imagingbook/imagefont.htm – Maxime Chéramy Sep 13 '15 at 22:11
  • @ErwinRenkema: various frameworks have utility methods that tell you the size in pixels for a given text. – Martijn Pieters Sep 13 '15 at 22:12
  • Duplicate: http://stackoverflow.com/questions/16007743/library-to-roughly-approximate-the-width-of-a-string-of-text – Maxime Chéramy Sep 13 '15 at 22:13
  • Instead of playing games with pixel counting, you can calculate width by using the font metrics. [Here](http://matplotlib.org/api/afm_api.html) is a pretty complete solution (for compatible fonts). If the question gets reopened, I'll expand it into an answer. – alexis Sep 14 '15 at 00:22
  • @alexis, thanks! This is exactly what I was looking for. I suppose Helevetica is pretty close to Arial, so using that font, might be sufficient. – Fezter Sep 14 '15 at 00:50
  • Added an answer with afm and tkinter solutions. Tkinter can calculate width for Arial (but needs a tkinter window, though it doesn't render anything). – alexis Sep 14 '15 at 14:41

4 Answers4

10

Instead of rendering into an image buffer and counting pixels, you can calculate width directly by using the font metrics. There doesn't seem to be a font API distributed with core python, but there are plenty of third-party ones in various packages. Here's a pretty complete solution for Adobe font metrics, using matplotlib:

>>> from matplotlib import rcParams
>>> import os.path

>>> afm_filename = os.path.join(rcParams['datapath'], 'fonts', 'afm', 'ptmr8a.afm')
>>>
>>> from matplotlib.afm import AFM
>>> afm = AFM(open(afm_filename, "rb"))
>>> afm.string_width_height('What the heck?')
(6220.0, 694)

The metrics are reported in units of 1/1000 of the scale factor (point size) of the font being used. (Thanks @JacobLee for digging up this information.)

Another possibility is the tkFont module of tkinter. This page documents the function tkFont.Font.measure("some string"), but it seems you need a Tk window before you can use it; so I don't know how practical it is:

# Python 3 names -- see Note below
import tkinter 
from tkinter import font as tkFont

tkinter.Frame().destroy()  # Enough to initialize resources
arial36b = tkFont.Font(family='Arial', size=36, weight='bold')
width = arial36b.measure("How wide is this?")
print(width)  # Prints: 404

Note: In python 2 (and in the page I mentioned above), tkinter is known as Tkinter, and tkinter.font is a top-level module, tkFont:

import Tkinter
import tkFont
alexis
  • 48,685
  • 16
  • 101
  • 161
  • I want to retrieve text from backend where font-size and display area dimensions are sent from front-end. Can I use matplotlib in the backend, where obviously, there is no display device at all? – Mohammed Shareef C Jun 13 '17 at 09:51
  • Can you please tell me what is the unit of the values (width and height) returned by matplotlib? Pixels? mm? – Mohammed Shareef C Jun 14 '17 at 07:12
  • No idea. Read the AFM documentation or experiment and find out yourself, e.g. treat them as pixels and see what happens. – alexis Jun 14 '17 at 09:44
  • The documentation does not give any clue about the unit of the width and height. And it does not change with font-size. Therefore, afm from matplotlib is not an answer to this question. The user has to hand tune the output by setting some params and dividing or multiplying the returned values. But still, it won't give reliable output. – Mohammed Shareef C Jun 16 '17 at 09:17
  • Gonna try ImageFont from PIL (Pillow) – Mohammed Shareef C Jun 16 '17 at 09:17
  • 1
    The matplotlib.afm documentation says, "As in the Adobe Font Metrics File Format Specification, all dimensions are given in units of 1/1000 of the scale factor (point size) of the font being used." – Jacob Lee Dec 02 '19 at 17:16
  • Can I apply this method to arbitrary fonts? I think there sould be afm for the font I am interested in. Or there might some apps to generate afm for arbitrary font. I just found [this](https://stackoverflow.com/questions/1676342/generating-afm-from-ttf) – KH Kim Oct 10 '21 at 06:45
  • If you can find or generate afm metrics for your font, then I expect you can. – alexis Oct 10 '21 at 08:16
5

If you are using Windows, then the following approach could be used.

It uses the current screen as the output context and calculates the dimensions needed to display the given font at the given point size. It returns a tuple holding the text width and text height:

import ctypes

def GetTextDimensions(text, points, font):
    class SIZE(ctypes.Structure):
        _fields_ = [("cx", ctypes.c_long), ("cy", ctypes.c_long)]

    hdc = ctypes.windll.user32.GetDC(0)
    hfont = ctypes.windll.gdi32.CreateFontA(-points, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, font)
    hfont_old = ctypes.windll.gdi32.SelectObject(hdc, hfont)
    size = SIZE(0, 0)
    ctypes.windll.gdi32.GetTextExtentPoint32A(hdc, text, len(text), ctypes.byref(size))
    ctypes.windll.gdi32.SelectObject(hdc, hfont_old)
    ctypes.windll.gdi32.DeleteObject(hfont)
    return (size.cx, size.cy)

for text, font in [
    ('....', 'Arial'), 
    ('WWWW', 'Arial'), 
    ('WWWW', 'Arial Narrow'),
    ('....', 'Courier New'), 
    ('WWWW', 'Courier New'), 
    ("Test", "Unknown font"),
    ('Test', 'Calibri')]:

    print '{:8} {:20} {}'.format(text, font, GetTextDimensions(text, 12, font))

This would display the following output:

....     Arial                (12, 15)
WWWW     Arial                (44, 15)
WWWW     Arial Narrow         (36, 16)
....     Courier New          (28, 15)
WWWW     Courier New          (28, 15)
Test     Unknown font         (24, 15)
Test     Calibri              (23, 14)

Arial being a proportional font shows different dimensions for .... and WWWW but Courier New being fixed width gives the same results. Arial Narrow gives 36 compared to 44 for Arial.

In the case of Unknown font, the Windows font mapper has automatically picked a default font.

Tested on Python 2.x.


Note for Python 3.x

As this is calling GetTextExtentPoint32A() in Windows, this expects ANSI text to be passed to it, as such the call could be changed as follows to fix this:

ctypes.windll.gdi32.GetTextExtentPoint32A(hdc, text.encode('cp1252'), len(text), ctypes.byref(size))

Alternatively, switch the code to use the wide versions, replace with these two:

hfont = ctypes.windll.gdi32.CreateFontW(-points, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, font)
ctypes.windll.gdi32.GetTextExtentPoint32W(hdc, text, len(text), ctypes.byref(size))
Martin Evans
  • 45,791
  • 17
  • 81
  • 97
  • I'm not sure this is a complete solution: >>> GetTextDimensions("Test",12,"Calibri") (32, 15) >>> GetTextDimensions("Test",12,"Arial") (32, 15) >>> GetTextDimensions("Test",12,"Courier New") (32, 15) >>> GetTextDimensions("Test",12,"Fake goshdang font") (32, 15) – gss Mar 19 '18 at 14:03
  • 1
    @fearwig, I am guessing you are trying this on Python 3.x ? As it is calling an ANSI Windows function, the text would have to first be encoded. e.g. pass it `text.encode('cp1252')` – Martin Evans Mar 19 '18 at 14:58
0

Use a graphic / font library like ImageFont. Draw the string and then use the getsize to get the width.

Note that some text like "AWAY" may be narrower than the sum of the individual letters due to kerning. So it would be difficult to lookup widths of each letter and add them.

Ayman
  • 11,265
  • 16
  • 66
  • 92
  • Well, I believe that is the thing - due to kerning for some fonts the width of the string will be different to the sum of individual glyphs' widths. – sophros Feb 12 '19 at 09:41
0

Just a small improvement of the answer of @alexis with tkinter. I find that method easy and reliable, except that it opens a tkinter window which you have to close manually. I modified the code so you don't have the window anymore, and made a function with a few more options.

from tkinter import Tk
from tkinter.font import Font

def get_text_size(text: str, font_family: str = 'Arial', font_size: int = 10, bold: bool = False) -> int:
    """Get the screen width of a text based on Font Type, Font Size and Font Weight

    Args:
        text (str): Text for which to calculate the screen width
        font_family (str, optional): Font family. Defaults to 'Arial'.
        font_size (int, optional): Font size. Defaults to 10.
        bold (bool, optional): If bold or not. Defaults to False.

    Returns:
        int: Screen width of the text
    """
    root = Tk()  # Needed to estimate the width.
    font_weight = 'bold' if bold else 'normal'
    font_var = Font(family=font_family, size=font_size, weight=font_weight)
    width = font_var.measure(text)
    root.destroy()  # Destroy the created window
    return width