Python Pillow实现文字转便签图片:从基础到性能优化

小鹿同学
小鹿同学
发布时间:2026-10-13 | 6人浏览过
原创

本文详解使用Python Pillow库将文本转换为便签图片的全过程。重点解决长文本自动换行、背景图动态生成及字体渲染问题,并通过对比Python 2与3的性能差异,提出减小图片尺寸、调整字体及利用内网上传等优化方案,显著提升生成效率。

前言

近期遇到将文字转换为图片的需求,为避免下载第三方应用程序,我决定使用Python的Pillow库自行实现该功能,最终效果如图所示:


PIL库提供了PIL.ImageDraw.ImageDraw.text 接口,能够便捷地将文字绘制到图片上,基础使用示例如下:

from PIL import Image, ImageDraw, ImageFont
# get an image
base = Image.open('Pillow/Tests/images/hopper.png').convert('RGBA')

# make a blank image for the text, initialized to transparent text color
txt = Image.new('RGBA', base.size, (255,255,255,0))

# get a font
fnt = ImageFont.truetype('Pillow/Tests/fonts/FreeMono.ttf', 40)
# get a drawing context
d = ImageDraw.Draw(txt)

# draw text, half opacity
d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128))
# draw text, full opacity
d.text((10,60), "World", font=fnt, fill=(255,255,255,255))

out = Image.alpha_composite(base, txt)

out.show()

为何需要预先计算文字的宽高?直接将文字写入背景图不行吗?

Pillow PIL.ImageDraw.ImageDraw.text在绘制文字时默认依据换行符\n进行换行。若字符串过长,文字部分可能会超出背景图的宽度限制。因此,首要步骤是根据固定宽度计算文本所需的高度。

如图所示,文字转图片的处理流程主要分为三个步骤:

  • 计算文字宽高
  • 生成对应尺寸的背景图
  • 将文字绘制到图片上

计算文字宽高

在此场景中,背景图的宽度是固定的,因此无需计算文字总宽度。PIL.ImageDraw.ImageDraw.text 依据\n进行换行,我们只需在合适的文本位置插入\n即可实现自动换行。

首先想到的方法是使用textwrap模块,它可以通过调整换行符位置来格式化文本。然而,textwrap仅基于字符数量进行分割,而文本中的字符并非等宽,这可能导致格式化后的文字在图片上显示效果不佳,例如出现参差不齐的情况:


若采用这种方式,每次调整字体大小时,都需要重新计算每一行的长度,操作较为繁琐。

为确保每行宽度尽可能一致,这里使用PIL.ImageDraw.ImageDraw.textsize获取每个字符的宽高,随后按照约定的宽度将长文本分割为文本列表,最后将列表中的每行文字绘制到图片上。

def get_paragraph(text, note_width):
 # 把每段文字按约定宽度分隔成几行
 txt = Image.new('RGBA', (100, 100), (255, 255, 255, 0))
 # get a drawing context
 draw = ImageDraw.Draw(txt)
 paragraph, sum_width = '', 0
 line_numbers, line_height = 1, 0
 for char in text:
 w, h = draw.textsize(char, font)
 sum_width += w
 if sum_width > note_width:
  line_numbers += 1
  sum_width = 0
  paragraph += '\n'
 paragraph += char
 line_height = max(h, line_height)
 if not paragraph.endswith('\n'):
 paragraph += '\n'
 return paragraph, line_height, line_numbers


def split_text(text):
 # 将文本按规定宽度分组
 max_line_height, total_lines = 0, 0
 paragraphs = []
 for t in text.split('\n'):
 # 先按 \n 把文本分段
 paragraph, line_height, line_numbers = get_paragraph(t)
 max_line_height = max(line_height, max_line_height)
 total_lines += line_numbers
 paragraphs.append((paragraph, line_numbers))
 line_height = max_line_height
 total_height = total_lines * line_height
 # 这里返回分好的段,文本总高度以及行高
 return paragraphs, total_height, line_height

这是按字符宽度分隔文本并绘制到图片上的效果:


由于文本长度不固定,生成的文本高度也随之变化,因此我们需要动态生成背景图。

根据文本高度生成背景图

