根据字符宽度将字符串分成多行 (python)
Breaking string into multiple lines according to character width (python)
我正在通过 PIL
在基础图像上绘制文本。要求之一是如果所有字符的组合宽度超过基本图像的宽度,则它会溢出到下一行。
目前我正在使用 textwrap.wrap(text, width=16)
来完成此操作。这里 width
定义了一行中容纳的字符数。现在文本可以是任何东西,因为它是用户生成的。所以问题是硬编码 width
不会考虑 width
由于字体类型、字体大小和字符选择引起的可变性。
我是什么意思?
好吧,假设我使用的是 DejaVuSans.ttf
,尺寸 14。W
的长度为 14,而 'i' 的长度为 4。对于宽度为 400 的基本图像,向上一行最多可以容纳 100 i
个字符。但只有 29 W
个字符。我需要制定一种更智能的换行方式,即当字符宽度之和超过基本图像宽度时字符串被断开的方式。
有人可以帮我制定这个吗?一个说明性的例子会很棒!
既然你知道每个字符的宽度,你应该把它变成一个字典,从中你得到宽度来计算字符串宽度:
char_widths = {
'a': 9,
'b': 11,
'c': 13,
# ...and so on
}
从这里您可以查找每个字母并使用该总和来检查您的宽度:
current_width = sum([char_widths[letter] for letter in word])
如果精度对您很重要,获得真实文本宽度的最佳方法是实际渲染它,因为字体指标并不总是线性的,关于字距调整或字体大小(参见 here)例如,因此不容易预测。
我们可以使用内部使用核心字体渲染方法的 ImageFont 方法 get_size
接近最佳断点(参见 PIL github)
def break_text(txt, font, max_width):
# We share the subset to remember the last finest guess over
# the text breakpoint and make it faster
subset = len(txt)
letter_size = None
text_size = len(txt)
while text_size > 0:
# Let's find the appropriate subset size
while True:
width, height = font.getsize(txt[:subset])
letter_size = width / subset
# min/max(..., subset +/- 1) are to avoid looping infinitely over a wrong value
if width < max_width - letter_size and text_size >= subset: # Too short
subset = max(int(max_width * subset / width), subset + 1)
elif width > max_width: # Too large
subset = min(int(max_width * subset / width), subset - 1)
else: # Subset fits, we exit
break
yield txt[:subset]
txt = txt[subset:]
text_size = len(txt)
并像这样使用它:
from PIL import Image
from PIL import ImageFont
img = Image.new('RGBA', (100, 100), (255,255,255,0))
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("Helvetica", 12)
text = "This is a sample text to break because it is too long for the image"
for i, line in enumerate(break_text(text, font, 100)):
draw.text((0, 16*i), line, (255,255,255), font=font)
最简单的解决方案可能就是使用等宽字体,其中每个字符的宽度都相同。显然你不能总是使用一个,但是当你可以的时候它会简单得多。
我正在通过 PIL
在基础图像上绘制文本。要求之一是如果所有字符的组合宽度超过基本图像的宽度,则它会溢出到下一行。
目前我正在使用 textwrap.wrap(text, width=16)
来完成此操作。这里 width
定义了一行中容纳的字符数。现在文本可以是任何东西,因为它是用户生成的。所以问题是硬编码 width
不会考虑 width
由于字体类型、字体大小和字符选择引起的可变性。
我是什么意思?
好吧,假设我使用的是 DejaVuSans.ttf
,尺寸 14。W
的长度为 14,而 'i' 的长度为 4。对于宽度为 400 的基本图像,向上一行最多可以容纳 100 i
个字符。但只有 29 W
个字符。我需要制定一种更智能的换行方式,即当字符宽度之和超过基本图像宽度时字符串被断开的方式。
有人可以帮我制定这个吗?一个说明性的例子会很棒!
既然你知道每个字符的宽度,你应该把它变成一个字典,从中你得到宽度来计算字符串宽度:
char_widths = {
'a': 9,
'b': 11,
'c': 13,
# ...and so on
}
从这里您可以查找每个字母并使用该总和来检查您的宽度:
current_width = sum([char_widths[letter] for letter in word])
如果精度对您很重要,获得真实文本宽度的最佳方法是实际渲染它,因为字体指标并不总是线性的,关于字距调整或字体大小(参见 here)例如,因此不容易预测。
我们可以使用内部使用核心字体渲染方法的 ImageFont 方法 get_size
接近最佳断点(参见 PIL github)
def break_text(txt, font, max_width):
# We share the subset to remember the last finest guess over
# the text breakpoint and make it faster
subset = len(txt)
letter_size = None
text_size = len(txt)
while text_size > 0:
# Let's find the appropriate subset size
while True:
width, height = font.getsize(txt[:subset])
letter_size = width / subset
# min/max(..., subset +/- 1) are to avoid looping infinitely over a wrong value
if width < max_width - letter_size and text_size >= subset: # Too short
subset = max(int(max_width * subset / width), subset + 1)
elif width > max_width: # Too large
subset = min(int(max_width * subset / width), subset - 1)
else: # Subset fits, we exit
break
yield txt[:subset]
txt = txt[subset:]
text_size = len(txt)
并像这样使用它:
from PIL import Image
from PIL import ImageFont
img = Image.new('RGBA', (100, 100), (255,255,255,0))
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("Helvetica", 12)
text = "This is a sample text to break because it is too long for the image"
for i, line in enumerate(break_text(text, font, 100)):
draw.text((0, 16*i), line, (255,255,255), font=font)
最简单的解决方案可能就是使用等宽字体,其中每个字符的宽度都相同。显然你不能总是使用一个,但是当你可以的时候它会简单得多。