在办公自动化与文档管理场景中,电子印章的需求越来越常见。相比实体印章扫描件,使用 Python 动态生成电子印章可以做到背景透明、尺寸可控、样式统一,并且能够集成到 Web 后端或自动化脚本中。下面从零开始解析一个基于 Pillow 库的电子印章生成实现,重点涉及弧形文字布局、五角星绘制、纹理叠加等关键细节。
一、环境准备与基础依赖
实现电子印章只需要 Python 的 Pillow 图像处理库。安装命令如下:代码中默认使用 Windows 系统字体 simsun.ttc(宋体)和 arial.ttf,若运行环境为 Linux/macOS,需要将字体路径替换为系统已安装的中英文字体。
二、完整实现代码
将以下代码保存为 stamp_generator.py,然后准备一张带有杂点的纹理图片(例如纸纹照片),修改主程序中的纹理图路径和保存路径即可运行。代码主要包含一个 Stamp 类,负责参数初始化、图像绘制、纹理合成和保存。- # encoding=utf8
- from PIL import Image, ImageFont, ImageDraw, ImageFilter
- from math import pi, cos, sin, tan
- from random import randint
- def is_Chinese(ch):
- '''判断字符是否为中文'''
- if '\u4e00' <= ch <= '\u9fff':
- return True
- return False
- def pentagram(x, y, R, yDegree=0):
- '''
- 计算五角星各个顶点
- int R:五角星的长轴
- int x, y:五角星的中心点
- int yDegree:长轴与y轴的夹角
- '''
- rad = pi / 180
- r = R * sin(18 * rad) / cos(36 * rad) # 五角星短轴的长度
- # 外圈5个顶点
- RVertex = [(x - (R * cos((90 + k * 72 + yDegree) * rad)),
- y - (R * sin((90 + k * 72 + yDegree) * rad))) for k in range(5)]
- # 内圈5个凹点
- rVertex = [(x - (r * cos((90 + 36 + k * 72 + yDegree) * rad)),
- y - (r * sin((90 + 36 + k * 72 + yDegree) * rad))) for k in range(5)]
- # 交替合并顶点
- vertex = [x for y in zip(RVertex, rVertex) for x in y]
- return vertex
- def circle(x, y, r):
- '''计算圆的上下左右切点'''
- return (x - r, y - r, x + r, y + r)
- class Stamp:
- def __init__(self, edge=5, H=160, R=250, border=13, r=90,
- fill=(255, 0, 0, 120),
- words_up="", angle_up=270, font_size_up=80,
- font_xratio_up=0.66, stroke_width_up=2,
- words_mid="", angle_mid=72, font_size_mid=60,
- font_xratio_mid=0.7, stroke_width_mid=1,
- words_down="", angle_down=60, font_size_down=20,
- font_xratio_down=1, stroke_width_down=1,
- img_wl_path="", save_path=""):
- self.img_wl = Image.open(img_wl_path)
- self.img = None
- self.save_path = save_path
- self.fill = fill
- self.edge = edge
- self.H = H
- self.R = R
- self.r = r
- self.border = border
- self.words_up = words_up
- self.angle_up = angle_up
- self.font_size_up = font_size_up
- self.font_xratio_up = font_xratio_up
- self.stroke_width_up = stroke_width_up
- self.words_mid = words_mid
- self.angle_mid = angle_mid
- self.font_size_mid = font_size_mid
- self.font_xratio_mid = font_xratio_mid
- self.stroke_width_mid = stroke_width_mid
- self.words_down = words_down
- self.angle_down = angle_down
- self.font_size_down = font_size_down
- self.font_xratio_down = font_xratio_down
- self.stroke_width_down = stroke_width_down
- def draw_rotated_text(self, image, angle, xy, r, word, fill,
- font_size, font_xratio, stroke_width,
- font_flip=False, *args, **kwargs):
- """
- 在圆周上绘制单个字符。
- 原理:先创建透明蒙版并写入文字,再缩放、旋转、裁剪后粘贴到底图。
- """
- if is_Chinese(word):
- font = ImageFont.truetype("C:/Windows/Fonts/simsun.ttc", font_size, encoding="utf-8")
- else:
- font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", font_size, encoding="utf-8")
- width, height = image.size
- max_dim = max(width, height)
- mask_size = (max_dim * 2, max_dim * 2)
- mask_resize = (int(max_dim * 2 * font_xratio), max_dim * 2)
- mask = Image.new('L', mask_size, 0)
- draw = ImageDraw.Draw(mask)
- bd = draw.textbbox((max_dim, max_dim), word, font=font, align="center", *args, **kwargs)
- font_width = bd[2] - bd[0]
- font_height = bd[3] - bd[1]
- if font_flip:
- word_pos = (int(max_dim - font_width / 2), max_dim + r - font_height)
- else:
- word_pos = (int(max_dim - font_width / 2), max_dim - r)
- draw.text(word_pos, word, 255, font=font, align="center", stroke_width=stroke_width, *args, **kwargs)
- if angle % 90 == 0:
- rotated_mask = mask.resize(mask_resize).rotate(angle)
- else:
- bigger_mask = mask.resize((int(max_dim * 8 * font_xratio), max_dim * 8),
- resample=Image.BICUBIC)
- rotated_mask = bigger_mask.rotate(angle).resize(mask_resize, resample=Image.LANCZOS)
- mask_xy = (max_dim * font_xratio - xy[0], max_dim - xy[1])
- b_box = mask_xy + (mask_xy[0] + width, mask_xy[1] + height)
- mask = rotated_mask.crop(b_box)
- color_image = Image.new('RGBA', image.size, fill)
- image.paste(color_image, mask)
- def draw_stamp(self):
- img = Image.new("RGBA", (2 * (self.R + self.edge), 2 * (self.R + self.edge)), (255, 255, 255, 0))
- draw = ImageDraw.Draw(img)
- # 圆环
- draw.arc(circle(self.R + self.edge, self.R + self.edge, self.R),
- start=0, end=360, fill=self.fill, width=self.border)
- # 五角星
- draw.polygon(pentagram(self.R + self.edge, self.R + self.edge, self.r),
- fill=self.fill, outline=self.fill)
- # 上部文字(顺时针)
- angle_word = self.angle_up / len(self.words_up)
- angle_word_curr = ((len(self.words_up) - 1) / 2) * angle_word
- for word in self.words_up:
- self.draw_rotated_text(img, angle_word_curr,
- (self.R + self.edge, self.R + self.edge),
- self.R - self.border * 2, word, self.fill,
- self.font_size_up, self.font_xratio_up,
- self.stroke_width_up)
- angle_word_curr -= angle_word
- # 中部文字(横向)
- angle_word = self.angle_mid / len(self.words_mid)
- angle_word_curr = -((len(self.words_mid) - 1) / 2) * angle_word
- for word in self.words_mid:
- self.draw_rotated_text(img, 0,
- (self.R + self.edge + self.H * tan(angle_word_curr * pi / 180),
- self.R + self.edge),
- self.H, word, self.fill,
- self.font_size_mid, self.font_xratio_mid,
- self.stroke_width_mid, font_flip=True)
- angle_word_curr += angle_word
- # 下部文字(逆时针)
- angle_word = self.angle_down / len(self.words_down)
- angle_word_curr = -((len(self.words_down) - 1) / 2) * angle_word
- for word in self.words_down:
- self.draw_rotated_text(img, angle_word_curr,
- (self.R + self.edge, self.R + self.edge),
- self.R - self.border * 2, word, self.fill,
- self.font_size_down, self.font_xratio_down,
- self.stroke_width_down, font_flip=True)
- angle_word_curr += angle_word
- # 纹理叠加
- pos_random = (randint(0, 200), randint(0, 100))
- box = (pos_random[0], pos_random[1], pos_random[0] + 300, pos_random[1] + 300)
- img_wl_random = self.img_wl.crop(box).rotate(randint(0, 360))
- img_wl_random = img_wl_random.resize(img.size).convert('L').filter(ImageFilter.GaussianBlur(1))
- L, H = img.size
- for h in range(H):
- for l in range(L):
- dot = (l, h)
- img.putpixel(dot, img.getpixel(dot)[:3] +
- (int(img_wl_random.getpixel(dot) / 255 * img.getpixel(dot)[3]),))
- self.img = img.filter(ImageFilter.GaussianBlur(0.6))
- def show_stamp(self):
- if self.img:
- self.img.show()
- def save_stamp(self):
- if self.img:
- self.img.save(self.save_path)
- if __name__ == '__main__':
- words_up = "深圳叁仁有限公司"
- words_mid = "编程专用章"
- words_down = "4778290600"
- img_wl_path = "texture.png" # 纹理图路径,需替换为本地实际路径
- save_path = "stamp_output.png" # 印章保存路径
- stamp = Stamp(words_up=words_up, words_mid=words_mid, words_down=words_down,
- img_wl_path=img_wl_path, save_path=save_path)
- stamp.draw_stamp()
- stamp.show_stamp()
- stamp.save_stamp()
复制代码
三、核心实现逻辑解析
整个生成流程可拆解为四步:绘制圆环、绘制五角星、沿圆周排布文字、叠加纹理模拟油墨不均匀效果。
1. 五角星顶点计算
五角星由外圈5个顶点和内圈5个凹点交替组成。pentagram 函数先根据长轴 R 计算短轴半径 r,公式为 r = R * sin(18°) / cos(36°)。然后通过三角函数分别计算两组坐标,最终 zip 合并成按顺序排列的10个点,供 draw.polygon 直接填充。yDegree 参数可控制五角星的整体旋转角度,默认0度保证五角星正立。
2. 弧形文字绘制
这是最核心的部分。draw_rotated_text 采用“先写后旋转再裁剪”的方式:
- 创建一个2倍于底图尺寸的透明灰度蒙版,在蒙版中心位置写入单字。
- 利用 font_xratio 参数在 x 方向压缩蒙版,使文字呈现出印章中常见的窄长效果。
- 将蒙版旋转指定角度。对于非90度倍数的角度,先将蒙版放大到8倍再旋转,最后用 LANCZOS 缩回,可明显减少锯齿。
- 裁剪出与底图同样大小的区域,粘贴到目标位置。
文字方向通过 font_flip 控制。上半圈文字从圆心向外看是正立,下半圈文字若直接旋转会出现倒置,因此需要垂直翻转。
3. 多圈文字布局
印章通常有三圈文字:上部沿圆弧顺时针排列(如公司名称),中部横向排列(如“专用章”),下部沿圆弧逆时针排列(如编码)。代码中根据总角度和字符数计算每个字符的步进角度,循环调用 draw_rotated_text 完成整圈文字的绘制。中部文字使用 tan 函数计算横向偏移量,使其沿弧形底部排列。
4. 纹理叠加与模糊
为了模拟纸质印章“油墨不均”的效果,从纹理图中随机裁剪一块区域,随机旋转后缩放到印章大小,转为灰度并做轻量高斯模糊。随后逐像素用灰度值调制印章原有透明度。纹理灰度越高,该处越不透明;灰度越低,则越透明,形成自然的斑驳效果。最后整体再做一次半径为0.6的高斯模糊,消除像素感。
四、使用建议与注意事项
- 纹理图建议使用带杂点的纸纹照片,尺寸不需要太大,代码会随机裁剪300x300区域。
- fill 的透明度建议设置在 90-180 之间,纹理映射会在其基础上继续降低透明度,过低会导致文字不清晰。
- 字体路径需根据操作系统调整。非 Windows 环境可改为 /usr/share/fonts/ 下的中文字体。
- 生成的图片为 RGBA 模式的 PNG,背景完全透明,可直接叠加到 PDF、Word 或网页中。
- 如果不需要纹理效果,也可以将纹理叠加部分的代码注释掉,得到干净平整的印章。
通过以上实现,即可在纯 Python 环境中快速生成自定义电子印章,整个流程无需额外设计软件,适合集成到自动化文档处理管线中。实际使用时,只需调整 Stamp 类的参数即可改变印章尺寸、文字内容、弧度、颜色等,满足多种业务场景需求。 |