用 Python 和 Pillow 生成自己的书法春联:从字体加载到排版合成
2026/9/12 0:05:19 网站建设 项目流程

简介:面向想在春节前后用Python程序创作年味内容的初学者,这份资源演示了如何借助操作系统自带的毛笔字体自动生成春联。作者在Windows的C:\Windows\Fonts字体目录中定位到"华文隶书"(STLITI.TTF),脚本据此完成字体加载、春联文字绘制与图片输出;这种方案无需额外安装字体,适合想接触PIL/Pillow图像处理、系统字体调用以及简单自动化出图的后端开发者。压缩包总共只有2个文件:1个py主脚本负责绘制春联文字,1张jpeg背景图提供年味底纹,整包153KB,结构清晰、便于直接阅读与二次修改。脚本保留了字体路径、字号、输出尺寸等核心设置,改一改文字内容就能得到不同春联,背景图也可按需替换;对理解中文字体在Python绘图中的调用逻辑尤其有帮助,也适合扩展成批量生成春联的小工具。目前已有1074人学习下载,是春节小工具、个性贺卡或教学演示中的一个轻量范例。

1. 用 Python 写春联,先解决“字从哪来”

春节前想给家里写副春联,手边没有毛笔,又不想用在线模板里千篇一律的楷体,最简单的方式是让 Python 直接调用系统字体库,把“毛笔字”写到图片上。操作系统自带的矢量字库里,有一部分是可以当毛笔字库用的,比如 Windows 的华文隶书,文件名叫 STLITI.TTF,在 C:\Windows\Fonts 目录下就能找到。用 Pillow 加载它,配合背景图和坐标计算,就能生成一副像模像样的春联图片。这种思路适合 python 入门阶段的读者练手,也适合后端工程师快速实现文字合成图片的功能;核心就两个点:字体加载和排版坐标。下面从字体文件开始拆。

2. 字体加载:从 STLITI.TTF 到 Pillow 的 Font 对象

2.1 为什么系统矢量字库可以直接作为毛笔字库

TrueType 和 OpenType 字体保存的是字形轮廓的矢量数据,缩放时由渲染引擎重新计算边界,所以放大到春联尺寸也不会出现明显的锯齿。华文隶书属于 TrueType 字体,笔画带有明显的波磔和蚕头燕尾特征,用在春联场景里比黑体和宋体更有手写感。不同系统的字体路径不一样:Windows 集中在 C:\Windows\Fonts,macOS 在 /System/Library/Fonts 和 /Library/Fonts,Linux 一般在 /usr/share/fonts。如果不确定系统里有哪些中文字体,在 Linux 上执行 fc-list :lang=zh 可以直接列出;Windows 上打开字体目录按“隶书”筛选更快。

2.2 test4.py 里的字体加载写法与参数含义

这个项目里真正起作用的脚本是 test4.py,它开头做的事情就是找到字体文件并加载。标准写法如下:

from PIL import Image, ImageDraw, ImageFont # Windows 下直接指向系统字体目录,注意文件名要和资源管理器里看到的一致 font_path = r"C:\Windows\Fonts\STLITI.TTF" # size=120 表示字体的逻辑尺寸,单位是像素 font = ImageFont.truetype(font_path, size=120) # 验证字体是否加载成功:getname 返回 (家族名, 样式名) print(font.getname()) # 期望输出 ('STLITI', 'Regular') print(font.getmetrics()) # 返回 (ascent, descent),用于垂直定位

在运行这段代码之前,需要先确认当前 python 环境里安装了 Pillow。命令是 pip install pillow。如果你刚完成 python 安装,还没有做过虚拟环境配置,建议直接用系统解释器安装,避免出现 python 环境配置混乱的问题;在 pycharm 配置 python 环境时,要保证解释器路径和 pip 所属环境一致,否则会报 ModuleNotFoundError: No module named 'PIL'。

这段代码里ImageFont.truetype的第一个参数是字体文件路径,第二个参数是字体大小。要注意size并不是最终渲染的字号上限,它只决定了字体轮廓被缩放到的标称尺寸;getmetrics()返回的 ascent 和 descent 加起来就是一行文字从基线到顶部的总高度,比getbbox的结果更适合用于行高计算。很多人第一次写时会把 font.getname() 打印出来的结果当成字体文件名,其实那只是字体的内部元数据,和磁盘文件名没有直接关系。

2.3 字体加载失败的常见原因与排查表