通过观察图片可知,头部和尾部是固定元素,变化的是中间的文本部分。背景图片的高度计算公式如下:

背景图片高度 = 头部高度 + 尾部高度 + 文本高度

具体实现代码如下:

NOTE_HEADER_IMG = path.normpath(path.join(
 path.dirname(__file__), 'note_header_660.png'))
NOTE_BODY_IMG = path.normpath(path.join(
 path.dirname(__file__), 'note_body_660.png'))
NOTE_FOOTER_IMG = path.normpath(path.join(
 path.dirname(__file__), 'note_footer_660.png'))
NOTE_WIDTH = 660
NOTE_TEXT_WIDTH = 460
body_height = NOTE_BODY_HEIGHT = 206
header_height = NOTE_HEADER_HEIGHT = 89
footer_height = NOTE_FOOTER_HEIGHT = 145
font = ImageFont.truetype(NOTE_OTF, 24)


def get_images(note_height):
 numbers = note_height // body_height + 1
 images = [(NOTE_HEADER_IMG, header_height)]
 images.extend([(NOTE_BODY_IMG, body_height)] * numbers)
 images.append((NOTE_FOOTER_IMG, footer_height))
 return images


def make_backgroud():
 # 将图片拼接到一起
 images = get_images()
 total_height = sum([height for _, height in images])
 # 最终拼接完成后的图片
 backgroud = Image.new('RGB', (body_width, total_height))
 left, right = 0, 0
 background_img = '/tmp/%s_backgroud.png' % total_height
 # 判断背景图是否存在
 if path.exists(background_img):
 return background_img
 for image_file, height in images:
 image = Image.open(image_file)
 # (0, left, self.body_width, right+height)
 # 分别为 左上角坐标 0, left
 # 右下角坐标 self.body_width, right+height
 backgroud.paste(image, (0, left, body_width, right+height))
 left += height # 从上往下拼接,左上角的纵坐标递增
 right += height # 左下角的纵坐标也递增
 backgroud.save(background_img, quality=85)
 return background_img

将文字写到图片

现在我们已经获得了背景图和分割好的文本,可以直接将文本绘制到图片上。

def draw_text(paragraphs, height):
 background_img = make_backgroud()
 note_img = Image.open(background_img).convert("RGBA")
 draw = ImageDraw.Draw(note_img)
 # 文字开始位置坐标,需要根据背景图的大小做调整
 x, y = 80, 100
 for paragraph, line_numbers in paragraphs:
 for line in paragraph.split('\n')[:-1]:
  draw.text((x, y), line, fill=(110, 99, 87), font=font)
  y += line_height
 # draw.text((x, y), paragraph, fill=(110, 99, 87), font=font)
 # y += self.line_height * line_numbers
 note_img.save(filename, "png", quality=1, optimize=True)
 return filename

完整代码请参阅 [ https://github.com/gusibi/momo/blob/master/momo/note.py ]

执行后的效果如图所示:


遇到的问题

为了方便使用,我将此功能封装为公众号的一个服务,但随即遇到了一个严重问题:生成速度太慢!

使用line_profiler进行分析后发现,大部分时间消耗在图片保存这一步骤上:

note_img.save(filename, "png", quality=1, optimize=True)

性能分析工具本身也会占用时间,测试完成后需关闭分析进程。

解决该问题的可能方法包括:

  • 减小背景图片的尺寸
  • 减小字体大小

经测试发现,将背景图宽度从990像素减小至660像素,并将字体大小从40px调整至24px,生成的图片体积缩小了近一倍,生成速度也比原来提升了约2/5。

在相同代码和文本条件下,使用Python 3仅需2.3秒,而Python 2却耗时5.3秒。此前从未在其他功能上观察到Python 2与Python 3之间存在如此巨大的性能差异。

具体差异可通过源码测试验证。

仍存在其他问题

优化图片生成速度后,发现在处理长文本时,公众号服务仍会因超时而报错。经检查,原因是图片上传至公众平台的速度过慢(服务器带宽仅为1M,无法避免)。

解决方法是将图片上传至腾讯云(文件上传使用内网带宽,不受限制),并返回图片URL。

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或工作具有一定的参考借鉴价值。如有疑问,欢迎留言交流,感谢大家对本站的支持。

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表