【Python】叠加图框pdf到报告pdf的每一页
·
1. 项目背景
需求描述
在工程设计中,计算书需要套用公司标准图框。手动操作繁琐且容易出错,因此开发一个自动化工具:
- 读取原始报告 PDF
- 将内容缩小并居中
- 叠加到标准图框上
- 批量处理所有页面
效果示意

2. 技术选型
2.1 PDF 处理库:pypdf
选择 pypdf 的原因:
| 特性 | pypdf | PyPDF2 | pdfplumber |
|---|---|---|---|
| 维护状态 | ✅ 活跃 | ❌ 已弃用 | ✅ 活跃 |
| 页面合并 | ✅ 支持 | ✅ 支持 | ❌ 不支持 |
| 坐标变换 | ✅ 支持 | ⚠️ 有限 | ❌ 不支持 |
| 安装依赖 | 轻量 | 轻量 | 较重 |
安装:
pip install pypdf
2.2 GUI 框架:Tkinter
选择 Tkinter 的原因:
- Python 内置,无需额外安装
- 跨平台支持 (Windows/Mac/Linux)
- 适合轻量级工具开发
- 学习曲线平缓
3. 核心功能实现
3.1 PDF 基本操作
读取 PDF 信息
from pypdf import PdfReader
# 打开 PDF
reader = PdfReader("report.pdf")
# 获取页数
total_pages = len(reader.pages)
print(f"总页数: {total_pages}")
# 获取页面尺寸 (单位: pt, 1pt = 1/72 inch)
page = reader.pages[0]
width = float(page.mediabox.width)
height = float(page.mediabox.height)
print(f"尺寸: {width} x {height} pt")
# 转换为毫米 (1 inch = 25.4 mm)
width_mm = width / 72 * 25.4
height_mm = height / 72 * 25.4
print(f"尺寸: {width_mm:.0f} x {height_mm:.0f} mm")
创建空白页面
from pypdf import PageObject
# 创建指定尺寸的空白页
new_page = PageObject.create_blank_page(
width=842, # A4 横向宽度 (pt)
height=595 # A4 横向高度 (pt)
)
3.2 页面合并与叠加
基本合并
# 将 page2 的内容叠加到 page1 上
page1.merge_page(page2)
带变换的合并(关键技术)
from pypdf import Transformation
# 创建变换:缩放 + 平移
transform = Transformation().scale(0.8).translate(tx=50, ty=100)
# 将变换后的页面叠加
base_page.merge_transformed_page(
content_page, # 要叠加的页面
transform, # 变换矩阵
over=True # True: 内容在上层, False: 内容在下层
)
3.3 坐标变换详解
Transformation 类
Transformation 支持链式调用,按顺序执行变换:
from pypdf import Transformation
# 单独使用
t1 = Transformation().scale(0.8) # 缩放到 80%
t2 = Transformation().translate(50, 100) # 平移 (50, 100)
t3 = Transformation().rotate(45) # 旋转 45°
# 链式调用 (注意顺序!)
t = Transformation().scale(0.8).translate(50, 100)
居中计算
将内容缩小后居中放置的计算方法:
def calculate_center_offset(frame_w, frame_h, content_w, content_h, scale):
"""
计算居中偏移量
Args:
frame_w, frame_h: 图框尺寸
content_w, content_h: 内容原始尺寸
scale: 缩放比例
Returns:
(offset_x, offset_y): 平移量
"""
# 缩放后的内容尺寸
scaled_w = content_w * scale
scaled_h = content_h * scale
# 居中偏移
offset_x = (frame_w - scaled_w) / 2
offset_y = (frame_h - scaled_h) / 2
return offset_x, offset_y
示意图:
图框 (frame_w x frame_h)
┌─────────────────────────────┐
│ │
│ offset_y │
│ ↓ │
│ ┌─────────────────┐ │
│ │ │ │
│←───│ scaled content │ │
│ │ │ │
│ └─────────────────┘ │
│ offset_x │
└─────────────────────────────┘
3.4 完整的叠加函数
from pathlib import Path
from pypdf import PdfReader, PdfWriter, Transformation, PageObject
def add_frame_to_pdf(report_path, frame_path, scale=0.80, output_path=None):
"""
将报告内容缩小后叠加到图框上
Args:
report_path: 原始报告 PDF 路径
frame_path: 图框 PDF 路径 (单页)
scale: 缩放比例 (0.0-1.0)
output_path: 输出文件路径
Returns:
输出文件路径
"""
report_path = Path(report_path)
frame_path = Path(frame_path)
# 默认输出路径
if output_path is None:
output_path = report_path.parent / f"{report_path.stem}_with_frame.pdf"
# 读取图框 (只取第一页)
frame_reader = PdfReader(frame_path)
frame_page = frame_reader.pages[0]
frame_w = float(frame_page.mediabox.width)
frame_h = float(frame_page.mediabox.height)
# 读取报告
report_reader = PdfReader(report_path)
writer = PdfWriter()
# 处理每一页
for page in report_reader.pages:
page_w = float(page.mediabox.width)
page_h = float(page.mediabox.height)
# 1. 创建新页面 (尺寸与图框一致)
new_page = PageObject.create_blank_page(width=frame_w, height=frame_h)
# 2. 放置图框 (底层)
new_page.merge_page(frame_page)
# 3. 计算居中偏移
scaled_w = page_w * scale
scaled_h = page_h * scale
offset_x = (frame_w - scaled_w) / 2
offset_y = (frame_h - scaled_h) / 2
# 4. 叠加缩放后的报告内容 (上层)
transform = Transformation().scale(scale).translate(offset_x, offset_y)
new_page.merge_transformed_page(page, transform, over=True)
# 5. 添加到输出
writer.add_page(new_page)
# 保存
with open(output_path, 'wb') as f:
writer.write(f)
return output_path
4. GUI 界面开发
4.1 Tkinter 基础结构
import tkinter as tk
from tkinter import ttk
# 创建主窗口
root = tk.Tk()
root.title("PDF 图框叠加工具")
root.geometry("600x450")
# 主循环
root.mainloop()
4.2 常用控件
Label 标签
label = ttk.Label(root, text="报告 PDF:")
label.pack()
Entry 输入框
# 关联变量
path_var = tk.StringVar()
entry = ttk.Entry(root, textvariable=path_var, width=50)
entry.pack()
# 获取/设置值
print(path_var.get())
path_var.set("new value")
Button 按钮
def on_click():
print("按钮被点击")
btn = ttk.Button(root, text="浏览...", command=on_click)
btn.pack()
Scale 滑块
scale_var = tk.DoubleVar(value=80.0)
def on_scale_change(val):
print(f"当前值: {float(val):.0f}%")
slider = ttk.Scale(
root,
from_=50, # 最小值
to=100, # 最大值
variable=scale_var,
orient=tk.HORIZONTAL,
command=on_scale_change
)
slider.pack()
Progressbar 进度条
progress = ttk.Progressbar(root, mode='determinate')
progress.pack(fill=tk.X)
# 更新进度 (0-100)
progress['value'] = 50
root.update_idletasks() # 强制刷新界面
4.3 布局管理
Pack 布局
widget.pack(side=tk.LEFT) # 靠左
widget.pack(side=tk.RIGHT) # 靠右
widget.pack(fill=tk.X) # 水平填充
widget.pack(expand=True) # 扩展占用空间
Grid 布局
ttk.Label(frame, text="报告:").grid(row=0, column=0, sticky=tk.W)
ttk.Entry(frame, width=50).grid(row=0, column=1, padx=5)
ttk.Button(frame, text="浏览").grid(row=0, column=2)
LabelFrame 分组框
file_frame = ttk.LabelFrame(root, text="文件选择", padding="10")
file_frame.pack(fill=tk.X, pady=10)
# 在分组框内添加控件
ttk.Label(file_frame, text="...").pack()
4.4 文件对话框
from tkinter import filedialog, messagebox
# 打开文件
path = filedialog.askopenfilename(
title="选择 PDF 文件",
filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")]
)
# 保存文件
path = filedialog.asksaveasfilename(
title="保存为",
defaultextension=".pdf",
filetypes=[("PDF files", "*.pdf")]
)
# 消息框
messagebox.showinfo("标题", "操作完成!")
messagebox.showerror("错误", "发生错误!")
messagebox.showwarning("警告", "请注意!")
4.5 类封装 GUI
class PDFFrameApp:
def __init__(self, root):
self.root = root
self.root.title("PDF 图框叠加工具")
# 定义变量
self.report_path = tk.StringVar()
self.scale_var = tk.DoubleVar(value=80.0)
# 创建界面
self.create_widgets()
def create_widgets(self):
# 文件选择区
file_frame = ttk.LabelFrame(self.root, text="文件选择")
file_frame.pack(fill=tk.X, padx=10, pady=10)
ttk.Label(file_frame, text="报告:").grid(row=0, column=0)
ttk.Entry(file_frame, textvariable=self.report_path).grid(row=0, column=1)
ttk.Button(file_frame, text="浏览", command=self.browse_report).grid(row=0, column=2)
def browse_report(self):
path = filedialog.askopenfilename(filetypes=[("PDF", "*.pdf")])
if path:
self.report_path.set(path)
# 启动应用
if __name__ == "__main__":
root = tk.Tk()
app = PDFFrameApp(root)
root.mainloop()
5. 完整代码
5.1 项目结构
pdf_frame_tool/
├── add_frame_to_pdf.py # 主程序
├── requirements.txt # 依赖
└── README.md # 说明文档
5.2 requirements.txt
pypdf>=3.0.0
5.3 主程序代码
"""
PDF 图框叠加工具 (GUI版)
"""
import sys
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
from pypdf import PdfReader, PdfWriter, Transformation, PageObject
def add_frame_to_pdf(report_path, frame_path, scale=0.80, output_path=None, progress_callback=None):
"""将报告内容缩小后叠加到图框上"""
report_path = Path(report_path)
frame_path = Path(frame_path)
if output_path is None:
output_path = report_path.parent / f"{report_path.stem}_with_frame.pdf"
else:
output_path = Path(output_path)
# 读取图框
frame_reader = PdfReader(frame_path)
frame_page = frame_reader.pages[0]
frame_w = float(frame_page.mediabox.width)
frame_h = float(frame_page.mediabox.height)
# 读取报告
report_reader = PdfReader(report_path)
writer = PdfWriter()
total_pages = len(report_reader.pages)
for i, page in enumerate(report_reader.pages):
page_w = float(page.mediabox.width)
page_h = float(page.mediabox.height)
# 创建新页面
new_page = PageObject.create_blank_page(width=frame_w, height=frame_h)
# 底层:图框
new_page.merge_page(frame_page)
# 计算居中偏移
scaled_w = page_w * scale
scaled_h = page_h * scale
offset_x = (frame_w - scaled_w) / 2
offset_y = (frame_h - scaled_h) / 2
# 上层:报告内容
transform = Transformation().scale(scale).translate(offset_x, offset_y)
new_page.merge_transformed_page(page, transform, over=True)
writer.add_page(new_page)
if progress_callback:
progress_callback(i + 1, total_pages)
with open(output_path, 'wb') as f:
writer.write(f)
return output_path, total_pages, frame_w, frame_h
class PDFFrameApp:
"""GUI 应用主类"""
def __init__(self, root):
self.root = root
self.root.title("PDF 图框叠加工具")
self.root.geometry("600x400")
self.report_path = tk.StringVar()
self.frame_path = tk.StringVar()
self.output_path = tk.StringVar()
self.scale_var = tk.DoubleVar(value=80.0)
self.create_widgets()
def create_widgets(self):
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# 文件选择
file_frame = ttk.LabelFrame(main_frame, text="文件选择", padding="10")
file_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(file_frame, text="报告 PDF:").grid(row=0, column=0, sticky=tk.W, pady=5)
ttk.Entry(file_frame, textvariable=self.report_path, width=50).grid(row=0, column=1, padx=5)
ttk.Button(file_frame, text="浏览...", command=self.browse_report).grid(row=0, column=2)
ttk.Label(file_frame, text="图框 PDF:").grid(row=1, column=0, sticky=tk.W, pady=5)
ttk.Entry(file_frame, textvariable=self.frame_path, width=50).grid(row=1, column=1, padx=5)
ttk.Button(file_frame, text="浏览...", command=self.browse_frame).grid(row=1, column=2)
ttk.Label(file_frame, text="输出 PDF:").grid(row=2, column=0, sticky=tk.W, pady=5)
ttk.Entry(file_frame, textvariable=self.output_path, width=50).grid(row=2, column=1, padx=5)
ttk.Button(file_frame, text="浏览...", command=self.browse_output).grid(row=2, column=2)
# 参数设置
param_frame = ttk.LabelFrame(main_frame, text="参数设置", padding="10")
param_frame.pack(fill=tk.X, pady=(0, 10))
scale_frame = ttk.Frame(param_frame)
scale_frame.pack(fill=tk.X)
ttk.Label(scale_frame, text="缩放比例:").pack(side=tk.LEFT)
self.scale_label = ttk.Label(scale_frame, text="80%", width=6)
self.scale_label.pack(side=tk.RIGHT)
self.scale_slider = ttk.Scale(
scale_frame, from_=50, to=100,
variable=self.scale_var,
orient=tk.HORIZONTAL,
command=self.update_scale_label
)
self.scale_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=10)
# 快捷按钮
quick_frame = ttk.Frame(param_frame)
quick_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Label(quick_frame, text="快捷:").pack(side=tk.LEFT)
for val in [70, 75, 80, 85, 90]:
ttk.Button(quick_frame, text=f"{val}%", width=5,
command=lambda v=val: self.set_scale(v)).pack(side=tk.LEFT, padx=2)
# 进度条
self.progress = ttk.Progressbar(main_frame, mode='determinate')
self.progress.pack(fill=tk.X, pady=(0, 10))
# 按钮
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X)
self.process_btn = ttk.Button(btn_frame, text="开始处理", command=self.process)
self.process_btn.pack(side=tk.RIGHT, padx=5)
def browse_report(self):
path = filedialog.askopenfilename(
title="选择报告 PDF",
filetypes=[("PDF files", "*.pdf")]
)
if path:
self.report_path.set(path)
p = Path(path)
self.output_path.set(str(p.parent / f"{p.stem}_with_frame.pdf"))
def browse_frame(self):
path = filedialog.askopenfilename(
title="选择图框 PDF",
filetypes=[("PDF files", "*.pdf")]
)
if path:
self.frame_path.set(path)
def browse_output(self):
path = filedialog.asksaveasfilename(
title="保存输出 PDF",
defaultextension=".pdf",
filetypes=[("PDF files", "*.pdf")]
)
if path:
self.output_path.set(path)
def update_scale_label(self, val):
self.scale_label.config(text=f"{float(val):.0f}%")
def set_scale(self, val):
self.scale_var.set(val)
self.scale_label.config(text=f"{val}%")
def process(self):
report = self.report_path.get()
frame = self.frame_path.get()
output = self.output_path.get()
if not report or not Path(report).exists():
messagebox.showerror("错误", "请选择有效的报告 PDF")
return
if not frame or not Path(frame).exists():
messagebox.showerror("错误", "请选择有效的图框 PDF")
return
scale = self.scale_var.get() / 100.0
self.process_btn.config(state=tk.DISABLED)
def update_progress(current, total):
self.progress['value'] = (current / total) * 100
self.root.update_idletasks()
try:
output_path, total_pages, _, _ = add_frame_to_pdf(
report, frame, scale, output, update_progress
)
messagebox.showinfo("完成", f"处理完成!\n共 {total_pages} 页\n保存至: {output_path}")
except Exception as e:
messagebox.showerror("错误", f"处理失败: {e}")
finally:
self.process_btn.config(state=tk.NORMAL)
self.progress['value'] = 0
def main():
if len(sys.argv) >= 3:
# 命令行模式
report = sys.argv[1]
frame = sys.argv[2]
scale = float(sys.argv[3]) if len(sys.argv) > 3 else 0.80
output = sys.argv[4] if len(sys.argv) > 4 else None
add_frame_to_pdf(report, frame, scale, output)
else:
# GUI 模式
root = tk.Tk()
app = PDFFrameApp(root)
root.mainloop()
if __name__ == "__main__":
main()
6. 总结
学到的技术点
| 类别 | 技术点 |
|---|---|
| PDF 操作 | pypdf 读写、页面合并、坐标变换 |
| GUI 开发 | Tkinter 控件、布局、事件处理 |
| Python | 类封装、路径处理、回调函数 |
扩展方向
- 支持批量处理 - 一次处理多个报告
- 自定义偏移 - 不仅居中,支持自定义位置
- 预览功能 - 处理前预览效果
- 打包发布 - 用 PyInstaller 打包为 exe
参考资料
更多推荐


所有评论(0)