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

作者:

文章目录
  • 这个工具的主要功能包括: 打开和预览PDF文件 加载印章图片(支持PNG、JPG等格式) 在PDF页面上精确点击添加印章 调整印章大小 撤销操作、清空页面印章 保存盖章后的PDF文件
  • 首先需要安装必要的Python库: pip install PyQt5 PyMuPDF pillow 注意:PyMuPDF是fitz模块的库,通过pip install PyMuPDF安装。
  • 工具界面分为三个主要区域: +—————-+——————————-+ | 左侧工具栏 | PDF预览区 | | | (带滚动条,支持长页面) | +—————-+——————————-+ | | 页面导航条 | +—————-+——————————-+ 左侧工具栏包含: 文件操作区域:打开PDF文件 印章设置区域:选择印章图片、调整大小 编辑控制区域:撤销、清空操作 保存按钮
  • 打开PDF文件:点击"打开PDF"按钮选择文件 加载印章:点击"选择印章"按钮,支持PNG、JPG等格式 调整印章大小:通过缩放比例旋钮调整(5%-500%) 添加印章:在PDF预览区域点击鼠标左键 编辑操作:可撤销最后一个印章或清空当前页 保存文件:点击"另存为PDF"保存盖章后的文件
  • 坐标系统转换:需要在屏幕像素坐标、PDF点坐标和图像显示坐标之间进行精确转换 图像处理:使用Pillow处理印章图片的透明通道 内存管理:及时关闭PDF文档,避免内存泄漏 用户体验:添加撤销功能、实时预览和错误提示
  • 以下是完整的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_())
  • 批量盖章:支持在多个位置批量添加相同印章 印章库管理:保存常用的多个印章,方便快速选择 模板功能:保存常用的盖章位置模板 文字水印:除了图片印章,支持添加文字水印 多页操作:支持跨页复制印章位置
  • 通过这个项目,我们实现了: 一个完整的桌面GUI应用 PDF文件的解析和渲染 精确的坐标定位系统 图像与PDF的合成功能 友好的用户交互界面 这个工具不仅实用,也是学习PyQt5图形界面编程和PDF处理的好例子。 到此这篇关于使用Python开发一个桌面版PDF盖章工具的文章就介绍到这了,更多相关Python PDF盖章工具内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客! 您可能感兴趣的文章: Python+PyQt5实现PDF转图片工具的深度解析 基于python实现PDF分页和管理工具开发详解 基于Python实现进阶版PDF合并/拆分工具 基于Python开发批量提取PDF书签的实用工具 使用Python打造一个将PDF转换为文本的工具
  • 目录
    • 一、项目概述
    • 二、环境准备
    • 三、核心代码解析
      • 1. PDF预览编辑画布类
      • 2. 印章添加逻辑
      • 3. 滚动预览的修复
      • 4. 保存PDF文件
    • 四、界面布局设计
      • 五、使用步骤
        • 六、技术要点
          • 七、完整代码
            • 八、扩展功能建议
              • 九、总结

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

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

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

                pip install PyQt5 PyMuPDF pillow

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

                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页面上绘制已添加的印章

                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()

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

                # 创建滚动区域
                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)

                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盖章工具内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!

                您可能感兴趣的文章:

                • Python+PyQt5实现PDF转图片工具的深度解析
                • 基于python实现PDF分页和管理工具开发详解
                • 基于Python实现进阶版PDF合并/拆分工具
                • 基于Python开发批量提取PDF书签的实用工具
                • 使用Python打造一个将PDF转换为文本的工具

                站内搜索