You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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)]

预期效果

文本适配不规则多边形示例


解决方案实现

核心思路

  1. 射线法判断点是否在多边形内:无需复杂向量知识,实现简单可靠的区域校验
  2. 逐行文本适配:对每个候选字号,逐字符构建文本行,校验行边界是否在多边形内
  3. 二分法快速找最大字号:替代遍历法,大幅提升效率

完整代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 10:40:10