
简介面向想在春节前后用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\FontsmacOS 在 /System/Library/Fonts 和 /Library/FontsLinux 一般在 /usr/share/fonts。如果不确定系统里有哪些中文字体在 Linux 上执行 fc-list :langzh 可以直接列出Windows 上打开字体目录按“隶书”筛选更快。2.2 test4.py 里的字体加载写法与参数含义这个项目里真正起作用的脚本是 test4.py它开头做的事情就是找到字体文件并加载。标准写法如下from PIL import Image, ImageDraw, ImageFont # Windows 下直接指向系统字体目录注意文件名要和资源管理器里看到的一致 font_path rC:\Windows\Fonts\STLITI.TTF # size120 表示字体的逻辑尺寸单位是像素 font ImageFont.truetype(font_path, size120) # 验证字体是否加载成功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, index0)。下表是几个最常见的异常场景。异常现象直接原因处理方式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 rC:\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 rC:\Windows\Fonts\STLITI.TTF font ImageFont.truetype(font_path, size120) img Image.new(RGB, (600, 300), white) draw ImageDraw.Draw(img) text 迎新春 # 测量实际绘制内容的包围盒返回 (left, top, right, bottom) bbox draw.textbbox((0, 0), text, fontfont) print(bbox:, bbox) # textlength 只返回水平方向的长度 width draw.textlength(text, fontfont) print(textlength:, width) # 水平居中用 textlength 的宽度 x (img.width - width) / 2 # 垂直居中用 bbox 的高度 y (img.height - (bbox[3] - bbox[1])) / 2 draw.text((x, y), text, fontfont, fillblack) 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(rC:\Windows\Fonts\STLITI.TTF, size120) bg Image.open(bg.jpeg).convert(RGBA) # 文字层单独一张透明画布尺寸和背景图一致 canvas Image.new(RGBA, bg.size, (255, 255, 255, 0)) draw ImageDraw.Draw(canvas) # anchormm 表示 (x, y) 是文字矩形的中心点 x bg.width // 2 y bg.height // 2 draw.text((x, y), 迎新春, fontfont, fill(255, 0, 0, 255), anchormm) result Image.alpha_composite(bg, canvas) result.convert(RGB).save(spring_couplet.png)这里先说明convert(RGBA)的意义背景图如果是 JPG本身没有 alpha 通道转成 RGBA 后统一像素格式。anchormm让文字的几何中心对准 (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_size120): size base_size font ImageFont.truetype(font_path, size) while size 10: bbox draw.textbbox((0, 0), text, fontfont) 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.TTFbg_path背景图片路径bg.jpegsidebar_font_size对联正文字号90header_font_size横批字号110margin画布边距60这里的字号建议按背景图高度比例计算例如int(bg.height * 0.15)而不是写死这样换一张竖版背景图后对联不会溢出。上联和下联文字较长时需要把字号调小否则会超出画布边界横批文字短字号可以稍大。参数表里的 margin 仅用于纯色背景的边界控制如果背景是整张图还需要配合 3.3 的 fit_font_size。4.2 自动排版一副完整春联的代码一副完整春联包括上联、下联和横批。横批一般放在顶部中间两个竖条分别放在画布左右两侧。这里涉及到竖排问题Pillow 没有内置竖排模式常见做法是把文字拆成单个字后再渲染或用换行符实现纵向排列。下面这段代码用换行符实现竖排。from PIL import Image, ImageDraw, ImageFont FONT_PATH rC:\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, outputcouplets.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) # 横批anchormm 表示以该坐标为中心 draw.text((W // 2, int(H * 0.12)), horizontal, fontfont_h, fill(255, 0, 0, 255), anchormm) # 第一个文本竖排放在画布左侧第二个文本放在右侧 draw.text((int(W * 0.22), int(H * 0.5)), vertical_text(left), fontfont_v, fill(0, 0, 0, 255), anchormm, spacing12) draw.text((int(W * 0.78), int(H * 0.5)), vertical_text(right), fontfont_v, fill(0, 0, 0, 255), anchormm, spacing12) bg.convert(RGB).save(output) print(saved:, output) render_task(天增岁月人增寿, 春满乾坤福满门, 万象更新)这段代码把spacing12传给了 draw.text其实只有在使用 multiline_text 时 spacing 才会按预期生效。更严谨的写法是调用draw.multiline_text它需要传入 xy、text、font、fill、anchor、spacing 等参数。如果把换行符交给 draw.textPillow 实际上也会处理但文档上不建议这么做。我在实际项目里会统一用 multiline_textdraw.multiline_text((x, y), vertical_text(left), fontfont_v, fill(0, 0, 0, 255), anchormm, spacing12, aligncenter)对比一下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, quality95)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 fcouplets_{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, fontfont) 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(descriptionRender spring festival couplets) parser.add_argument(--left, requiredTrue, help上联文本) parser.add_argument(--right, requiredTrue, help下联文本) parser.add_argument(--horizontal, requiredTrue, help横批文本) parser.add_argument(--font, defaultrC:\Windows\Fonts\STLITI.TTF) parser.add_argument(--bg, defaultbg.jpeg) parser.add_argument(--output, defaultcouplets.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(encodingutf-8)避免输出信息时乱码。实际使用时把渲染函数放到 render.pytest4.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 下再把路径改成项目目录内的副本这一步跨系统迁移时最容易踩但排查成本最低。本文还有配套的精品资源点击获取