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

如何让PIL绘制的转盘切片内文本精准居中?

转盘GIF文本居中问题

我正在制作可灵活调整选项数量的转盘GIF,尝试让每个饼状切片内的文本完美居中,但部分文本(如eat、dance等)无法正确居中。以下是生成转盘GIF的代码片段:

import math
import random

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

DIMENSIONS = (500, 500)
CENTER = (250, 250)
RADIUS = 200
NUM_FRAMES = 50
MASK_IMG = Image.open('mask.png')
SPINNER_CENTER_IMG = Image.open('spinner_center.png')
TRIANGLE_IMG = Image.open('triangle.png')


class SpinnerGifMaker:

    def __init__(self, options):
        self.options = options
        colors = []
        for _ in enumerate(options):
            r = random.randint(0, 255)
            g = random.randint(0, 255)
            b = random.randint(0, 255)
            a = 128
            color = (r, g, b, a)
            colors.append(color)
        self.colors = colors

        frame_list = []
        for i in range(NUM_FRAMES):
            angle = i * -50
            frame = self.getSpinnerFrame(angle)
            frame_list.append(frame)
            frame_list[0].save('spinner.gif', format='GIF', append_images=frame_list[1:], save_all=True, duration=100,
                               loop=0)

    def getSpinnerFrame(self, angle):

        spinner_img = Image.new('RGB', DIMENSIONS, color=(255, 255, 255, 0))
        spinner_draw = ImageDraw.Draw(spinner_img, 'RGBA')
        num_sections = len(self.options)
        for i, option in enumerate(self.options):
            start_angle = i * (360 / num_sections)
            end_angle = (i + 1) * (360 / num_sections)
            color = self.colors[i]
            spinner_draw.pieslice(xy=((CENTER[0] - RADIUS, CENTER[1] - RADIUS), (CENTER[0] + RADIUS, CENTER[1] +
                                                                                 RADIUS)),
                                  start=start_angle,
                                  end=end_angle, fill=color, outline='black')

            font = ImageFont.truetype("arial.ttf", 30)
            _, _, text_width, text_height = spinner_draw.textbbox((0, 0), option, font=font)
            section_center_angle = (start_angle + end_angle) / 2
            section_center_x = CENTER[0] + RADIUS * 0.6 * math.cos(section_center_angle * math.pi / 180)
            section_center_y = CENTER[1] + RADIUS * 0.6 * math.sin(section_center_angle * math.pi / 180)
            text_center_x = section_center_x - text_width / 2
            text_center_y = section_center_y - text_height / 2
            text_angle = 180 - section_center_angle
            print(i, option, text_angle, text_width, text_height)
            text_img = Image.new('RGBA', (text_width, text_height), color=(255, 255, 255, 0))
            text_draw = ImageDraw.Draw(text_img)
            text_draw.text((0, 0), option, fill=(0, 0, 0), font=font)
            text_img = text_img.rotate(text_angle, expand=True)
            spinner_img.paste(text_img, (int(text_center_x), int(text_center_y)), text_img)

        spinner_img = spinner_img.rotate(angle, center=CENTER)
        spinner_img.putalpha(MASK_IMG)
        spinner_img.paste(SPINNER_CENTER_IMG, mask=SPINNER_CENTER_IMG)
        spinner_img.paste(TRIANGLE_IMG, mask=TRIANGLE_IMG)
        return spinner_img


SpinnerGifMaker(["hi", "play", "sleep", "run", "dance", "eat", "fly", "study"])

截图显示‘eat’、‘dance’、‘run’和‘study’等选项的文本明显未正确居中,调整中心值等参数后仍无法完美居中,请求解决该问题。


解决方案

问题根源在于文本旋转后未重新计算偏移量,以及初始文本绘制的锚点与旋转后的边界变化不匹配。以下是修正后的代码和关键说明:

核心问题分析

  1. 仅用原始文本的text_width/text_height计算偏移,但旋转后文本的实际边界会因expand=True扩大,原始中心点偏移不再适用。
  2. 文本绘制默认锚点为左上角,旋转后未基于新的文本图像中心重新对齐切片中心。

修正后的完整代码

import math
import random

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

