3. 文本渲染入门:用 Pillow 绘制静态文本

这一章我们正式进入字体渲染的世界。说实话,很多人觉得在图片上写字很简单——不就是调个 API 吗?但等你真正上手,就会发现字体大小、颜色、位置、锯齿……每个细节都能让你折腾半天。我个人习惯先把最基础的东西跑通,再谈优化。

咱们用的工具是 Pillow,Python 生态里最成熟的图像处理库。它封装了底层 FreeType 引擎,让我们不用直接跟字形轮廓打交道。但理解它背后的原理,能帮你写出更稳的代码。

3.1 从一张空白画布开始

先创建一个图像对象,再在上面写字。这是最基础的流程:

from PIL import Image, ImageDraw, ImageFont

# 创建一张 400x200 的 RGB 图像,背景白色
img = Image.new('RGB', (400, 200), color='white')

# 获取绘图上下文
draw = ImageDraw.Draw(img)

# 加载字体(这里用系统默认字体,后面会讲自定义)
font = ImageFont.load_default()

# 在坐标 (20, 80) 处绘制文本
draw.text((20, 80), "Hello, 字体世界!", font=font, fill='black')

# 保存
img.save('hello_font.png')

这段代码跑完,你会得到一张白底黑字的图片。嗯,很简单对吧?但这里有个坑——load_default() 返回的是 Pillow 内置的位图字体,大小固定,不支持缩放。我在项目里第一次用的时候,发现文字小得几乎看不见,还以为是代码写错了。

注意: ImageFont.load_default() 只适合测试。生产环境一定要加载 .ttf 或 .otf 字体文件,否则文字大小和清晰度都不受控。

3.2 设置字体大小与颜色

要控制字体大小,必须加载 TrueType 字体。Pillow 通过 ImageFont.truetype() 来加载:

# 加载思源黑体,字号 48
font = ImageFont.truetype('SourceHanSansSC-Regular.otf', 48)

# 绘制红色文字
draw.text((20, 60), "字体大小与颜色", font=font, fill='#e74c3c')

颜色可以用多种方式指定:

格式 示例 说明
颜色名称 'red' Pillow 支持常见颜色名
RGB 元组 (231, 76, 60) 每个通道 0-255
十六进制 '#e74c3c' 带 # 号的 6 位 HEX
RGBA 元组 (231, 76, 60, 128) 带透明度,需配合 PNG

我个人习惯用十六进制,因为从设计稿里复制颜色值最方便。但要注意,如果图片是 JPEG 格式,RGBA 的透明度会被忽略——我曾经因为这个排查了半天。

3.3 字体加载的常见问题

你可能会遇到字体路径找不到的情况。我的建议是:

  • 把字体文件放在项目目录下的 fonts/ 文件夹里
  • 用相对路径引用,不要写死绝对路径
  • 跨平台时注意字体文件名大小写(Linux 区分大小写)
import os

font_path = os.path.join('fonts', 'SourceHanSansSC-Regular.otf')
font = ImageFont.truetype(font_path, 36)
小技巧: 如果你不确定系统里有哪些中文字体,可以用 fc-list :lang=zh 命令(Linux/macOS)查看。Windows 用户可以在 C:\Windows\Fonts 里找。

3.4 抗锯齿原理:为什么文字边缘是模糊的?

现在我们把字号调大,比如 120px,然后放大看文字边缘:

font = ImageFont.truetype('SourceHanSansSC-Regular.otf', 120)
draw.text((20, 40), "Aa", font=font, fill='black')
img.save('aa_demo.png')

放大后你会发现,文字边缘不是纯黑或纯白,而是深浅不一的灰色像素。这就是抗锯齿(Anti-Aliasing)。

为什么会这样?

想象一下,文字轮廓是一条平滑的曲线,但屏幕是由一个个方形像素组成的。如果每个像素要么全黑要么全白,曲线边缘就会出现明显的锯齿——像楼梯一样。抗锯齿的做法是:根据曲线穿过像素的面积比例,给像素分配一个中间灰度值。曲线穿过的面积越大,像素越黑;穿过的面积越小,像素越浅。

核心原理: 抗锯齿通过灰度过渡来模拟平滑曲线,牺牲一点锐利度,换来视觉上的平滑感。这是现代字体渲染的基石。

Pillow 默认开启抗锯齿。你不需要额外设置,但了解它有助于你理解为什么不同字号下文字清晰度不一样——小字号下抗锯齿效果不明显,因为像素太少,过渡区域被压缩了。

3.5 本章知识体系

下面这张图梳理了文本渲染的核心流程和关键概念:

文本渲染核心流程 创建画布 加载字体 设置参数 绘制文本 抗锯齿处理 输出图像 抗锯齿原理 文字轮廓是平滑曲线,像素是方形网格。 根据曲线穿过像素的面积比例分配灰度值, 让边缘产生视觉上的平滑过渡。

3.6 避坑指南

最后分享几个我踩过的坑:

  • 字体路径写死: 我曾经把字体路径写成 C:/Users/me/fonts/xxx.ttf,结果代码部署到服务器上直接崩溃。后来改用 os.path.join 加相对路径,再也没出过问题。
  • 字号单位混淆: Pillow 的 size 参数单位是像素(px),不是点(pt)。1pt ≈ 1.333px,如果你从设计稿里直接复制 12pt,画出来会偏小。
  • 忘记设置字体: 如果不调用 truetype(),默认字体不支持中文,画出来全是方框。我第一次做中文海报时就遇到了,还以为是 Pillow 的 bug。
最佳实践: 每次加载字体后,用 font.getbbox("测试") 获取文本的边界框,确认尺寸和位置符合预期。这能帮你提前发现 90% 的排版问题。