【Python 实战】---- 实现批量给 pdf 插入 excel 动态生成的印章

94 阅读10分钟

1. 需求

  1. 想要能否实现批量自动为多个pdf加盖不同六格虚拟章(不改变pdf原有分辨率和文字可识别性);
  2. 改在pdf首页上方空白位置,一般居中即可;
  3. 如可由使用者自主选择靠页边距更好,以便部分首页上方有字的文件时人工可微调位置;
  4. 从上而下,自左往右分别对应 excel 中各个字段;
  5. pdf 的文件名和 excel 中档号字段一致。

2. 需求分析

  1. 印章绘制技术可采用 opencv,PIL,reportlab等;
  2. excel 文件的读取可以采用 openpyxl,pandas,xlrd,xlwt 和 xlutils等;
  3. pdf 文件的读写操作可采用 PyPDF2,pdfminer.six,PyMuPDF;
  4. 根据需求绘制印章采用了 PIL, excel 操作采用 openpyxl, pdf 读写操作采用 PyPDF2。

3. 生成印章 SealImage.py

  1. 初始化印章的边框、文本、背景颜色;
  2. 创建印章函数;

印章大小、创建印章图像、绘制印章格子、绘制印章文字、添加纹理。

  1. 方形印章格子绘制;

获取图像长宽、计算各个矩形的起始点、绘制上侧中间矩形、绘制上下矩形, 注意边框占位一半、绘制边框。

  1. 绘制印章中的文字;

获取图像长宽、计算各个文本的中心点坐标、文本、数字、英文对应的font、获取传入的字段、依次绘制对应的文本。

  1. 给印章添加纹理; 5.1 打开纹理图像,随机截取旋转; 5.2 随机截取一部分纹理图; 5.3 重新设置截取图片的的大小,并进行一次高斯模糊; 5.4 将纹理图的灰度映射到原图的透明度,由于纹理图片自带灰度,映射后会有透明效果,所以fill的透明度不能太低; 5.5 进行一次高斯模糊,提高真实度。
  2. 测试使用查看当前图片绘制情况;
  3. 保存图片;
  4. 绘制矩形;
  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

  1. 初始化插入图片的宽高;

init 默认插入图片的宽高。

  1. 设置图片的宽高;

reset_image_width_and_height 方法提供修改插入图片宽高的方法。

  1. 创建一个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 文件,并将第一页返回。
  2. 将创建的覆盖层合并到目标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 文件进行自动批量操作

  1. 设置印章距离居中的距离,距离顶部的距离;
  2. 获取路径下的所有 excel 表; 2.1 获取 path 路径下的所有文件和文件夹名称; 2.2 筛选文件列表中的 xlsx 后缀的文件名称; 2.3 返回筛选结果。
  3. 循环读取 xlsx 文件;
  4. 读取 xlsx 文件中的文本字段; 4.1 读取 xlsx 文件中的数据; 4.2 获取印章文件名以及印章中的文本字段; 4.3 读取的全部数据; 4.4 将图片合并到pdf中。
  5. 将图片合并到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. 总结

  1. 将每一个单独的功能提出来,作为一个独立的类存在,各自完善独立的功能;
  2. 最后将各个模块的代码组装,在批量程序中使用,完成自动批量化使用。