DIMENSIONS = (500, 500)
CENTER = (250, 250)
RADIUS = 200
NUM_FRAMES = 50
MASK_IMG = Image.open('mask.png')
SPINNER_CENTER_IMG = Image.open('spinner_center.png')
TRIANGLE_IMG = Image.open('triangle.png')


class SpinnerGifMaker:

    def __init__(self, options):
        self.options = options
        colors = []
        for _ in enumerate(options):
            r = random.randint(0, 255)
            g = random.randint(0, 255)
            b = random.randint(0, 255)
            a = 128
            color = (r, g, b, a)
            colors.append(color)
        self.colors = colors

        frame_list = []
        for i in range(NUM_FRAMES):
            angle = i * -50
            frame = self.getSpinnerFrame(angle)
            frame_list.append(frame)
            frame_list[0].save('spinner.gif', format='GIF', append_images=frame_list[1:], save_all=True, duration=100,
                               loop=0)

    def getSpinnerFrame(self, angle):
        spinner_img = Image.new('RGB', DIMENSIONS, color=(255, 255, 255, 0))
        spinner_draw = ImageDraw.Draw(spinner_img, 'RGBA')
        num_sections = len(self.options)
        for i, option in enumerate(self.options):
            start_angle = i * (360 / num_sections)
            end_angle = (i + 1) * (360 / num_sections)
            color = self.colors[i]
            spinner_draw.pieslice(xy=((CENTER[0] - RADIUS, CENTER[1] - RADIUS), (CENTER[0] + RADIUS, CENTER[1] + RADIUS)),
                                  start=start_angle,
                                  end=end_angle, fill=color, outline='black')

            font = ImageFont.truetype("arial.ttf", 30)
            # 获取文本完整边界(包含字体自带的留白)
            text_bbox = spinner_draw.textbbox((0, 0), option, font=font)
            text_width = text_bbox[2] - text_bbox[0]
            text_height = text_bbox[3] - text_bbox[1]
            # 计算文本相对于bbox左上角的偏移,消除字体留白影响
            text_offset_x = -text_bbox[0]
            text_offset_y = -text_bbox[1]

            section_center_angle = (start_angle + end_angle) / 2
            # 计算切片中心坐标
            section_center_x = CENTER[0] + RADIUS * 0.6 * math.cos(section_center_angle * math.pi / 180)
            section_center_y = CENTER[1] + RADIUS * 0.6 * math.sin(section_center_angle * math.pi / 180)

            # 创建初始文本图像
            text_img = Image.new('RGBA', (text_width, text_height), color=(255, 255, 255, 0))
            text_draw = ImageDraw.Draw(text_img)
            # 基于偏移绘制文本,确保文本在初始图像中居中
            text_draw.text((text_offset_x, text_offset_y), option, fill=(0, 0, 0), font=font)

            text_angle = 180 - section_center_angle
            # 旋转文本并自动扩展尺寸以容纳内容
            rotated_text_img = text_img.rotate(text_angle, expand=True)
            # 计算旋转后文本的中心偏移
            rotated_center_x = rotated_text_img.width // 2
            rotated_center_y = rotated_text_img.height // 2
            # 计算最终粘贴位置:切片中心减去旋转后文本的中心偏移
            paste_x = int(section_center_x - rotated_center_x)
            paste_y = int(section_center_y - rotated_center_y)

            spinner_img.paste(rotated_text_img, (paste_x, paste_y), rotated_text_img)

        spinner_img = spinner_img.rotate(angle, center=CENTER)
        spinner_img.putalpha(MASK_IMG)
        spinner_img.paste(SPINNER_CENTER_IMG, mask=SPINNER_CENTER_IMG)
        spinner_img.paste(TRIANGLE_IMG, mask=TRIANGLE_IMG)
        return spinner_img


SpinnerGifMaker(["hi", "play", "sleep", "run", "dance", "eat", "fly", "study"])

关键改进点

  • 精准获取文本边界:利用textbbox的四个坐标值计算文本实际占用空间,消除字体自带留白的影响。
  • 旋转后重新对齐中心:文本旋转后,基于新图像的尺寸计算中心点偏移,确保旋转后的文本与切片中心完全对齐。
  • 修正文本绘制锚点:通过text_offset_x/text_offset_y调整文本绘制位置,保证初始文本在图像内居中,旋转后整体位置更准确。

内容的提问来源于stack exchange,提问作者Linus Tan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 15:10:06