4. Pillow 实战:在图片上绘制文字、插入 Logo、添加边框

好了,到了真正动手的时候了。

前面几章我们聊了证书的模板设计、数据准备,但那些都还停留在「规划」阶段。这一章,我们要用 Python 的 Pillow 库,把文字、Logo、边框这些东西,真正画到图片上去。

说白了,就是让一张空白的底图,变成一张能拿得出手的证书。

4.1 准备工作:安装与基础概念

Pillow 是 Python 里最成熟的图像处理库。我最早接触它是在做一个电商图片批量加水印的项目,那时候还是 PIL,后来才升级到 Pillow。

安装命令:
pip install Pillow

建议使用 9.0.0 以上版本,对字体渲染支持更好。

在开始之前,先理清几个核心概念:

  • Image 对象:Pillow 里的一切操作都围绕它展开。打开图片、新建图片,返回的都是 Image 对象。
  • Draw 对象:负责在 Image 上「画」东西。文字、线条、形状,都通过它来绘制。
  • 坐标系:左上角是 (0, 0),x 轴向右,y 轴向下。这个和很多图形库一致。
  • 字体对象:Pillow 需要加载一个具体的字体文件(.ttf 或 .otf),才能绘制文字。

核心流程:

  1. 打开或新建一张图片 → Image.open() 或 Image.new()
  2. 创建一个 Draw 对象 → ImageDraw.Draw(image)
  3. 加载字体 → ImageFont.truetype(font_path, size)
  4. 绘制文字/图片/形状 → draw.text() / image.paste() / draw.rectangle()
  5. 保存或输出 → image.save()

4.2 在图片上绘制文字

这是证书生成里最核心的操作。你想想看,证书上那些「姓名」、「奖项名称」、「日期」,本质上都是文字。

4.2.1 基础文字绘制

from PIL import Image, ImageDraw, ImageFont

# 打开一张背景图
img = Image.open("certificate_template.png")
draw = ImageDraw.Draw(img)

# 加载字体(注意路径要换成你自己的)
font = ImageFont.truetype("fonts/SimSun.ttf", 48)

# 绘制文字
draw.text((200, 300), "张三", fill="#333333", font=font)

img.save("output.png")

这段代码看起来简单,但我在项目中踩过坑——文字位置不对。你给的坐标是文字的左上角,但不同字体的基线不一样,导致文字看起来偏上或偏下。

避坑指南:我曾经在生成一批证书时,发现「一等奖」三个字总是偏左,后来才发现是中文字体里「一」字太窄,导致整体视觉重心偏移。解决办法是用 draw.textbbox() 先计算文字的实际占位,再做居中处理。

4.2.2 文字居中与对齐

证书上的文字,大部分需要居中。比如姓名要放在证书中间,日期要右对齐。怎么做?

def draw_centered_text(draw, text, font, center_x, center_y, fill="#333333"):
    # 获取文字边界框
    bbox = draw.textbbox((0, 0), text, font=font)
    text_width = bbox[2] - bbox[0]
    text_height = bbox[3] - bbox[1]
    
    # 计算左上角坐标
    x = center_x - text_width // 2
    y = center_y - text_height // 2
    
    draw.text((x, y), text, fill=fill, font=font)

# 使用示例
font_title = ImageFont.truetype("fonts/SimHei.ttf", 72)
draw_centered_text(draw, "荣誉证书", font_title, 600, 200)

这里有个细节:textbbox() 返回的是 (left, top, right, bottom),注意它和 textsize() 的区别。textsize 已经废弃了,新版本建议用 textbbox。

4.2.3 多行文字处理

有时候证书上需要写一段话,比如「特发此证,以资鼓励」。一行放不下怎么办?

def draw_multiline_text(draw, text, font, start_x, start_y, line_height, fill="#333333"):
    lines = text.split("\n")
    for i, line in enumerate(lines):
        y = start_y + i * line_height
        draw.text((start_x, y), line, fill=fill, font=font)

# 使用示例
cert_text = "该同学在2024年度表现优异\n特发此证,以资鼓励"
draw_multiline_text(draw, cert_text, font_body, 200, 500, 60)

我的习惯:多行文字我一般用 textwrap 模块先做自动换行,再逐行绘制。这样即使文字长度变化,也不会溢出边界。

4.3 插入 Logo 与图片

证书上通常会有公司 Logo、学校校徽、或者一些装饰图案。这些需要用 image.paste() 来合成。

4.3.1 基础图片粘贴

logo = Image.open("logo.png")
# 粘贴到左上角,留边距20像素
img.paste(logo, (20, 20))

嗯,这里要注意一个问题。如果 Logo 是 PNG 格式,带有透明通道,直接 paste 会把透明部分变成黑色块。

避坑指南:我曾经在给客户做证书时,Logo 周围出现了一圈黑边,排查了半天才发现是 paste 时没有传 mask 参数。带透明通道的图片,必须用自身作为蒙版。

# 正确处理带透明通道的图片
if logo.mode == 'RGBA':
    img.paste(logo, (20, 20), logo)  # 第三个参数是 mask
else:
    img.paste(logo, (20, 20))

4.3.2 图片缩放与居中

Logo 的大小通常需要控制。太大显得突兀,太小看不清。我一般控制在证书宽度的 10%-15%。

