在Python中大致估算文本字符串的宽度?

7

如何使用Python,近似计算给定文本字符串的字体宽度?

我正在寻找一个原型类似于以下函数的函数:

def getApproximateFontWidth(the_string, font_name="Arial", font_size=12):
   return ... picas or pixels or something similar ...

我不需要非常严格的东西,一个近似值就可以了。

这样做的动机是因为我在我的网络应用程序后端生成了一个截断的字符串,并将其发送到前端进行显示。大多数情况下,这些字符串都是小写字母,但有时字符串全部是大写字母,使它们非常宽。如果字符串没有正确地被截断,它看起来很丑。我想知道根据它们的大致宽度要截断多少个字符。如果偏差在10%以内,这不是什么大问题,这只是一个美容功能。


你能否在前端使用 CSS 或者 JS 来实现它? - Alex L
1
理论上是可以在前端完成的,但与完整文本相比,显示的文本量非常小。我只呈现了一小部分(一两行),而完整文本可能有许多页(可能达到数百页)。如果可能的话,我更愿意将其保留在后端。 - speedplane
1
text-overflow: ellipsis 对于短文本看起来很丑。 印地语、藏语、泰语、韩语和中文都是等宽字体, 拉丁文和西里尔文应该通过字符渲染到快速查找表中,然后可以使用该查找表计算任何字符串的宽度。由于自动连字,阿拉伯语和乌尔都语应该更加复杂。 - Barney Szabolcs
4个回答

14
以下是我的简单解决方案,可以达到约80%的准确度,非常适合我的需求。它仅适用于Arial字体,仅限ASCII字符,并且假定为12pt字体大小,但它可能也适用于其他字体。
def getApproximateArialStringWidth(st):
    size = 0 # in milinches
    for s in st:
        if s in 'lij|\' ': size += 37
        elif s in '![]fI.,:;/\\t': size += 50
        elif s in '`-(){}r"': size += 60
        elif s in '*^zcsJkvxy': size += 85
        elif s in 'aebdhnopqug#$L+<>=?_~FZT' + string.digits: size += 95
        elif s in 'BSPEAKVXY&UwNRCHD': size += 112
        elif s in 'QGOMm%W@': size += 135
        else: size += 50
    return size * 6 / 1000.0 # Convert to picas

如果你想截取字符串,这里是方法:

def truncateToApproximateArialWidth(st, width):
    size = 0 # 1000 = 1 inch
    width = width * 1000 / 6 # Convert from picas to miliinches
    for i, s in enumerate(st):
        if s in 'lij|\' ': size += 37
        elif s in '![]fI.,:;/\\t': size += 50
        elif s in '`-(){}r"': size += 60
        elif s in '*^zcsJkvxy': size += 85
        elif s in 'aebdhnopqug#$L+<>=?_~FZT' + string.digits: size += 95
        elif s in 'BSPEAKVXY&UwNRCHD': size += 112
        elif s in 'QGOMm%W@': size += 135
        else: size += 50
        if size >= width:
            return st[:i+1]
    return st

接下来是以下内容:
>> width = 15
>> print truncateToApproxArialWidth("the quick brown fox jumps over the lazy dog", width) 
the quick brown fox jumps over the
>> print truncateToApproxArialWidth("THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG", width) 
THE QUICK BROWN FOX JUMPS

当渲染时,这些字符串的宽度大致相同:
快速的棕色狐狸跳过了
快速的棕色狐狸跳过了

2
为什么您认为这比PIL的font.getsize('spam')更好? - tom10
PIL是一个相当大的库,还需要加载字体文件。我只是寻找一个近似值,如果不必依赖于PIL,那就更好了。PIL肯定会更准确,并且对于非ASCII字符可能效果更好,但对于我的目的,近似值就可以了。 - speedplane
在你的问题中列出这些标准会很有用。 - tom10
有这个标准定义的链接吗? - weaming

4

是的,那确实是准确严谨的方式,但我想避免渲染图像。我打算自己写出来并在这里发布。 - speedplane
6
дҪҝз”Ёе·ІеҠ иҪҪеӯ—дҪ“зҡ„getsizeж–№жі•пјҢдҫӢеҰӮfont.getsize('spam')гҖӮе®ғиҝ”еӣһдёҖдёӘд»ҘеғҸзҙ дёәеҚ•дҪҚзҡ„е…ғз»„(е®ҪеәҰ, й«ҳеәҰ)гҖӮ - Eryk Sun

1

那看起来会渲染文本,我想避免这种情况。 - speedplane

0
虽然仅仅为了检查字符串长度而添加一个新的第三方依赖库(如PIL)可能不是一个好主意,但是可以使用一个第三方库来生成可重用的近似函数。这样,解决方案既快速又无需依赖,同时也能保证准确性。
如果所涉及的字体是TrueType字体,您可以使用PIL.ImageFont.FreeTypeFont.getbbox。提示:最好先使用一个过大的字体计算宽度,因为getbbox返回整数。
使用自定义字体Inter-Regular的示例:
第一部分:生成宽度
import string

from PIL import ImageFont

WIDTH_DICT = dict()

supported_chars = [c for c in string.printable if not c.isspace() or c == ' ']


