当前位置: 代码网 > it编程>前端脚本>Python > 使用Python开发一个桌面版PDF盖章工具

使用Python开发一个桌面版PDF盖章工具

2025年12月18日 Python 我要评论
一、项目概述这个工具的主要功能包括:打开和预览pdf文件加载印章图片(支持png、jpg等格式)在pdf页面上精确点击添加印章调整印章大小撤销操作、清空页面印章保存盖章后的pdf文件二、环境准备首先需

一、项目概述

这个工具的主要功能包括:

  • 打开和预览pdf文件
  • 加载印章图片(支持png、jpg等格式)
  • 在pdf页面上精确点击添加印章
  • 调整印章大小
  • 撤销操作、清空页面印章
  • 保存盖章后的pdf文件

二、环境准备

首先需要安装必要的python库:

pip install pyqt5 pymupdf pillow

注意:pymupdf是fitz模块的库,通过pip install pymupdf安装。

三、核心代码解析

1. pdf预览编辑画布类

class pdfeditpreview(qlabel):
    # 信号:点击坐标 (x, y)
    clicked_pos = pyqtsignal(float, float)

    def __init__(self):
        super().__init__()
        self.setalignment(qt.aligncenter)
        self.setstylesheet('background: #e0e0e0; color: #888;')
        self.settext("请打开 pdf 文件")
        
        self.doc = none  # pdf文档对象
        self.page_index = 0  # 当前页码
        self.current_pixmap = none  # 当前页面图像
        self.stamps_to_draw = []  # 当前页印章列表

这个类继承自qlabel,负责显示pdf页面并处理用户点击。关键点:

  1. 坐标转换:将屏幕点击坐标转换为pdf点坐标(1点=1/72英寸)
  2. 自适应高度:根据pdf页面比例自动调整显示高度
  3. 印章叠加:在pdf页面上绘制已添加的印章

2. 印章添加逻辑

def add_stamp(self, pdf_x, pdf_y):
    if not self.doc or self.seal_bytes is none:
        qmessagebox.warning(self, "提示", "请先加载 pdf 和 印章图片!")
        return
        
    scale = self.spin_scale.value() / 100.0
    
    # 计算印章在pdf中的实际尺寸
    actual_w = self.seal_w_pt * scale
    actual_h = actual_w * self.seal_ratio
    
    stamp_record = {
        'page': self.current_page,
        'x': pdf_x,      # pdf坐标x
        'y': pdf_y,      # pdf坐标y
        'w_pt': actual_w, # 印章宽度(点)
        'h_pt': actual_h, # 印章高度(点)
        'img_bytes': self.seal_bytes, # 印章图片原始数据
        'pixmap': self.seal_pixmap    # 用于显示的qpixmap
    }
    
    self.stamps_list.append(stamp_record)
    self.refresh_preview()

3. 滚动预览的修复

原版本存在预览区域底部被裁剪的问题,修复方案:

# 创建滚动区域
self.scroll_area = qscrollarea()
self.scroll_area.setwidget(self.preview)
self.scroll_area.setwidgetresizable(true)  # 关键:让预览组件宽度跟随滚动区
self.scroll_area.setstylesheet("border: 2px dashed #aaa;")

# 在预览类中实现自适应高度
def adjust_height(self):
    """根据当前控件宽度和图片比例,自动调整控件高度"""
    if not self.current_pixmap or self.img_w_raw == 0:
        return
        
    # 计算宽高比
    ratio = self.img_h_raw / self.img_w_raw
    # 目标高度 = 当前宽度 * 比例
    target_height = int(self.width() * ratio)
    
    # 设置最小高度,这样scrollarea就会出现滚动条
    if self.minimumheight() != target_height:
        self.setminimumheight(target_height)

4. 保存pdf文件