def paste_logo_centered(img, logo_path, target_width):
    logo = Image.open(logo_path)
    
    # 按比例缩放
    ratio = target_width / logo.width
    new_height = int(logo.height * ratio)
    logo_resized = logo.resize((target_width, new_height), Image.LANCZOS)
    
    # 计算居中位置(假设放在顶部)
    x = (img.width - target_width) // 2
    y = 30  # 顶部留30像素边距
    
    if logo_resized.mode == 'RGBA':
        img.paste(logo_resized, (x, y), logo_resized)
    else:
        img.paste(logo_resized, (x, y))

# 使用示例
paste_logo_centered(img, "logo.png", 120)

4.4 添加边框与装饰线

证书的边框,说白了就是画几条线或者矩形。Pillow 的 Draw 对象提供了 rectangle() 和 line() 方法。

4.4.1 矩形边框

# 绘制外边框
draw.rectangle(
    [(30, 30), (img.width - 30, img.height - 30)],
    outline="#8B4513",  # 棕色边框
    width=4
)

# 绘制内边框(双线效果)
draw.rectangle(
    [(40, 40), (img.width - 40, img.height - 40)],
    outline="#DAA520",  # 金色边框
    width=2
)

你想想看,很多正式证书都有这种双线边框,外粗内细,显得庄重。

4.4.2 装饰线

有时候需要在标题下方加一条横线,或者用花纹做分隔。

# 在标题下方画一条金色横线
line_y = 250  # 标题下方位置
draw.line(
    [(200, line_y), (1000, line_y)],
    fill="#DAA520",
    width=3
)

# 画两条平行线(更精致的效果)
draw.line([(200, line_y), (1000, line_y)], fill="#8B4513", width=1)
draw.line([(200, line_y + 5), (1000, line_y + 5)], fill="#8B4513", width=1)

4.5 本章知识体系

下面这张图,把本章的核心操作和它们之间的关系梳理清楚了。

Pillow 证书绘制核心流程 输入:模板图片 创建 Draw 对象 绘制文字 插入 Logo 添加边框 输出:合成后的证书 三个操作可以按任意顺序执行,最终合并到同一张图片上

4.6 完整示例:合成一张证书

把上面所有的知识点串起来,写一个完整的函数。这个函数我实际用在过好几个项目里,稍微改改就能用。

from PIL import Image, ImageDraw, ImageFont
import os

def generate_certificate(template_path, output_path, name, title, date, logo_path=None):
    """
    生成一张完整的证书
    :param template_path: 背景模板路径
    :param output_path: 输出路径
    :param name: 姓名
    :param title: 奖项名称
    :param date: 日期
    :param logo_path: Logo路径(可选)
    """
    # 1. 打开模板
    img = Image.open(template_path)
    draw = ImageDraw.Draw(img)
    
    # 2. 加载字体
    font_title = ImageFont.truetype("fonts/SimHei.ttf", 80)
    font_name = ImageFont.truetype("fonts/SimSun.ttf", 60)
    font_body = ImageFont.truetype("fonts/SimSun.ttf", 36)
    
    # 3. 绘制边框
    draw.rectangle([(20, 20), (img.width-20, img.height-20)], outline="#8B4513", width=6)
    draw.rectangle([(35, 35), (img.width-35, img.height-35)], outline="#DAA520", width=2)
    
    # 4. 绘制标题(居中)
    center_x = img.width // 2
    bbox = draw.textbbox((0, 0), "荣誉证书", font=font_title)
    title_width = bbox[2] - bbox[0]
    draw.text((center_x - title_width//2, 80), "荣誉证书", fill="#8B4513", font=font_title)
    
    # 5. 绘制姓名
    bbox = draw.textbbox((0, 0), name, font=font_name)
    name_width = bbox[2] - bbox[0]
    draw.text((center_x - name_width//2, 220), name, fill="#333333", font=font_name)
    
    # 6. 绘制奖项
    bbox = draw.textbbox((0, 0), title, font=font_body)
    title_w = bbox[2] - bbox[0]
    draw.text((center_x - title_w//2, 320), title, fill="#555555", font=font_body)
    
    # 7. 绘制日期(右对齐)
    date_text = f"签发日期:{date}"
    bbox = draw.textbbox((0, 0), date_text, font=font_body)
    date_width = bbox[2] - bbox[0]
    draw.text((img.width - 80 - date_width, 450), date_text, fill="#777777", font=font_body)
    
    # 8. 插入Logo(如果有)
    if logo_path and os.path.exists(logo_path):
        logo = Image.open(logo_path)
        logo_width = 100
        ratio = logo_width / logo.width
        logo_height = int(logo.height * ratio)
        logo_resized = logo.resize((logo_width, logo_height), Image.LANCZOS)
        
        logo_x = 50
        logo_y = 50
        if logo_resized.mode == 'RGBA':
            img.paste(logo_resized, (logo_x, logo_y), logo_resized)
        else:
            img.paste(logo_resized, (logo_x, logo_y))
    
    # 9. 保存
    img.save(output_path)
    print(f"证书已生成:{output_path}")

# 使用示例
generate_certificate(
    template_path="template.png",
    output_path="cert_张三.png",
    name="张三",
    title="荣获2024年度优秀员工称号",
    date="2024年12月31日",
    logo_path="company_logo.png"
)

关键参数速查表:

方法 用途 常用参数
draw.text() 绘制文字 xy, text, fill, font
draw.textbbox() 计算文字尺寸 xy, text, font
img.paste() 粘贴图片 im, box, mask
draw.rectangle() 绘制矩形边框 xy, outline, width
draw.line() 绘制线条 xy, fill, width
Image.open() 打开图片 filepath
ImageFont.truetype() 加载字体 fontpath, size