本教程详细讲解在 Python 中使用 Pillow 库加载 TTF/OTF 矢量字体并在图片上绘制中文文字的完整方法,包含环境准备、标准调用链路、坐标与 anchor 对齐机制、长文本换行与常见问题排查。
Pillow 实用开发
阅读时长:约 8 分钟
环境要求:Python 3.8+ / Pillow 9.0+
在 Python 中使用 Pillow 库在图片上添加文字时,默认字体仅支持微缩英文字符且无法调整大小。要绘制清晰、抗锯齿且包含中文的文字,核心路径是通过 PIL.ImageFont.truetype(font, size) 加载外部 TTF 或 OTF 矢量字体文件,再结合 ImageDraw.Draw.text() 进行栅格化渲染。本文提供可立即执行的完整步骤、坐标度量技巧及常见排坑说明。
一、准备工作与环境依赖
Pillow 是 Python 原生图像处理库 PIL 的现代分支。处理文字需要依赖系统已构建好的 FreeType 字体引擎支持。通常通过标准包管理器安装的 Pillow 预编译轮子(Wheels)已默认静态编译包含 FreeType 模块,无需手动安装底层 C 库。
终端执行命令
BASH
pip install Pillow
执行验证命令确认模块安装正常:
环境检查示例
PYTHON
import PIL
from PIL import Image, ImageDraw, ImageFont
print("Pillow 版本:", PIL.__version__)
准备一个可用的中文字体文件(.ttf 或 .otf)。如果是在 Windows 环境下,系统目录 C:/Windows/Fonts/msyh.ttc(微软雅黑)或 simhei.ttf(黑体)可直接作为字体路径;在 Linux 或跨平台服务器端,建议将字体文件(例如开源免费商用的“阿里巴巴普惠体”或“思源黑体”)与代码存放在同一工程目录中。
二、核心操作流程与标准代码
向图片写入文字的标准生命周期包含 5 个明确步骤:建立画布对象、载入 TTF 字体规格、实例化绘图图层、测定排版边界、执行文字像素栅格化并写出。
图 1:Pillow 加载 TTF 并在图片写字的 5 步标准操作流水线
以下为完整的端到端示范脚本。脚本将在内存中新建一张暖灰底色的画布,加载指定 TTF 字体并写入标题及正文:
完整示例脚本:render_text.py
PYTHON
import os
from PIL import Image, ImageDraw, ImageFont
# 1. 创建画布:新建一张 800x400、RGB 色彩模式、底色为浅暖灰的图像
width, height = 800, 400
canvas_color = (245, 244, 240) # #F5F4F0
image = Image.new("RGB", (width, height), color=canvas_color)
# 2. 绑定绘图句柄:所有在 image 上的绘图与写字均通过 draw 操作完成
draw = ImageDraw.Draw(image)
# 3. 加载外部 TTF 字体
# 这里指定字体文件路径;若工程目录下有名为 "AlibabaPuHuiTi.ttf" 的字体即可直接填写
font_file = "AlibabaPuHuiTi.ttf"
# 防御性判断:若本地无此测试文件,自动回退到系统常见宋体/黑体,确保能够直接运行
if not os.path.exists(font_file):
# Linux 常见路径: /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
# macOS 常见路径: /System/Library/Fonts/PingFang.ttc
# Windows 常见路径: C:/Windows/Fonts/simhei.ttf
candidate_fonts = [
"/System/Library/Fonts/PingFang.ttc",
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/simhei.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
]
for candidate in candidate_fonts:
if os.path.exists(candidate):
font_file = candidate
break
title_font = ImageFont.truetype(font_file, size=32)
sub_font = ImageFont.truetype(font_file, size=18)
# 4. 写入文字:指定左上角绝对坐标、文本内容、字体对象与 RGBA/RGB 颜色值
title_text = "Python Pillow 字体绘制实践"
sub_text = "基于 TrueType 矢量字体引擎实现抗锯齿文字排版"
# 绘制标题(深灰墨色)
draw.text((60, 120), title_text, font=title_font, fill=(45, 55, 72))
# 绘制副标题(灰蓝色)
draw.text((60, 180), sub_text, font=sub_font, fill=(104, 125, 141))
# 5. 保存生成的图像
output_path = "output_pillow_text.png"
image.save(output_path, quality=95)
print(f"图像生成完毕,保存至: {output_path}")
执行输出示例
图像生成完毕,保存至: output_pillow_text.png
打开生成的图片后,即可看到清晰无锯齿的中文文字整齐排列在设定坐标处。
三、排版控制:文本边界测距与 Anchor 锚点对齐
在实际生产场景中(如自动生成宣发海报、照片打水印或生成证书),文字往往需要居中、居右或在限定宽度内排版,直接写死绝对坐标会导致不同长度的文字位置偏移。
图 2:Pillow 文本度量边界线与 Anchor 锚点对齐参数解析
1. 使用 textbbox 准确计算文字宽高
在较早的 Pillow 版本中,开发者习惯使用 draw.textsize() 或 font.getsize()。从 Pillow 10.0.0 起,这些旧接口已被正式移除,统一采用 draw.textbbox()。该方法接收起始参考坐标、文本及字体对象,返回一个四元组 (left, top, right, bottom):
文字尺寸精确测量
PYTHON
# 测量以 (0, 0) 为原点时的包围盒
bbox = draw.textbbox((0, 0), title_text, font=title_font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 计算水平居中时所需的 X 坐标(画布宽度为 width)
center_x = (width - text_width) // 2
draw.text((center_x, 80), title_text, font=title_font, fill=(45, 55, 72))
2. 使用 anchor 参数一行代码实现对齐
Pillow 内部提供了 anchor(锚点)机制。参数由两个英文字母组成:第一个代表水平对齐(l=左, m=居中, r=右),第二个代表垂直对齐(a=升部顶端, m=垂直居中, b=降部底端, s=基线)。
若要将文字精准放置在画布的正中心点 (width // 2, height // 2),无需人工测算宽高做减法,只需指定 anchor="mm":
居中锚点极简调用
PYTHON
# 直接以 (400, 200) 为几何中心点居中渲染
draw.text((width // 2, height // 2), "全图正中心对齐文字", font=title_font, fill=(135, 155, 136), anchor="mm")
四、方案对比:ImageFont 两种载入模式的关键差异
初学者在使用 Pillow 时常疑惑为什么存在 load_default() 和 truetype() 两种获取字体的方式。下表对比了两者的本质技术特征与能力范围:
图 3:load_default() 简易位图与 truetype() 矢量字体的渲染差异
对比维度
ImageFont.load_default()
ImageFont.truetype(font, size)
字符编码覆盖
仅包含基础 ASCII 字符,中文全部输出为空格或乱码问号
完整支持 Unicode/CJK 汉字、标点与日韩等多国语言
字号无级缩放
固定位图点阵尺寸(约 10px),无法任意放大或缩小
无级矢量缩放,从 8px 到 1000px 均保持矢量平滑
边缘渲染质量
点阵硬边缘,带有明显颗粒感和锯齿
基于 FreeType 子像素平滑渲染,抗锯齿过渡柔和自然
外部资源依赖
无任何外部依赖,Pillow 内部硬编码集成
必须指定系统或本地存在的物理 TTF/OTF 文件路径
差异的关键在于:load_default() 的设计定位是无环境依赖下的代码测试占位,而任何包含非英文字符或具有视觉要求的图形合成任务,都必须使用 truetype()。
五、生产实践与长文本自动折行
Pillow 的底层渲染引擎将换行符(
)视作换行依据,但它不会 自动根据图片宽度进行段落自动换行。如果传入的一行文本过长,超出图片边界的部分将直接被截断。
在批量处理业务文案时,需要编写一个轻量级的文本切分函数,利用 font.getlength() 逐字计算宽度,在达到最大宽度上限时主动切行:
多行自动折行与间距控制
PYTHON
def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int) -> str:
lines = []
current_line = ""
for char in text:
# 测试追加该字符后的总宽度
test_line = current_line + char
if font.getlength(test_line) <= max_width:
current_line = test_line
else:
lines.append(current_line)
current_line = char
if current_line:
lines.append(current_line)
return "\n".join(lines)
# 使用示例
long_paragraph = "Python 是一门语法优美、易于阅读的脚本语言。结合 Pillow 库,我们可以非常方便地构建出自动化报表封面与内容合成服务。"
wrapped_content = wrap_text(long_paragraph, sub_font, max_width=420)
# 使用 multiline_text 写入,并设定行间距 spacing=8
draw.multiline_text((60, 240), wrapped_content, font=sub_font, fill=(74, 85, 104), spacing=8)
六、高频报错与排坑清单
加载 TTF 字体操作涉及操作系统文件系统访问、字体字形表解析与图像混合,在工程化部署中容易遇到以下典型问题:
图 4:Pillow 字体加载核心排坑要点与避雷指南
报错 OSError: cannot open resource:
原因:Pillow 无法在指定路径找到该字体文件。请避免使用不稳定的相对路径(如直接写 "font.ttf"),推荐使用 os.path.join(os.path.dirname(__file__), "fonts/myfont.ttf") 构建绝对路径,并在载入前通过 os.path.isfile() 确认文件存在。
中文字符显示为白色方块(“豆腐块” □):
原因:加载的字体文件中缺少中文字符的字形轮廓(Glyph)。例如西方系统自带的 arial.ttf 或 times.ttf 仅包含西文字符,遇到汉字时没有对应点阵或贝塞尔数据,FreeType 只能以未定义方块代替。解决办法是明确引入涵盖 GB2312/GBK 或通用中文字库的字体文件。
批量产图性能迟缓:
原因:在批量循环(例如处理上万张图片)中,每次迭代都重复执行 ImageFont.truetype()。解析 TTF 二进制字形表存在磁盘 I/O 与内存解析成本,应在循环体外部预先完成字体实例加载,循环内部复用同一 font 实例。