def save_pdf(self):
    if not self.doc: return
    if not self.stamps_list:
        qmessagebox.information(self, "提示", "当前没有盖任何章,不需要保存。")
        return
        
    save_path, _ = qfiledialog.getsavefilename(
        self, "保存文件", 
        self.pdf_path.replace(".pdf", "_stamped.pdf"), 
        "pdf files (*.pdf)"
    )
    
    for stamp in self.stamps_list:
        page = self.doc[stamp['page']]
        # 计算印章在pdf中的位置
        rect_x0 = stamp['x'] - stamp['w_pt'] / 2
        rect_y0 = stamp['y'] - stamp['h_pt'] / 2
        rect_x1 = stamp['x'] + stamp['w_pt'] / 2
        rect_y1 = stamp['y'] + stamp['h_pt'] / 2
        rect = fitz.rect(rect_x0, rect_y0, rect_x1, rect_y1)
        
        # 将印章插入pdf
        page.insert_image(rect, stream=stamp['img_bytes'])

四、界面布局设计

工具界面分为三个主要区域:

+----------------+-------------------------------+
|  左侧工具栏    |         pdf预览区            |
|                |  (带滚动条,支持长页面)      |
+----------------+-------------------------------+
|                |        页面导航条           |
+----------------+-------------------------------+

左侧工具栏包含:

  1. 文件操作区域:打开pdf文件
  2. 印章设置区域:选择印章图片、调整大小
  3. 编辑控制区域:撤销、清空操作
  4. 保存按钮

五、使用步骤

  1. 打开pdf文件:点击"打开pdf"按钮选择文件
  2. 加载印章:点击"选择印章"按钮,支持png、jpg等格式
  3. 调整印章大小:通过缩放比例旋钮调整(5%-500%)
  4. 添加印章:在pdf预览区域点击鼠标左键
  5. 编辑操作:可撤销最后一个印章或清空当前页
  6. 保存文件:点击"另存为pdf"保存盖章后的文件

六、技术要点

  1. 坐标系统转换:需要在屏幕像素坐标、pdf点坐标和图像显示坐标之间进行精确转换
  2. 图像处理:使用pillow处理印章图片的透明通道
  3. 内存管理:及时关闭pdf文档,避免内存泄漏
  4. 用户体验:添加撤销功能、实时预览和错误提示

七、完整代码

以下是完整的python代码:

import sys
import os
import fitz  # pymupdf
from io import bytesio 
from pil import image

from pyqt5.qtwidgets import (
    qapplication, qmainwindow, qwidget, qvboxlayout, qhboxlayout,
    qgridlayout, qpushbutton, qlabel, qfiledialog, qmessagebox,
    qspinbox, qgroupbox, qlineedit, qsplitter, qscrollarea
)
from pyqt5.qtcore import qt, pyqtsignal, qrect
from pyqt5.qtgui import qpixmap, qimage, qpainter, qpen, qcolor, qicon