font_file_path = str(Path("~/.fonts/Inter-Regular.ttf").expanduser())
font = ImageFont.truetype(font_file_path, 15)

for char in supported_chars:
    left, _, right, _ = font.getbbox(char)
    width = right - left
    WIDTH_DICT[char] = width
    

AVERAGE_WIDTH = sum(WIDTH_DICT.values()) / len(WIDTH_DICT)

print(f'{WIDTH_DICT=}')
print(f'{AVERAGE_WIDTH=}')

输出
WIDTH_DICT={'0': 9, '1': 7, '2': 9, '3': 10, '4': 10, '5': 9, '6': 9, '7': 9, '8': 9, '9': 9, 'a': 8, 'b': 9, 'c': 8, 'd': 9, 'e': 9, 'f': 6, 'g': 9, 'h': 9, 'i': 4, 'j': 5, 'k': 8, 'l': 4, 'm': 13, 'n': 9, 'o': 9, 'p': 9, 'q': 9, 'r': 6, 's': 8, 't': 5, 'u': 9, 'v': 8, 'w': 12, 'x': 8, 'y': 8, 'z': 8, 'A': 10, 'B': 10, 'C': 11, 'D': 11, 'E': 9, 'F': 9, 'G': 11, 'H': 11, 'I': 4, 'J': 8, 'K': 10, 'L': 8, 'M': 13, 'N': 11, 'O': 11, 'P': 10, 'Q': 11, 'R': 10, 'S': 10, 'T': 10, 'U': 11, 'V': 10, 'W': 14, 'X': 10, 'Y': 10, 'Z': 9, '!': 4, '"': 6, '#': 10, '$': 10, '%': 12, '&': 10, "'": 3, '(': 5, ')': 5, '*': 8, '+': 10, ',': 4, '-': 7, '.': 4, '/': 5, ':': 4, ';': 4, '<': 10, '=': 10, '>': 10, '?': 8, '@': 14, '[': 5, '\\': 5, ']': 5, '^': 7, '_': 8, '`': 7, '{': 5, '|': 5, '}': 5, '~': 10, ' ': 4}
AVERAGE_WIDTH=8.31578947368421

第二部分:创建宽度计算器函数

现在只需将您的函数定义为:


WIDTH_DICT={'0': 9, '1': 7, '2': 9, '3': 10, '4': 10, '5': 9, '6': 9, '7': 9, '8': 9, '9': 9, 'a': 8, 'b': 9, 'c': 8, 'd': 9, 'e': 9, 'f': 6, 'g': 9, 'h': 9, 'i': 4, 'j': 5, 'k': 8, 'l': 4, 'm': 13, 'n': 9, 'o': 9, 'p': 9, 'q': 9, 'r': 6, 's': 8, 't': 5, 'u': 9, 'v': 8, 'w': 12, 'x': 8, 'y': 8, 'z': 8, 'A': 10, 'B': 10, 'C': 11, 'D': 11, 'E': 9, 'F': 9, 'G': 11, 'H': 11, 'I': 4, 'J': 8, 'K': 10, 'L': 8, 'M': 13, 'N': 11, 'O': 11, 'P': 10, 'Q': 11, 'R': 10, 'S': 10, 'T': 10, 'U': 11, 'V': 10, 'W': 14, 'X': 10, 'Y': 10, 'Z': 9, '!': 4, '"': 6, '#': 10, '$': 10, '%': 12, '&': 10, "'": 3, '(': 5, ')': 5, '*': 8, '+': 10, ',': 4, '-': 7, '.': 4, '/': 5, ':': 4, ';': 4, '<': 10, '=': 10, '>': 10, '?': 8, '@': 14, '[': 5, '\\': 5, ']': 5, '^': 7, '_': 8, '`': 7, '{': 5, '|': 5, '}': 5, '~': 10, ' ': 4}
AVERAGE_WIDTH=8.31578947368421

def get_width_inter_font_15(string:str) -> float:
  return sum(WIDTH_DICT.get(s, AVERAGE_WIDTH) for s in string)

这可以用作:
>>> get_width_inter_font_15("the quick brown fox jumps over the")
252
>>> get_width_inter_font_15("THE QUICK BROWN FOX JUMPS")
231

这个比例252/231是1.091,与我通过比较从字符串创建的图像的宽度时所看到的1.078(由Inkscape给出)非常接近。

enter image description here

由于从getbbox返回的宽度是整数,使用较大的字体大小会有所帮助。例如,使用字体大小为500:
>>> get_width_inter_font_500("THE QUICK BROWN FOX JUMPS")
7722
>>> get_width_inter_font_500("the quick brown fox jumps over the")
8358

这里的估计比例是1.082,仅比Inkscape所说的大0.4%。

关于速度

生成的函数get_width_inter_font_500比使用PIL快大约1000倍:

>>> %timeit get_width_inter_font_500("the quick brown fox jumps over the")
3.04 µs ± 10.9 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
>>> def pil_width(string):
      total = 0
      for char in string:
          left, _, right, _ = font.getbbox(char)
          width = right - left
          total += width
      return total
    
>>> pil_width("the quick brown fox jumps over the")
8358
>>> %timeit pil_width("the quick brown fox jumps over the")
3.6 ms ± 22.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接