如何让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’等选项的文本明显未正确居中,调整中心值等参数后仍无法完美居中,请求解决该问题。
解决方案
问题根源在于文本旋转后未重新计算偏移量,以及初始文本绘制的锚点与旋转后的边界变化不匹配。以下是修正后的代码和关键说明:
核心问题分析
- 仅用原始文本的
text_width/text_height计算偏移,但旋转后文本的实际边界会因expand=True扩大,原始中心点偏移不再适用。 - 文本绘制默认锚点为左上角,旋转后未基于新的文本图像中心重新对齐切片中心。
修正后的完整代码
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
相关产品推荐
相关产品推荐