# ===========================
# pdf 预览/编辑画布
# ===========================
class pdfeditpreview(qlabel):
    # 信号:点击坐标 (x, y)
    clicked_pos = pyqtsignal(float, float)

    def __init__(self):
        super().__init__()
        self.setalignment(qt.aligncenter)
        # 去掉固定的 border 样式,改由 scrollarea 管理外观,这里只保留背景
        self.setstylesheet('background: #e0e0e0; color: #888;')
        self.settext("请打开 pdf 文件")
        
        self.doc = none
        self.page_index = 0
        self.current_pixmap = none
        
        # 存储当前页面的已盖章列表 (由主窗口传入)
        self.stamps_to_draw = []
        
        # 坐标转换参数
        self.img_w_raw = 0.0
        self.img_h_raw = 0.0

    def load_page(self, doc, index):
        self.doc = doc
        self.page_index = index
        self.render()

    def render(self):
        if not self.doc: return
        
        page = self.doc[self.page_index]
        
        # 获取 120 dpi 的图像用于显示
        pix = page.get_pixmap(dpi=120) 
        self.img_w_raw = pix.width
        self.img_h_raw = pix.height

        img = qimage(pix.samples, self.img_w_raw, self.img_h_raw, pix.stride, qimage.format_rgb888)
        self.current_pixmap = qpixmap.fromimage(img)
        
        # 加载新图片后,立即调整控件高度以适应比例
        self.adjust_height()
        self.update() 

    def adjust_height(self):
        """根据当前控件宽度和图片比例,自动调整控件高度,确保底部不被裁剪"""
        if not self.current_pixmap or self.img_w_raw == 0:
            return
            
        # 计算宽高比
        ratio = self.img_h_raw / self.img_w_raw
        # 目标高度 = 当前宽度 * 比例
        target_height = int(self.width() * ratio)
        
        # 设置最小高度,这样 scrollarea 就会出现滚动条
        if self.minimumheight() != target_height:
            self.setminimumheight(target_height)

    def resizeevent(self, event):
        """当窗口大小改变(宽度改变)时,重新计算高度"""
        super().resizeevent(event)
        self.adjust_height()

    def set_stamps(self, stamps_list):
        """接收主窗口传来的当前页盖章数据,用于重绘"""
        self.stamps_to_draw = stamps_list
        self.update()

    def mousepressevent(self, e):
        if not self.doc or not self.current_pixmap: return
        
        # 计算图片在 label 中的显示区域(居中)
        scaled_pix = self.current_pixmap.scaledtowidth(self.width(), qt.smoothtransformation)
        img_x_start = (self.width() - scaled_pix.width()) / 2
        img_y_start = (self.height() - scaled_pix.height()) / 2
        
        # 获取相对于图片的点击坐标
        click_x = e.x() - img_x_start
        click_y = e.y() - img_y_start
        
        # 检查是否点击在图片范围内
        if click_x < 0 or click_x > scaled_pix.width() or \
           click_y < 0 or click_y > scaled_pix.height():
            return

        # 转换为 pdf 点坐标 (pdf points)
        pdf_page = self.doc[self.page_index]
        scale = pdf_page.rect.width / scaled_pix.width()
        
        pdf_x = click_x * scale
        pdf_y = click_y * scale
        
        self.clicked_pos.emit(pdf_x, pdf_y)

    def paintevent(self, e):
        super().paintevent(e) # 绘制背景
        
        if not self.doc or not self.current_pixmap: return

        painter = qpainter(self)
        painter.setrenderhint(qpainter.smoothpixmaptransform)
        
        # 1. 绘制 pdf 底图
        # 使用 scaledtowidth 确保宽度填满,高度随 resizeevent 自动调整
        scaled_pix = self.current_pixmap.scaledtowidth(self.width(), qt.smoothtransformation)
        img_x_start = (self.width() - scaled_pix.width()) / 2
        img_y_start = (self.height() - scaled_pix.height()) / 2
        
        painter.drawpixmap(int(img_x_start), int(img_y_start), scaled_pix)
        
        # 2. 绘制已确认的印章 (overlay)
        pdf_page = self.doc[self.page_index]
        pt_to_px = scaled_pix.width() / pdf_page.rect.width
        
        for stamp in self.stamps_to_draw:
            # 尺寸转屏幕像素
            w_screen = stamp['w_pt'] * pt_to_px
            h_screen = stamp['h_pt'] * pt_to_px
            
            # 中心坐标转屏幕像素
            cx_screen = stamp['x'] * pt_to_px
            cy_screen = stamp['y'] * pt_to_px
            
            # 计算绘制左上角
            draw_x = img_x_start + cx_screen - (w_screen / 2)
            draw_y = img_y_start + cy_screen - (h_screen / 2)
            
            target_rect = qrect(int(draw_x), int(draw_y), int(w_screen), int(h_screen))
            painter.drawpixmap(target_rect, stamp['pixmap'])

            # 绘制一个小红框表示这是后加的章
            painter.setpen(qpen(qcolor(255, 0, 0, 100), 1, qt.dashline))
            painter.drawrect(target_rect)

        painter.end()


