1. 需求
- 想要能否实现批量自动为多个pdf加盖不同六格虚拟章(不改变pdf原有分辨率和文字可识别性);
- 改在pdf首页上方空白位置,一般居中即可;
- 如可由使用者自主选择靠页边距更好,以便部分首页上方有字的文件时人工可微调位置;
- 从上而下,自左往右分别对应 excel 中各个字段;
- pdf 的文件名和 excel 中档号字段一致。
2. 需求分析
- 印章绘制技术可采用 opencv,PIL,reportlab等;
- excel 文件的读取可以采用 openpyxl,pandas,xlrd,xlwt 和 xlutils等;
- pdf 文件的读写操作可采用 PyPDF2,pdfminer.six,PyMuPDF;
- 根据需求绘制印章采用了 PIL, excel 操作采用 openpyxl, pdf 读写操作采用 PyPDF2。
3. 生成印章 SealImage.py
- 初始化印章的边框、文本、背景颜色;
- 创建印章函数;
印章大小、创建印章图像、绘制印章格子、绘制印章文字、添加纹理。
- 方形印章格子绘制;
获取图像长宽、计算各个矩形的起始点、绘制上侧中间矩形、绘制上下矩形, 注意边框占位一半、绘制边框。
- 绘制印章中的文字;
获取图像长宽、计算各个文本的中心点坐标、文本、数字、英文对应的font、获取传入的字段、依次绘制对应的文本。
- 给印章添加纹理; 5.1 打开纹理图像,随机截取旋转; 5.2 随机截取一部分纹理图; 5.3 重新设置截取图片的的大小,并进行一次高斯模糊; 5.4 将纹理图的灰度映射到原图的透明度,由于纹理图片自带灰度,映射后会有透明效果,所以fill的透明度不能太低; 5.5 进行一次高斯模糊,提高真实度。
- 测试使用查看当前图片绘制情况;
- 保存图片;
- 绘制矩形;
- 绘制文本。
计算传入文本的占位的盒子宽高、根据文字的占位,计算绘制文本。
from random import randint
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
from PIL import ImageFilter
class Seal:
def __init__(self):
# 印章边框颜色
self.border_color = (234, 24, 27, 180)
# 印章文本颜色
self.text_color = (234, 24, 27, 180)
# 印章背景颜色
self.bg_color = (255,255,255,1)
# 保存印章的图像对象
self.image = None
# 印章图像绘制对象
self.draw = None
# 创建印章
def create_seal(self, opts = {}):
# 印章大小
base = [950,240]
# 创建印章图像
self.image = Image.new('RGBA',tuple(base),self.bg_color)
self.draw = ImageDraw.Draw(self.image)
# 绘制印章格子
self.draw_seal_box()
# 绘制印章文字
self.draw_seal_text(opts)
# 添加纹理
self.add_texture_to_image()
# 绘制印章的格子
def draw_seal_box(self):
# 获取图像长宽
w, h = self.image.size
# 计算各个矩形的起始点
# 三分之一宽
third_w = w / 3
# 二分之一高
half_h = h / 2
# 绘制下侧右边的矩形
# self.draw_rect([third_w * 2 - 5,0,w,h], 6)
# 绘制上侧中间矩形
self.draw_rect([third_w,0,third_w * 2,h], 6)
# 绘制上下矩形, 注意边框占位一半
self.draw_rect([0,0,w,half_h], 6)
# 绘制边框
self.draw_rect([0,0,w,h])
# 绘制印章中的文字
def draw_seal_text(self, opts = {}):
# 获取图像长宽
w, h = self.image.size
# 计算各个文本的中心点坐标
# 六分之一宽
half_third_w = w / 6
# 四分之一高
half_half_h = h / 4
font_size = int(half_half_h * 1.2)
# 文本、数字、英文对应的font
font = ImageFont.truetype('C:/Windows/Fonts/simsun.ttc',font_size)
# font = ImageFont.truetype('./lib/font/FZLTZHJW.TTF',font_size)
font_num = ImageFont.truetype('./lib/font/HelveticaNeueLTPro-Lt.ttf',font_size)
font_num_en = ImageFont.truetype('./lib/font/PingFang Medium.ttf',font_size)
# "name": name,
# "year": year,
# "file_num": file_num,
# "file_page_num": file_page_num,
# "file_save_date": file_save_date,
# "qb": qb,
# "file_no": file_no
# 获取传入的字段
year = opts.get("year")
file_num = opts.get("file_num")
file_page_num = opts.get("file_page_num")
file_save_date = opts.get("file_save_date")
qb = opts.get("qb")
file_no = opts.get("file_no")
# 依次绘制对应的文本
self.draw_text(file_num, font, half_third_w * 5, half_half_h)
self.draw_text(year, font, half_third_w * 3, half_half_h)
self.draw_text(file_no, font, half_third_w, half_half_h)
self.draw_text(file_page_num, font, half_third_w * 5, half_half_h * 3)
self.draw_text(file_save_date, font, half_third_w * 3, half_half_h * 3)
self.draw_text(qb, font, half_third_w, half_half_h * 3)
# 给印章添加纹理
def add_texture_to_image(self):
# 打开纹理图像,随机截取旋转
img_wl = Image.open('./lib/icon/bg.png')
# 随机截取一部分纹理图
pos_random = (randint(0,220), randint(0,340))
box = (pos_random[0], pos_random[1], pos_random[0]+180, pos_random[1]+60)
img_wl_random = img_wl.crop(box)
# 重新设置截取图片的的大小,并进行一次高斯模糊
img_wl_random = img_wl_random.resize(self.image.size).convert('L').filter(ImageFilter.GaussianBlur(1))
# 将纹理图的灰度映射到原图的透明度,由于纹理图片自带灰度,映射后会有透明效果,所以fill的透明度不能太低
L, H = self.image.size
for h in range(H):
for l in range(L):
dot = (l, h)
color = self.image.getpixel(dot)[:3]+(int(img_wl_random.getpixel(dot)/255*self.image.getpixel(dot)[3]),)
self.image.putpixel(dot, color)
# 进行一次高斯模糊,提高真实度
self.image = self.image.filter(ImageFilter.GaussianBlur(0.6))
# 测试使用查看当前图片绘制情况
def show(self):
if self.image:
self.image.show()
# 保存图片
def save(self, path):
if self.image:
self.image.save(path)
# 绘制矩形
def draw_rect(self, shape, width=8):
self.draw.rectangle(shape, fill=None, outline=self.border_color, width=width)
# 绘制文本
def draw_text(self, text, font, x, y):
# 计算传入文本的占位的盒子宽高
x1, y1, x2, y2 = self.draw.textbbox((x, y), text, font)
text_size_x = x2 - x1
text_size_y = y2 - y1
# 根据文字的占位,计算绘制文本
self.draw.text(
(int(x - text_size_x / 2), int(y - text_size_y / 2)),
text,
font=font,
fill=self.text_color
)
if __name__ == "__main__":
seal = Seal()
seal.create_seal({
"name": "pdf-1",
"year": 2024,
"file_num": 56,
"file_page_num": 15,
"file_save_date": "期限",
"qb": "XXX",
"file_no": "X156"
})
if not os.path.exists(f'./out_images/'):
os.makedirs(f'./out_images/')
# 保存图片
seal.save(f'./out_images/pdf-1.png')
4. 印章绘制结果
5. 合并图片到 pdf 指定位置 MergeImageToPDF.py
- 初始化插入图片的宽高;
init 默认插入图片的宽高。
- 设置图片的宽高;
reset_image_width_and_height 方法提供修改插入图片宽高的方法。
- 创建一个PDF页面,并在其中放置图片; 3.1 创建一个 BytesIO 对象; 3.2 Canvas 类是 ReportLab PDF 创建工具包的核心,它提供了一个画布,你可以在上面绘制文本、图形、图像等,以生成 PDF 文档; 3.3 创建一个 Canvas 对象,指定 packet 和页面大小; 3.4 drawImage 方法插入图像; 3.5 调用 save() 方法将绘制的内容保存到 packet 对象中; 3.6 packet.seek(0) 将文件指针移动到 BytesIO 对象的开头; 3.7 使用 PdfReader 读取 pdf 文件,并将第一页返回。
- 将创建的覆盖层合并到目标PDF的指定页面。 4.1. 读取原始PDF文件【需要插入图片的 pdf】; 4.2 创建一个PDF writer对象; 4.3 遍历PDF的每一页,找到目标页面; 4.4 获取当前页面对象; 4.5 判断当前页是否是目标页面; 4.6 计算图片的插入位置; 4.7 调用 create_overlay 创建一个插入图片的 pdf 页面对象; 4.8 将 create_overlay 返回的页面通过 merge_page 合并到当前 page; 4.9 将每一个循环的 page 通过 add_page 添加到第二步中创建的 writer 对象中; 4.10 判断输出路径是否存在,然后写入 pdf。
from reportlab.pdfgen import canvas
from PyPDF2 import PdfWriter, PdfReader
import io
class InsertImageToPDF:
def __init__(self):
self.image_width = int(237 * 0.6)
self.image_height = int(60 * 0.8)
# 设置图片的宽高
def reset_image_width_and_height(self, w, h):
self.image_width = w
self.image_height = h
# 创建一个PDF页面,并在其中放置图片
def create_overlay(self, image_path, x_pos, y_pos, page_width, page_height):
packet = io.BytesIO()
can = canvas.Canvas(packet, pagesize=(page_width, page_height))
can.drawImage(image_path, x_pos, y_pos, width=self.image_width, height=self.image_height) # 图片位置和大小
can.save()
packet.seek(0)
overlay = PdfReader(packet)
return overlay.pages[0]
# 将创建的覆盖层合并到目标PDF的指定页面
def merge_image_to_pdf(self, input_pdf_path, output_pdf_path, image_path, page_number, x_pos, y_pos):
# 读取原始PDF文件
original_pdf = PdfReader(open(input_pdf_path, "rb"))
# 创建一个PDF writer对象
pdf_writer = PdfWriter()
# 遍历PDF的每一页
for page_num in range(len(original_pdf.pages)):
page = original_pdf.pages[page_num]
if page_number == page_num:
# 计算距离
left_distance = int((page.mediabox.width - self.image_width) / 2 + x_pos)
top_distance = int((page.mediabox.height - self.image_height) - y_pos)
overlay = self.create_overlay(image_path, left_distance, top_distance, page.mediabox.width, page.mediabox.height)
page.merge_page(overlay)
pdf_writer.add_page(page)
# 写入修改后的PDF
with open(output_pdf_path, "wb") as out_file:
pdf_writer.write(out_file)
if __name__ == "__main__":
pdf = InsertImageToPDF()
# 使用函数
pdf.merge_image_to_pdf(
"./pdf/pdf-1/pdf-1.pdf",
"./out_pdf/pdf-1.pdf",
"./out_images/pdf.png",
0, 0, 10)
6. 插入图片运行结果
7. excel 文件进行自动批量操作
- 设置印章距离居中的距离,距离顶部的距离;
- 获取路径下的所有 excel 表; 2.1 获取 path 路径下的所有文件和文件夹名称; 2.2 筛选文件列表中的 xlsx 后缀的文件名称; 2.3 返回筛选结果。
- 循环读取 xlsx 文件;
- 读取 xlsx 文件中的文本字段; 4.1 读取 xlsx 文件中的数据; 4.2 获取印章文件名以及印章中的文本字段; 4.3 读取的全部数据; 4.4 将图片合并到pdf中。
- 将图片合并到pdf中。
import os
import openpyxl
import shutil
from MergeImageToPDF import InsertImageToPDF
from SealImage import Seal
pdf = InsertImageToPDF()
seal = Seal()
# 设置印章距离居中的距离,距离顶部的距离
top = 10
left = 0
# 获取路径下的所有 excel 表
def get_all_xlsx(path):
# 获取 path 路径下的所有文件和文件夹名称
names = os.listdir(path)
# 筛选文件列表中的 xlsx 后缀的文件名称
xlsx_names = list(filter(lambda x : x.split('.').pop() in ['xlsx'], names))
# 返回筛选结果
return xlsx_names
# 循环读取 xlsx 文件
def loop_read_xlsx_file(path, names):
for name in names:
read_xlsx_data(f'{path}/{name}', seal, merge_image)
# 读取 xlsx 文件中的文本字段
def read_xlsx_data(path, seal, merge_image):
# 读取 xlsx 文件中的数据
wb = openpyxl.load_workbook(path,data_only=True)
sheets = wb.sheetnames
sheet_first = sheets[0]
ws = wb[sheet_first]
for r in range(2,ws.max_row + 1):
# 获取印章文件名以及印章中的文本字段
name = ws.cell(row=r, column=1).value
year = ws.cell(row=r, column=2).value
file_num = ws.cell(row=r, column=3).value
file_page_num = ws.cell(row=r, column=4).value
file_save_date = ws.cell(row=r, column=5).value
qb = ws.cell(row=r, column=6).value
file_no = ws.cell(row=r, column=7).value
# 读取的全部数据
print('当前印章数据:',name, year, file_num, file_page_num, file_save_date, qb, file_no)
if name and year and file_num and file_page_num and file_save_date and qb and file_no:
seal.create_seal({
"name": name,
"year": year,
"file_num": file_num,
"file_page_num": file_page_num,
"file_save_date": file_save_date,
"qb": qb,
"file_no": file_no
})
if not os.path.exists(f'./out_images/'):
os.makedirs(f'./out_images/')
# 保存图片
seal.save(f'./out_images/{name}.png')
print('图片绘制成功,开始合并到 pdf 中!')
# 将图片合并到pdf中
merge_image(name)
# 将图片合并到pdf中
def merge_image(name):
if not os.path.exists(f'./out_pdf/{name}/'):
os.makedirs(f'./out_pdf/{name}/')
# 使用函数
# merge_image_to_pdf(
# "./pdf/Q103-WS·2022-Y-DQB-0037/Q103-WS·2022-Y-DQB-0037.pdf",
# "./out/Q103-WS·2022-Y-DQB-0037.pdf",
# "20210125.png",
# 0, 0, 10)
pdf.merge_image_to_pdf(
f'./pdf/{name}/{name}.pdf',
f'./out_pdf/{name}/{name}.pdf',
f'./out_images/{name}.png',
0, left, top
)
print(f'{name} 文件合并成功!')
if __name__ == "__main__":
# 获取印章宽度
# image_width = int(237 * 0.6)
# image_height = int(60 * 0.8)
init_img_w = input("请输入印章宽度(默认142px):")
init_img_h = input("请输入印章高度(默认48px):")
init_img_top = input("请输入印章距离顶部距离(默认10px):")
init_img_left = input("请输入印章距离左侧距离(默认居中):")
# 判断输入宽高的值是否有效
if init_img_w and init_img_h:
pdf.reset_image_width_and_height(int(init_img_w), int(init_img_h))
# 判断输入的边距是否有效
if init_img_top:
print('init_img_top', init_img_top)
top = int(init_img_top)
if init_img_left:
print('init_img_left', int(init_img_left))
left = int(init_img_left)
# 获取所有的xlsx文件
xlsxs = get_all_xlsx('./')
# 读取文件内容
loop_read_xlsx_file('./', xlsxs)
print('数据处理完成!')
# 数据处理完成,删除生成的图片文件夹
shutil.rmtree('./out_images')
8. 批量处理运行结果
9. 总结
- 将每一个单独的功能提出来,作为一个独立的类存在,各自完善独立的功能;
- 最后将各个模块的代码组装,在批量程序中使用,完成自动批量化使用。