如何在PIL中实现不规则多边形内的最大字号文本自动排版?
不规则多边形内自适应最大字号文本排版方案
需求概述
需要将任意长度文本排版到指定非矩形多边形内,实现类似Word文本绕排的效果,且字号尽可能大。现有PIL/textwrap仅支持矩形区域排版,缺乏不规则形状适配能力。
现有矩形适配代码
已实现顶部小矩形区域的最大字号适配:
img = Image.open("Frames/defaultframe.png") draw = ImageDraw.Draw(img) selected_size = 1 for size in range(1, 150): title = "SOME TITLE HERE" titleFont = ImageFont.FreeTypeFont('C:/Windows/Fonts/POORICH.TTF', size=size) w, h = titleFont.getsize(title) # older versions if w > 300 or h > 45: break selected_size = size titleFont = ImageFont.FreeTypeFont('C:/Windows/Fonts/POORICH.TTF', size=selected_size) draw.text((435, 135), title, fill='black', anchor='mm', font=titleFont)
目标多边形边界
polygon = [(130, 405),(642, 405),(625, 537),(580, 660),(285, 660),(200, 550)]
预期效果

解决方案实现
核心思路
- 射线法判断点是否在多边形内:无需复杂向量知识,实现简单可靠的区域校验
- 逐行文本适配:对每个候选字号,逐字符构建文本行,校验行边界是否在多边形内
- 二分法快速找最大字号:替代遍历法,大幅提升效率
完整代码
from PIL import Image, ImageDraw, ImageFont def point_in_polygon(point, polygon): """用射线法判断点是否在多边形内部""" x, y = point inside = False n = len(polygon) for i in range(n): p1x, p1y = polygon[i] p2x, p2y = polygon[(i+1)%n] # 检查点的y坐标是否在当前边的y区间内 if ((p1y > y) != (p2y > y)): # 计算射线与边的交点x坐标 x_intersect = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x if x <= x_intersect: inside = not inside return inside def fit_text_in_polygon(text, polygon, font_path, max_size=150): """寻找能完整放入多边形的最大字号,并返回排版后的文本行""" low = 1 high = max_size best_size = 1 best_lines = [] while low <= high: mid = (low + high) // 2 font = ImageFont.FreeTypeFont(font_path, size=mid) lines = [] current_line = "" valid = True line_height = font.getsize("A")[1] # 获取多边形的边界范围 min_x = min(p[0] for p in polygon) max_x = max(p[0] for p in polygon) min_y = min(p[1] for p in polygon) max_y = max(p[1] for p in polygon) for char in text: test_line = current_line + char line_width = font.getsize(test_line)[0] # 计算当前测试行的边界坐标 line_left = min_x line_top = min_y + len(lines) * line_height line_right = line_left + line_width line_bottom = line_top + line_height # 校验行的四个角是否都在多边形内,且不超出多边形垂直范围 if line_bottom > max_y or not ( point_in_polygon((line_left, line_top), polygon) and point_in_polygon((line_right, line_top), polygon) and point_in_polygon((line_left, line_bottom), polygon) and point_in_polygon((line_right, line_bottom), polygon) ): # 无法添加当前字符,换行 if not current_line: valid = False break lines.append(current_line) current_line = char # 检查新行的垂直位置是否超出多边形 if min_y + len(lines) * line_height + line_height > max_y: valid = False break else: current_line = test_line # 处理最后一行 if current_line and valid: lines.append(current_line) last_line_bottom = min_y + (len(lines)-1)*line_height + line_height if not point_in_polygon((min_x, last_line_bottom), polygon): valid = False if valid: best_size = mid best_lines = lines low = mid + 1 else: high = mid - 1 return best_size, best_lines # 主执行逻辑 if __name__ == "__main__": img = Image.open("Frames/defaultframe.png") draw = ImageDraw.Draw(img) target_polygon = [(130, 405),(642, 405),(625, 537),(580, 660),(285, 660),(200, 550)] input_text = "SOME TITLE HERE OR LONGER TEXT YOU NEED TO FIT" font_path = 'C:/Windows/Fonts/POORICH.TTF' best_size, text_lines = fit_text_in_polygon(input_text, target_polygon, font_path) font = ImageFont.FreeTypeFont(font_path, size=best_size) line_height = font.getsize("A")[1] min_x = min(p[0] for p in target_polygon) min_y = min(p[1] for p in target_polygon) # 绘制文本(左对齐,可自行修改为居中/右对齐) for idx, line in enumerate(text_lines): draw.text((min_x, min_y + idx*line_height), line, fill='black', font=font) # 可选:绘制多边形边界用于调试 draw.polygon(target_polygon, outline='red') img.save("text_in_polygon.png")
优化与扩展建议
- 文本对齐:若需居中对齐,可将每行绘制的x坐标改为
(max_x + min_x - font.getsize(line)[0])//2 - 精确字符校验:如果需要更严格的适配,可以遍历每个字符的 bounding box 所有点,确保完全在多边形内
- 字体选择:不同字体的字符宽度差异较大,可根据需求调整字体参数
- 性能优化:对于非常复杂的多边形,可预先缓存边界信息,减少重复计算
内容的提问来源于stack exchange,提问作者connor mcevoy
相关产品推荐
相关产品推荐