# ===========================
# 主窗口
# ===========================
class mainwindow(qmainwindow):
    def __init__(self):
        super().__init__()
        
        # --- 数据状态 ---
        self.doc = none
        self.pdf_path = ""
        self.current_page = 0
        self.stamps_list = [] 
        self.seal_pixmap = none
        self.seal_bytes = none
        self.seal_w_pt = 0.0
        self.seal_ratio = 1.0
        
        self.init_ui()

    def init_ui(self):
        self.setwindowtitle('pdf 手动盖章编辑器 (滚动预览修复版)')
        self.setgeometry(100, 100, 1300, 850)
        
        main_widget = qwidget()
        self.setcentralwidget(main_widget)
        main_layout = qhboxlayout(main_widget)

        # ===== 左侧:工具栏 (保持不变) =====
        tools_panel = qvboxlayout()
        tools_panel.setcontentsmargins(0, 0, 10, 0)
        
        gb_file = qgroupbox("1. 文件操作")
        v_file = qvboxlayout(gb_file)
        btn_open = qpushbutton("📂 打开 pdf")
        btn_open.setstylesheet("padding: 8px; font-weight: bold;")
        btn_open.clicked.connect(self.open_pdf)
        self.lbl_info = qlabel("未加载文件")
        self.lbl_info.setstylesheet("color: #666; font-size: 11px;")
        v_file.addwidget(btn_open)
        v_file.addwidget(self.lbl_info)
        
        gb_seal = qgroupbox("2. 印章设置")
        v_seal = qvboxlayout(gb_seal)
        self.txt_seal_path = qlineedit()
        self.txt_seal_path.setplaceholdertext("选择印章图片...")
        self.txt_seal_path.setreadonly(true)
        btn_sel_seal = qpushbutton("🖼️ 选择印章")
        btn_sel_seal.clicked.connect(self.select_seal)
        self.spin_scale = qspinbox()
        self.spin_scale.setrange(5, 500)
        self.spin_scale.setvalue(35)
        self.spin_scale.setsuffix(" %")
        self.spin_scale.valuechanged.connect(self.update_seal_preview_info)
        self.lbl_seal_preview = qlabel("印章预览")
        self.lbl_seal_preview.setalignment(qt.aligncenter)
        self.lbl_seal_preview.setfixedsize(150, 150)
        self.lbl_seal_preview.setstylesheet("border: 1px solid #ddd; background: #fff;")
        v_seal.addwidget(btn_sel_seal)
        v_seal.addwidget(self.txt_seal_path)
        v_seal.addwidget(qlabel("缩放比例:"))
        v_seal.addwidget(self.spin_scale)
        v_seal.addwidget(self.lbl_seal_preview, 0, qt.aligncenter)
        
        gb_action = qgroupbox("3. 编辑控制")
        v_action = qvboxlayout(gb_action)
        btn_undo = qpushbutton("↩️ 撤销上一个印章")
        btn_undo.clicked.connect(self.undo_last_stamp)
        btn_clear_page = qpushbutton("🗑️ 清空当前页印章")
        btn_clear_page.clicked.connect(self.clear_page_stamps)
        v_action.addwidget(btn_undo)
        v_action.addwidget(btn_clear_page)

        btn_save = qpushbutton("💾 另存为 pdf")
        btn_save.setfixedheight(50)
        btn_save.setstylesheet("background-color: #28a745; color: white; font-size: 14px; font-weight: bold;")
        btn_save.clicked.connect(self.save_pdf)
        
        tools_panel.addwidget(gb_file)
        tools_panel.addwidget(gb_seal)
        tools_panel.addwidget(gb_action)
        tools_panel.addstretch()
        tools_panel.addwidget(btn_save)

        # ===== 中间:预览区 (修复重点) =====
        preview_layout = qvboxlayout()
        
        self.preview = pdfeditpreview()
        self.preview.clicked_pos.connect(self.add_stamp)
        
        # --- 创建滚动区域 ---
        self.scroll_area = qscrollarea()
        self.scroll_area.setwidget(self.preview)
        self.scroll_area.setwidgetresizable(true) # 关键:让预览组件宽度跟随滚动区,但高度由组件自己决定
        self.scroll_area.setstylesheet("border: 2px dashed #aaa;")
        
        # 翻页条
        nav_layout = qhboxlayout()
        self.btn_prev = qpushbutton("◀ 上一页")
        self.btn_next = qpushbutton("下一页 ▶")
        self.lbl_page = qlabel("0 / 0")
        
        self.btn_prev.clicked.connect(self.prev_page)
        self.btn_next.clicked.connect(self.next_page)
        
        nav_layout.addwidget(self.btn_prev)
        nav_layout.addwidget(self.lbl_page)
        nav_layout.addwidget(self.btn_next)
        
        preview_layout.addwidget(qlabel("<b>🖱️ 操作说明:</b>加载 pdf 和印章后,直接在右侧页面上<b>点击鼠标左键</b>即可盖章。"))
        preview_layout.addwidget(self.scroll_area, 1) # 添加滚动区域而不是直接添加 preview
        preview_layout.addlayout(nav_layout)

        # 组合布局
        tools_widget = qwidget()
        tools_widget.setlayout(tools_panel)
        tools_widget.setfixedwidth(280)
        
        main_layout.addwidget(tools_widget)
        main_layout.addlayout(preview_layout)
        
        self.update_ui_state()

    # ---------- 逻辑功能 (保持不变) ----------

    def open_pdf(self):
        path, _ = qfiledialog.getopenfilename(self, "打开 pdf", "", "pdf files (*.pdf)")
        if not path: return
        
        try:
            if self.doc: self.doc.close()
            self.doc = fitz.open(path)
            self.pdf_path = path
            self.current_page = 0
            self.stamps_list = []
            
            self.lbl_info.settext(os.path.basename(path))
            self.refresh_preview()
            self.update_ui_state()
            
        except exception as e:
            qmessagebox.critical(self, "错误", f"无法打开文件:\n{str(e)}")

    def select_seal(self):
        path, _ = qfiledialog.getopenfilename(self, "选择印章图片", "", "images (*.png *.jpg *.jpeg *.bmp)")
        if not path: return
        
        self.txt_seal_path.settext(path)
        
        try:
            pil_img = image.open(path)
            if pil_img.mode != 'rgba':
                pil_img = pil_img.convert('rgba')
            
            byte_io = bytesio()
            pil_img.save(byte_io, format='png')
            self.seal_bytes = byte_io.getvalue()
            
            img_doc = fitz.open("png", self.seal_bytes)
            page = img_doc.load_page(0)
            self.seal_w_pt = page.rect.width
            self.seal_ratio = page.rect.height / page.rect.width
            img_doc.close()
            
            self.seal_pixmap = qpixmap(path)
            self.lbl_seal_preview.setpixmap(self.seal_pixmap.scaled(
                self.lbl_seal_preview.size(), qt.keepaspectratio, qt.smoothtransformation
            ))
            self.update_seal_preview_info()
            
        except exception as e:
            qmessagebox.critical(self, "图片错误", f"处理印章图片失败:\n{e}")

    def update_seal_preview_info(self):
        pass

    def add_stamp(self, pdf_x, pdf_y):
        if not self.doc or self.seal_bytes is none:
            qmessagebox.warning(self, "提示", "请先加载 pdf 和 印章图片!")
            return
            
        scale = self.spin_scale.value() / 100.0
        
        actual_w = self.seal_w_pt * scale
        actual_h = actual_w * self.seal_ratio
        
        stamp_record = {
            'page': self.current_page,
            'x': pdf_x,
            'y': pdf_y,
            'w_pt': actual_w,
            'h_pt': actual_h,
            'img_bytes': self.seal_bytes,
            'pixmap': self.seal_pixmap
        }
        
        self.stamps_list.append(stamp_record)
        self.refresh_preview()

    def undo_last_stamp(self):
        if not self.stamps_list: return
        removed = self.stamps_list.pop()
        if removed['page'] == self.current_page:
            self.refresh_preview()
        else:
            qmessagebox.information(self, "撤销", f"已撤销第 {removed['page']+1} 页上的印章")

    def clear_page_stamps(self):
        old_len = len(self.stamps_list)
        self.stamps_list = [s for s in self.stamps_list if s['page'] != self.current_page]
        if len(self.stamps_list) < old_len:
            self.refresh_preview()

    def refresh_preview(self):
        if not self.doc: return
        current_page_stamps = [s for s in self.stamps_list if s['page'] == self.current_page]
        self.preview.set_stamps(current_page_stamps)
        self.preview.load_page(self.doc, self.current_page)
        self.lbl_page.settext(f"{self.current_page + 1} / {len(self.doc)}")
        self.update_ui_state()

    def prev_page(self):
        if self.current_page > 0:
            self.current_page -= 1
            self.refresh_preview()
            # 翻页时重置滚动条到顶部
            self.scroll_area.verticalscrollbar().setvalue(0)

    def next_page(self):
        if self.doc and self.current_page < len(self.doc) - 1:
            self.current_page += 1
            self.refresh_preview()
            # 翻页时重置滚动条到顶部
            self.scroll_area.verticalscrollbar().setvalue(0)
            
    def update_ui_state(self):
        has_doc = self.doc is not none
        self.btn_prev.setenabled(has_doc and self.current_page > 0)
        self.btn_next.setenabled(has_doc and self.current_page < len(self.doc) - 1)

    def save_pdf(self):
        if not self.doc: return
        if not self.stamps_list:
            qmessagebox.information(self, "提示", "当前没有盖任何章,不需要保存。")
            return
            
        save_path, _ = qfiledialog.getsavefilename(self, "保存文件", self.pdf_path.replace(".pdf", "_stamped.pdf"), "pdf files (*.pdf)")
        if not save_path: return
        
        try:
            for stamp in self.stamps_list:
                page = self.doc[stamp['page']]
                rect_x0 = stamp['x'] - stamp['w_pt'] / 2
                rect_y0 = stamp['y'] - stamp['h_pt'] / 2
                rect_x1 = stamp['x'] + stamp['w_pt'] / 2
                rect_y1 = stamp['y'] + stamp['h_pt'] / 2
                rect = fitz.rect(rect_x0, rect_y0, rect_x1, rect_y1)
                page.insert_image(rect, stream=stamp['img_bytes'])
            
            self.doc.save(save_path)
            qmessagebox.information(self, "成功", f"文件已保存至:\n{save_path}")
            
            self.doc.close()
            self.doc = fitz.open(save_path)
            self.pdf_path = save_path
            self.stamps_list = []
            self.refresh_preview()
            
        except exception as e:
            qmessagebox.critical(self, "保存失败", str(e))

if __name__ == '__main__':
    qapplication.setattribute(qt.aa_enablehighdpiscaling)
    qapplication.setattribute(qt.aa_usehighdpipixmaps)
    
    app = qapplication(sys.argv)
    w = mainwindow()
    w.show()
    sys.exit(app.exec_())

八、扩展功能建议

  1. 批量盖章:支持在多个位置批量添加相同印章
  2. 印章库管理:保存常用的多个印章,方便快速选择
  3. 模板功能:保存常用的盖章位置模板
  4. 文字水印:除了图片印章,支持添加文字水印
  5. 多页操作:支持跨页复制印章位置

九、总结

通过这个项目,我们实现了:

  • 一个完整的桌面gui应用
  • pdf文件的解析和渲染
  • 精确的坐标定位系统
  • 图像与pdf的合成功能
  • 友好的用户交互界面

这个工具不仅实用,也是学习pyqt5图形界面编程和pdf处理的好例子。

到此这篇关于使用python开发一个桌面版pdf盖章工具的文章就介绍到这了,更多相关python pdf盖章工具内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com