字体加载看似简单,实际报错大多集中在路径和格式上。以 Windows 为例,C:\Windows\Fonts 里有大量字体文件,但某些字体在资源管理器里显示为“华文隶书”,实际文件名可能是 STLITI.TTF 或 STLITI.TTC,前者是 TrueType,后者是 TrueType Collection。Pillow 支持 TTC,但加载 TTC 时需要通过 index 参数指定是第几个字体,例如ImageFont.truetype("STLITI.TTC", 120, index=0)。下表是几个最常见的异常场景。

异常现象直接原因处理方式
OSError: cannot open resource路径写错或文件被占用检查实际文件名,用 os.path.exists 先确认
OSError: invalid font header文件不是合法 TTF/OTF/TTC换字体文件,或确认下载的文件没有损坏
UnicodeEncodeError字体不含某些字或符号换字库,或把文字转为繁体再渲染
每次运行路径都变用相对路径或临时目录把字体复制到项目 fonts 目录,用file定位

注意:Pillow 在 Linux 下对字体路径大小写敏感,在 Windows 下不敏感。把字体文件放进项目目录,用绝对路径定位,能避免换机器后路径失效。

如果出现 cannot open resource,常见做法是先写一段检测逻辑:

import os font_path = r"C:\Windows\Fonts\STLITI.TTF" if not os.path.exists(font_path): raise FileNotFoundError(f"字体不存在: {font_path}")

这段代码的作用是提前暴露路径问题,而不是等到 ImageFont.truetype 内部抛 OSError。参数说明:只需注意 os.path.exists 接受字符串路径;如果将来要做跨平台,建议用 pathlib.Path(font_path).exists(),返回布尔值,语义更清晰。

3. 排版坐标:文本测量、anchor 锚点与背景合成

3.1 文本测量:getbbox 与 textlength 的差异

写春联时最常做的一件事是让文字居中。Pillow 的 ImageDraw 提供了两种测量工具,很多 python 教程里还在讲旧版的 textsize,实际上它已经弃用,现在建议用 textbbox 和 textlength。下面这段代码演示两者的差别。

from PIL import Image, ImageDraw, ImageFont font_path = r"C:\Windows\Fonts\STLITI.TTF" font = ImageFont.truetype(font_path, size=120) img = Image.new("RGB", (600, 300), "white") draw = ImageDraw.Draw(img) text = "迎新春" # 测量实际绘制内容的包围盒,返回 (left, top, right, bottom) bbox = draw.textbbox((0, 0), text, font=font) print("bbox:", bbox) # textlength 只返回水平方向的长度 width = draw.textlength(text, font=font) print("textlength:", width) # 水平居中用 textlength 的宽度 x = (img.width - width) / 2 # 垂直居中用 bbox 的高度 y = (img.height - (bbox[3] - bbox[1])) / 2 draw.text((x, y), text, font=font, fill="black") img.save("center_test.png")

textlength 返回的是浮点数,考虑到了字距、字重和字体渲染的细微差异;textbbox 返回四元组,是精确定位文字矩形区域的基础。如果你的需求仅仅是水平居中,用 textlength 就够了;如果要同时控制文字在背景中的上下位置,必须用 textbbox 的高度。textbbox 的 left 和 top 对某些字体不是 0,因为字体的内含边距不同,直接取 textbbox[2] 作为宽度容易偏大,通常用right - left计算。

3.2 背景图合成:bg.jpeg 的打开与居中

项目里附带了一张 bg.jpeg,说明春联不一定要纯色背景,可以基于真实纸张或红纸纹理生成。常见做法是先把背景图转成 RGBA,再在上面叠加文字层,最后用 alpha_composite 合成。

from PIL import Image, ImageDraw, ImageFont font = ImageFont.truetype(r"C:\Windows\Fonts\STLITI.TTF", size=120) bg = Image.open("bg.jpeg").convert("RGBA") # 文字层单独一张透明画布,尺寸和背景图一致 canvas = Image.new("RGBA", bg.size, (255, 255, 255, 0)) draw = ImageDraw.Draw(canvas) # anchor="mm" 表示 (x, y) 是文字矩形的中心点 x = bg.width // 2 y = bg.height // 2 draw.text((x, y), "迎新春", font=font, fill=(255, 0, 0, 255), anchor="mm") result = Image.alpha_composite(bg, canvas) result.convert("RGB").save("spring_couplet.png")

这里先说明convert("RGBA")的意义:背景图如果是 JPG,本身没有 alpha 通道,转成 RGBA 后统一像素格式。anchor="mm"让文字的几何中心对准 (x, y),省去了自己计算 left/top 的步骤。参数 fill 里的第四个值是透明度,255 表示完全不透明;如果要做半透明效果,可以改成 128,但春联场景通常不需要。

