查看: 154|回复: 3

Python电子印章制作:弧形文字与纹理防伪实现解析

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在办公自动化与文档管理场景中,电子印章的需求越来越常见。相比实体印章扫描件,使用 Python 动态生成电子印章可以做到背景透明、尺寸可控、样式统一,并且能够集成到 Web 后端或自动化脚本中。下面从零开始解析一个基于 Pillow 库的电子印章生成实现,重点涉及弧形文字布局、五角星绘制、纹理叠加等关键细节。

一、环境准备与基础依赖

实现电子印章只需要 Python 的 Pillow 图像处理库。安装命令如下:
  1. pip install pillow
复制代码
代码中默认使用 Windows 系统字体 simsun.ttc(宋体)和 arial.ttf,若运行环境为 Linux/macOS,需要将字体路径替换为系统已安装的中英文字体。

二、完整实现代码

将以下代码保存为 stamp_generator.py,然后准备一张带有杂点的纹理图片(例如纸纹照片),修改主程序中的纹理图路径和保存路径即可运行。代码主要包含一个 Stamp 类,负责参数初始化、图像绘制、纹理合成和保存。
  1. # encoding=utf8
  2. from PIL import Image, ImageFont, ImageDraw, ImageFilter
  3. from math import pi, cos, sin, tan
  4. from random import randint
  5. def is_Chinese(ch):
  6.     '''判断字符是否为中文'''
  7.     if '\u4e00' <= ch <= '\u9fff':
  8.         return True
  9.     return False
  10. def pentagram(x, y, R, yDegree=0):
  11.     '''
  12.     计算五角星各个顶点
  13.     int R:五角星的长轴
  14.     int x, y:五角星的中心点
  15.     int yDegree:长轴与y轴的夹角
  16.     '''
  17.     rad = pi / 180
  18.     r = R * sin(18 * rad) / cos(36 * rad)  # 五角星短轴的长度
  19.     # 外圈5个顶点
  20.     RVertex = [(x - (R * cos((90 + k * 72 + yDegree) * rad)),
  21.                 y - (R * sin((90 + k * 72 + yDegree) * rad))) for k in range(5)]
  22.     # 内圈5个凹点
  23.     rVertex = [(x - (r * cos((90 + 36 + k * 72 + yDegree) * rad)),
  24.                 y - (r * sin((90 + 36 + k * 72 + yDegree) * rad))) for k in range(5)]
  25.     # 交替合并顶点
  26.     vertex = [x for y in zip(RVertex, rVertex) for x in y]
  27.     return vertex
  28. def circle(x, y, r):
  29.     '''计算圆的上下左右切点'''
  30.     return (x - r, y - r, x + r, y + r)
  31. class Stamp:
  32.     def __init__(self, edge=5, H=160, R=250, border=13, r=90,
  33.                  fill=(255, 0, 0, 120),
  34.                  words_up="", angle_up=270, font_size_up=80,
  35.                  font_xratio_up=0.66, stroke_width_up=2,
  36.                  words_mid="", angle_mid=72, font_size_mid=60,
  37.                  font_xratio_mid=0.7, stroke_width_mid=1,
  38.                  words_down="", angle_down=60, font_size_down=20,
  39.                  font_xratio_down=1, stroke_width_down=1,
  40.                  img_wl_path="", save_path=""):
  41.         self.img_wl = Image.open(img_wl_path)
  42.         self.img = None
  43.         self.save_path = save_path
  44.         self.fill = fill
  45.         self.edge = edge
  46.         self.H = H
  47.         self.R = R
  48.         self.r = r
  49.         self.border = border
  50.         self.words_up = words_up
  51.         self.angle_up = angle_up
  52.         self.font_size_up = font_size_up
  53.         self.font_xratio_up = font_xratio_up
  54.         self.stroke_width_up = stroke_width_up
  55.         self.words_mid = words_mid
  56.         self.angle_mid = angle_mid
  57.         self.font_size_mid = font_size_mid
  58.         self.font_xratio_mid = font_xratio_mid
  59.         self.stroke_width_mid = stroke_width_mid
  60.         self.words_down = words_down
  61.         self.angle_down = angle_down
  62.         self.font_size_down = font_size_down
  63.         self.font_xratio_down = font_xratio_down
  64.         self.stroke_width_down = stroke_width_down
  65.     def draw_rotated_text(self, image, angle, xy, r, word, fill,
  66.                           font_size, font_xratio, stroke_width,
  67.                           font_flip=False, *args, **kwargs):
  68.         """
  69.         在圆周上绘制单个字符。
  70.         原理:先创建透明蒙版并写入文字,再缩放、旋转、裁剪后粘贴到底图。
  71.         """
  72.         if is_Chinese(word):
  73.             font = ImageFont.truetype("C:/Windows/Fonts/simsun.ttc", font_size, encoding="utf-8")
  74.         else:
  75.             font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", font_size, encoding="utf-8")
  76.         width, height = image.size
  77.         max_dim = max(width, height)
  78.         mask_size = (max_dim * 2, max_dim * 2)
  79.         mask_resize = (int(max_dim * 2 * font_xratio), max_dim * 2)
  80.         mask = Image.new('L', mask_size, 0)
  81.         draw = ImageDraw.Draw(mask)
  82.         bd = draw.textbbox((max_dim, max_dim), word, font=font, align="center", *args, **kwargs)
  83.         font_width = bd[2] - bd[0]
  84.         font_height = bd[3] - bd[1]
  85.         if font_flip:
  86.             word_pos = (int(max_dim - font_width / 2), max_dim + r - font_height)
  87.         else:
  88.             word_pos = (int(max_dim - font_width / 2), max_dim - r)
  89.         draw.text(word_pos, word, 255, font=font, align="center", stroke_width=stroke_width, *args, **kwargs)
  90.         if angle % 90 == 0:
  91.             rotated_mask = mask.resize(mask_resize).rotate(angle)
  92.         else:
  93.             bigger_mask = mask.resize((int(max_dim * 8 * font_xratio), max_dim * 8),
  94.                                      resample=Image.BICUBIC)
  95.             rotated_mask = bigger_mask.rotate(angle).resize(mask_resize, resample=Image.LANCZOS)
  96.         mask_xy = (max_dim * font_xratio - xy[0], max_dim - xy[1])
  97.         b_box = mask_xy + (mask_xy[0] + width, mask_xy[1] + height)
  98.         mask = rotated_mask.crop(b_box)
  99.         color_image = Image.new('RGBA', image.size, fill)
  100.         image.paste(color_image, mask)
  101.     def draw_stamp(self):
  102.         img = Image.new("RGBA", (2 * (self.R + self.edge), 2 * (self.R + self.edge)), (255, 255, 255, 0))
  103.         draw = ImageDraw.Draw(img)
  104.         # 圆环
  105.         draw.arc(circle(self.R + self.edge, self.R + self.edge, self.R),
  106.                  start=0, end=360, fill=self.fill, width=self.border)
  107.         # 五角星
  108.         draw.polygon(pentagram(self.R + self.edge, self.R + self.edge, self.r),
  109.                      fill=self.fill, outline=self.fill)
  110.         # 上部文字(顺时针)
  111.         angle_word = self.angle_up / len(self.words_up)
  112.         angle_word_curr = ((len(self.words_up) - 1) / 2) * angle_word
  113.         for word in self.words_up:
  114.             self.draw_rotated_text(img, angle_word_curr,
  115.                                    (self.R + self.edge, self.R + self.edge),
  116.                                    self.R - self.border * 2, word, self.fill,
  117.                                    self.font_size_up, self.font_xratio_up,
  118.                                    self.stroke_width_up)
  119.             angle_word_curr -= angle_word
  120.         # 中部文字(横向)
  121.         angle_word = self.angle_mid / len(self.words_mid)
  122.         angle_word_curr = -((len(self.words_mid) - 1) / 2) * angle_word
  123.         for word in self.words_mid:
  124.             self.draw_rotated_text(img, 0,
  125.                                    (self.R + self.edge + self.H * tan(angle_word_curr * pi / 180),
  126.                                     self.R + self.edge),
  127.                                    self.H, word, self.fill,
  128.                                    self.font_size_mid, self.font_xratio_mid,
  129.                                    self.stroke_width_mid, font_flip=True)
  130.             angle_word_curr += angle_word
  131.         # 下部文字(逆时针)
  132.         angle_word = self.angle_down / len(self.words_down)
  133.         angle_word_curr = -((len(self.words_down) - 1) / 2) * angle_word
  134.         for word in self.words_down:
  135.             self.draw_rotated_text(img, angle_word_curr,
  136.                                    (self.R + self.edge, self.R + self.edge),
  137.                                    self.R - self.border * 2, word, self.fill,
  138.                                    self.font_size_down, self.font_xratio_down,
  139.                                    self.stroke_width_down, font_flip=True)
  140.             angle_word_curr += angle_word
  141.         # 纹理叠加
  142.         pos_random = (randint(0, 200), randint(0, 100))
  143.         box = (pos_random[0], pos_random[1], pos_random[0] + 300, pos_random[1] + 300)
  144.         img_wl_random = self.img_wl.crop(box).rotate(randint(0, 360))
  145.         img_wl_random = img_wl_random.resize(img.size).convert('L').filter(ImageFilter.GaussianBlur(1))
  146.         L, H = img.size
  147.         for h in range(H):
  148.             for l in range(L):
  149.                 dot = (l, h)
  150.                 img.putpixel(dot, img.getpixel(dot)[:3] +
  151.                              (int(img_wl_random.getpixel(dot) / 255 * img.getpixel(dot)[3]),))
  152.         self.img = img.filter(ImageFilter.GaussianBlur(0.6))
  153.     def show_stamp(self):
  154.         if self.img:
  155.             self.img.show()
  156.     def save_stamp(self):
  157.         if self.img:
  158.             self.img.save(self.save_path)
  159. if __name__ == '__main__':
  160.     words_up = "深圳叁仁有限公司"
  161.     words_mid = "编程专用章"
  162.     words_down = "4778290600"
  163.     img_wl_path = "texture.png"  # 纹理图路径,需替换为本地实际路径
  164.     save_path = "stamp_output.png"  # 印章保存路径
  165.     stamp = Stamp(words_up=words_up, words_mid=words_mid, words_down=words_down,
  166.                   img_wl_path=img_wl_path, save_path=save_path)
  167.     stamp.draw_stamp()
  168.     stamp.show_stamp()
  169.     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 类的参数即可改变印章尺寸、文字内容、弧度、颜色等,满足多种业务场景需求。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python电子印章制作:弧形文字与纹理防伪实现解析

