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% 的排版问题。