需要留意的是 alpha_composite 要求两张图的尺寸完全一致,如果背景图是旋转过的,文字也会跟着旋转。test4.py 里通常不会单独旋转文字,而是通过调整背景图的宽高比来适配横排或竖排。

3.3 画布尺寸变化时如何缩放字体

同一个脚本如果被用来生成不同尺寸的春联,字体不能写死。我一般会用一个循环递减的方式,从基准字号开始,逐步缩小到能放下文字为止。

def fit_font_size(draw, text, font_path, max_width, max_height, base_size=120): size = base_size font = ImageFont.truetype(font_path, size) while size > 10: bbox = draw.textbbox((0, 0), text, font=font) w = bbox[2] - bbox[0] h = bbox[3] - bbox[1] if w <= max_width and h <= max_height: return font, size size -= 2 font = ImageFont.truetype(font_path, size) return font, size

循环递减会多次调用 truetype,这个操作在字体文件较大时有一点开销,但春联文字一般不超过 10 个字,影响可以忽略。如果追求效率,可以先按比例估算字号:estimated_size = int(base_size * min(max_width / w, max_height / h)),然后再微调。参数上 max_width 和 max_height 是文字允许占用的最大矩形,size 步长设为 2 是为了避免字号变化太慢;如果把步长改成 1,结果更精细但循环次数会增加一倍。

注意draw.textbbox的坐标是基于 (0, 0) 原点计算的,如果你的画布本身有边距,要在判断时把 margin 减掉。这一段也可以和 2.2 的 getmetrics 对照看:getmetrics 给的是字体整体度量,textbbox 给的是具体文本的包围盒,两者不能互相替代。

4. 上下联批量生成:参数表、竖排实现与导出

4.1 上下联和横批的参数表

在实际写春联脚本时,最好把参数集中放置,方便以后换成别的字体或背景。把 test4.py 里的散落参数整理成一张表,对应关系如下。

参数名含义示例值
left_text第一句(上联内容)天增岁月人增寿
right_text第二句(下联内容)春满乾坤福满门
horizontal_text横批文本万象更新
font_path毛笔字库路径C:\Windows\Fonts\STLITI.TTF
bg_path背景图片路径bg.jpeg
sidebar_font_size对联正文字号90
header_font_size横批字号110
margin画布边距60

这里的字号建议按背景图高度比例计算,例如int(bg.height * 0.15),而不是写死,这样换一张竖版背景图后对联不会溢出。上联和下联文字较长时,需要把字号调小,否则会超出画布边界;横批文字短,字号可以稍大。参数表里的 margin 仅用于纯色背景的边界控制,如果背景是整张图,还需要配合 3.3 的 fit_font_size。

4.2 自动排版一副完整春联的代码

一副完整春联包括上联、下联和横批。横批一般放在顶部中间,两个竖条分别放在画布左右两侧。这里涉及到竖排问题,Pillow 没有内置竖排模式,常见做法是把文字拆成单个字后再渲染,或用换行符实现纵向排列。下面这段代码用换行符实现竖排。

from PIL import Image, ImageDraw, ImageFont FONT_PATH = r"C:\Windows\Fonts\STLITI.TTF" BG_PATH = "bg.jpeg" def vertical_text(text: str) -> str: # 每个字之间插入换行,让 draw.text 自动换行 return "\n".join(text) def render_task(left, right, horizontal, output="couplets.png"): bg = Image.open(BG_PATH).convert("RGBA") W, H = bg.size font_h = ImageFont.truetype(FONT_PATH, int(H * 0.12)) font_v = ImageFont.truetype(FONT_PATH, int(H * 0.09)) draw = ImageDraw.Draw(bg) # 横批,anchor="mm" 表示以该坐标为中心 draw.text((W // 2, int(H * 0.12)), horizontal, font=font_h, fill=(255, 0, 0, 255), anchor="mm") # 第一个文本竖排放在画布左侧,第二个文本放在右侧 draw.text((int(W * 0.22), int(H * 0.5)), vertical_text(left), font=font_v, fill=(0, 0, 0, 255), anchor="mm", spacing=12) draw.text((int(W * 0.78), int(H * 0.5)), vertical_text(right), font=font_v, fill=(0, 0, 0, 255), anchor="mm", spacing=12) bg.convert("RGB").save(output) print("saved:", output) render_task("天增岁月人增寿", "春满乾坤福满门", "万象更新")

这段代码把spacing=12传给了 draw.text,其实只有在使用 multiline_text 时 spacing 才会按预期生效。更严谨的写法是调用draw.multiline_text,它需要传入 xy、text、font、fill、anchor、spacing 等参数。如果把换行符交给 draw.text,Pillow 实际上也会处理,但文档上不建议这么做。我在实际项目里会统一用 multiline_text:

draw.multiline_text((x, y), vertical_text(left), font=font_v, fill=(0, 0, 0, 255), anchor="mm", spacing=12, align="center")

对比一下:draw.text 适合单行文本,multiline_text 适合带换行符的文本,spacing 参数控制行间距。对于春联来说,最常见的竖排间距是 8 到 16 像素,具体数值取决于字体大小。

4.3 输出格式与文件命名

渲染结果通常保存为 PNG 而不是 JPG,因为春联图案中包含大块红色和黑色文字,JPG 的压缩会产生色块噪点,尤其是文字边缘。保存时要注意格式差异。

# PNG 可以直接保存 RGBA bg.save("couplets.png") # JPG 必须先转成 RGB bg.convert("RGB").save("couplets.jpg", quality=95)

PNG 保存时不传 quality 参数,因为无损压缩;JPG 的 quality 一般取 90 到 95 就能在体积和清晰度之间取得平衡。文件命名可以用内容片段加时间戳,避免多副春联互相覆盖。

import datetime def make_output_name(left, right): stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") return f"couplets_{stamp}.png"

这种命名方式在批量生成场景下很有用:比如生成数十副不同内容的春联时,直接用 left 和 right 拼接文件名更直观,但要注意文字里可能包含/\,Windows 下不允许出现在文件名中,需要先替换掉。

5. 进阶:验证字体加载结果并把脚本做成命令行工具

5.1 快速验证字体是否真的加载成功

有时候脚本能跑通,但生成的字体却不是华文隶书,而是系统自动回退的默认字体。避免这个问题的方法是打印字体元数据并和预期对比。在 test4.py 的调试阶段可以加一个检查函数。

from PIL import Image, ImageDraw, ImageFont def check_font(font_path, sample="迎新春"): font = ImageFont.truetype(font_path, 60) print("font name:", font.getname()) tmp = Image.new("RGB", (200, 100), "white") d = ImageDraw.Draw(tmp) bbox = d.textbbox((0, 0), sample, font=font) print("bbox:", bbox) return bbox

如果打印出的 font name 是 ('STLITI', 'Regular'),说明加载的是目标字体;如果变成 ('Arial', 'Regular') 或 ('simhei', 'Regular'),那多半是路径失效后 Pillow 走了默认回退逻辑。bbox 的值也能帮助你判断文字是否偏小:比如 60 号字体的 bbox 宽度远小于 60,说明字符串实际没有占满,可能是字体对当前文案缺少字形,渲染时被替换成了空字符。

5.2 把春联脚本封装成命令行小工具

更进一步的做法是在 test4.py 上包一层 argparse,让参数可以从命令行传入。这样就不用每次改代码。

import argparse from render import render_task parser = argparse.ArgumentParser(description="Render spring festival couplets") parser.add_argument("--left", required=True, help="上联文本") parser.add_argument("--right", required=True, help="下联文本") parser.add_argument("--horizontal", required=True, help="横批文本") parser.add_argument("--font", default=r"C:\Windows\Fonts\STLITI.TTF") parser.add_argument("--bg", default="bg.jpeg") parser.add_argument("--output", default="couplets.png") args = parser.parse_args() render_task(args.left, args.right, args.horizontal, args.font, args.bg, args.output)

这里需要把 render_task 的参数列表扩展成 font_path、bg_path、output 这几个参数,主流程可以复用第 4 章的渲染逻辑。命令行方式的好处是适合批处理:写一个 for 循环读取春联文案文件,逐行调用这个脚本,就能一次性生成多副不同内容的春联。需要特别注意的是,Windows 控制台默认编码可能不是 UTF-8,如果文案里有生僻字,建议在脚本开头设置sys.stdout.reconfigure(encoding="utf-8"),避免输出信息时乱码。

实际使用时,把渲染函数放到 render.py,test4.py 只做参数解析和调用。执行下面这条命令,会基于 bg.jpeg 生成一张带横批的春联图:

python test4.py --left "天增岁月人增寿" --right "春满乾坤福满门" --horizontal "万象更新" --font "C:\Windows\Fonts\STLITI.TTF" --bg "bg.jpeg" --output "new_year.png"

命令里的 --font 和 --bg 都可以替换成绝对路径,argparse 会让路径中的空格无需额外转义。如果第一次运行时字体路径报错,先检查文件是否真的存在于 C:\Windows\Fonts 下,再把路径改成项目目录内的副本;这一步跨系统迁移时最容易踩,但排查成本最低。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询