看了楼主的实现,弧形文字这块确实讲得清楚,特别是五角星顶点坐标用内外圈交替合并的思路很巧妙。纹理叠加那部分对防伪感提升挺大的,真实印章那种油墨不均匀的质感一下就出来了。我平时用Pillow最多就处理个图片加水印,没想到还能这么玩。 有个小问题想请教下:弧形文字如果字数比较多,会不会出现首尾间距不协调的情况?调整 `font_xratio_up` 参数能解决吗?还是说需要在文字长度上做动态计算?另外,楼主用的纹理图是直接叠加还是做了混合模式处理?如果纹理图尺幅比印章小,拉伸会不会影响防伪纹路的效果?
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python电子印章制作:弧形文字与纹理防伪实现解析

感谢楼主的详细分享,把弧形文字和五角星绘制的思路拆解得挺清晰,对用 Pillow 做电子印章的常见需求很有参考价值。期待看到后续完整代码或更多效果演示,特别是纹理防伪合成那块,感觉还能展开讲讲。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python电子印章制作:弧形文字与纹理防伪实现解析

感谢分享,这个思路很实用,正好最近在搞电子签章相关的自动化。弧形文字那段用角度和半径算坐标挺巧妙的,但 `draw_rotated_text` 的代码好像没贴完整?后面纹理叠加的部分也想看看具体怎么合成的。方便补全一下吗?或者简单说说纹理图是怎么和印章融合的?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-23 12:31 , Processed in 0.023625 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部