如何在Pillow(PIL)中实现按单词而非字符的文本换行功能
问题描述
我正在使用Python的PIL库生成海报,需求为海报宽度固定,高度根据内容动态调整,实现代码如下:
from PIL import Image, ImageDraw, ImageFont def break_fix(text, width, font, draw): """ Fix line breaks in text. """ if not text: return lo = 0 hi = len(text) while lo < hi: mid = (lo + hi + 1) // 2 t = text[:mid] w, h = draw.textsize(t, font=font) if w <= width: lo = mid else: hi = mid - 1 t = text[:lo] w, h = draw.textsize(t, font=font) yield t, w, h yield from break_fix(text[lo:], width, font, draw) def fit_text(img, text, color, font, x_start_offset=0, x_end_offset=0, center=False): """ Fit text into container after applying line breaks. Returns the total height taken up by the text, which can be used to create containers of dynamic heights. """ width = img.size[0] - x_start_offset - x_end_offset draw = ImageDraw.Draw(img) pieces = list(break_fix(text, width, font, draw)) height = sum(p[2] for p in pieces) y = (img.size[1] - height) // 2 h_taken_by_text = 0 for t, w, h in pieces: if center: x = (img.size[0] - w) // 2 else: x = x_start_offset draw.text((x, y), t, font=font, fill=color) new_width, new_height = draw.textsize(t, font=font) y += h h_taken_by_text += new_height return h_taken_by_text def generate_text_section(width, text, color, font, x_start_offset, x_end_offset, v_spacing): """ Generates an image for a text section. """ # Calculate height using "fake" canvas img = Image.new('RGB', (width, 1), color='white') calc_height = fit_text( img, text.upper(), color, font, x_start_offset, x_end_offset, False ) # Create real canvas and fit text img = Image.new('RGB', (width, calc_height + v_spacing), color='white') fit_text( img, text.upper(), color, font, x_start_offset, x_end_offset, False ) return img test_img_1 = generate_text_section( width=400, text='But I must explain to you', color='black', font=ImageFont.truetype('fonts/RobotoMono-Bold.ttf', 14), x_start_offset=30, x_end_offset=30, v_spacing=30 ) test_img_1.show() test_img_1.save('1.png') test_img_2 = generate_text_section( width=400, text='But I must explain to you how all this mistaken idea of denouncing', color='black', font=ImageFont.truetype('fonts/RobotoMono-Bold.ttf', 14), x_start_offset=30, x_end_offset=30, v_spacing=30 ) test_img_2.show() test_img_2.save('2.png')
代码运行逻辑为:先调用fit_text()方法计算文本溢出换行后所需总高度,再用该高度创建可容纳文本的画布,目前运行效果基本符合预期,生成的两张示例图如下:
和
但现有实现是按字符进行溢出截断,第二张图中单词MISTAKEN被拆分到两行。我期望可以按单词边界换行,或至少在换行前添加连字符"-",请问该如何实现?
解决方案
你只需要修改核心的break_fix换行逻辑即可实现需求,以下提供两种可选实现方案,无需修改其他代码:
方案1:按单词边界换行(不拆分单词)
该方案优先在空格位置换行,保证所有单词完整展示,不会被拆分。仅当单个单词本身长度超过行宽时才会强制截断。
def break_fix(text, width, font, draw): """ 按单词边界换行,不拆分单词 """ if not text.strip(): return lo = 0 hi = len(text) # 二分查找当前行最大可容纳的字符长度 while lo < hi: mid = (lo + hi + 1) // 2 t = text[:mid] w, h = draw.textsize(t, font=font) if w <= width: lo = mid else: hi = mid - 1 # 向前查找最近的空格作为实际截断点 last_space_idx = text.rfind(' ', 0, lo) if last_space_idx != -1: lo = last_space_idx + 1 line_text = text[:lo].rstrip() # 处理单个单词超过行宽的极端情况 if not line_text: lo = 1 line_text = text[:lo] w, h = draw.textsize(line_text, font=font) yield line_text, w, h # 递归处理剩余文本,去掉行首多余空格 yield from break_fix(text[lo:].lstrip(), width, font, draw)
方案2:超长单词拆分时添加连字符
如果允许拆分长单词,可选择该方案,拆分时会在行尾自动添加连字符标识单词未结束。
def break_fix(text, width, font, draw): """ 优先按单词边界换行,超长单词拆分时添加连字符 """ if not text.strip(): return lo = 0 hi = len(text) # 提前计算连字符宽度预留空间 hyphen_w, _ = draw.textsize('-', font=font) # 二分查找当前行最大可容纳的字符长度 while lo < hi: mid = (lo + hi + 1) // 2 t = text[:mid] w, h = draw.textsize(t, font=font) # 预留连字符位置 available_width = width if ' ' in t[:mid] else width - hyphen_w if w <= available_width: lo = mid else: hi = mid - 1 # 向前查找最近的空格作为实际截断点 last_space_idx = text.rfind(' ', 0, lo) add_hyphen = False if last_space_idx != -1: lo = last_space_idx + 1 line_text = text[:lo].rstrip() else: # 无空格说明是单个超长单词,添加连字符 lo -= 1 line_text = text[:lo] + '-' add_hyphen = True w, h = draw.textsize(line_text, font=font) yield line_text, w, h # 处理剩余文本 next_text = text[lo:].lstrip() if not add_hyphen else text[lo:] yield from break_fix(next_text, width, font, draw)
注意事项
如果你使用的是Pillow 9.1.0及以上版本,draw.textsize方法已被官方废弃,可替换为draw.textbbox计算文本尺寸,示例如下:
# 替换原w, h = draw.textsize(t, font=font) bbox = draw.textbbox((0, 0), t, font=font) w = bbox[2] - bbox[0] h = bbox[3] - bbox[1]
内容的提问来源于stack exchange,提问作者darkhorse
相关产品推荐
相关产品推荐

