Explorar o código

add:在pdf上添加签名和公章

zoie hai 8 horas
pai
achega
95b520553f
Modificáronse 4 ficheiros con 449 adicións e 4 borrados
  1. 46 2
      README.md
  2. 162 0
      app.py
  3. 241 0
      drag_signature.py
  4. 0 2
      extract_table.py

+ 46 - 2
README.md

@@ -1,4 +1,48 @@
 pdf文件处理
 
-add_signature 添加公章
-add_watermark 添加水印
+## 功能列表
+
+1. **add_signature** - 添加公章(自动识别签名位置)
+2. **add_watermark** - 添加水印
+3. **drag_signature** - 拖放签名/电子章到PDF(新增)
+
+## 拖放签名功能使用说明
+
+### 后端API
+
+**接口地址**: `/drag_signature`
+
+**请求方法**: POST
+
+**请求参数**:
+```json
+{
+    "pdf_url": "PDF文件URL",
+    "signatures": [
+        {
+            "signature_url": "签名/电子章图片URL",
+            "page_num": 页码(从0开始),
+            "x": X坐标(PDF点,相对于页面左上角),
+            "y": Y坐标(PDF点,相对于页面左上角),
+            "width": 宽度(可选,PDF点,默认43mm),
+            "height": 高度(可选,PDF点,默认按比例)
+        }
+    ]
+}
+```
+
+**返回结果**:
+```json
+{
+    "success": true,
+    "pdf_url": "新生成的PDF文件URL"
+}
+```
+
+
+### 注意事项
+
+- 确保后端服务运行在 `http://localhost:6500`(可在HTML中修改API地址)
+- PDF和签名图片都需要通过URL访问(支持跨域)
+- 坐标系统:前端使用屏幕像素坐标,会自动转换为PDF点坐标
+- 默认签名尺寸约为43mm(可在代码中调整)

+ 162 - 0
app.py

@@ -10,6 +10,7 @@ from add_signature import find_signature_positions, add_signature_to_pdf
 from add_watermark import add_watermark_to_pdf
 from extract_table import extract_temp_time, extract_pdf_table_to_excel, extract_temp_by_datetime_pattern, allowed_file, \
     safe_filename, extract_temperature_data_from_pdf, extract_data_from_pdf_5
+from drag_signature import add_signature_at_position, add_multiple_signatures
 from lib import Qiniu
 from werkzeug.utils import secure_filename
 from flask_cors import CORS
@@ -19,6 +20,11 @@ UPLOAD_FOLDER = tempfile.gettempdir()
 app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
 CORS(app)
 
+# 确保temp目录存在
+temp_dir = "./temp"
+if not os.path.exists(temp_dir):
+    os.makedirs(temp_dir)
+
 
 @app.route('/add_signature', methods=['POST'])
 def add_signature():
@@ -166,6 +172,162 @@ def extract_table():
         return jsonify({'error': 'Invalid file type'}), 400
 
 
+@app.route('/upload_pdf', methods=['POST'])
+def upload_pdf():
+    """
+    上传PDF文件到服务器并返回URL
+    """
+    try:
+        if 'file' not in request.files:
+            return jsonify({"error": "没有文件"}), 400
+        
+        file = request.files['file']
+        
+        if file.filename == '':
+            return jsonify({"error": "文件名为空"}), 400
+        
+        if not file.filename.lower().endswith('.pdf'):
+            return jsonify({"error": "只支持PDF文件"}), 400
+        
+        # 保存临时文件
+        filename = secure_filename(file.filename)
+        temp_pdf = os.path.join("./temp", f"upload_{uuid.uuid4()}.pdf")
+        file.save(temp_pdf)
+        
+        # 上传到七牛云
+        file_key = f"UpImage/{uuid.uuid4()}.pdf"
+        pdf_url = Qiniu.upload_to_qiniu(temp_pdf, file_key)
+        
+        # 清理临时文件
+        try:
+            os.remove(temp_pdf)
+        except:
+            pass
+        
+        return jsonify({
+            "success": True,
+            "pdf_url": pdf_url
+        })
+        
+    except Exception as e:
+        return jsonify({
+            "error": str(e),
+            "success": False,
+        }), 500
+
+
+@app.route('/upload_signature', methods=['POST'])
+def upload_signature():
+    """
+    上传签名图片文件到服务器并返回URL
+    """
+    try:
+        if 'file' not in request.files:
+            return jsonify({"error": "没有文件"}), 400
+        
+        file = request.files['file']
+        
+        if file.filename == '':
+            return jsonify({"error": "文件名为空"}), 400
+        
+        # 检查文件类型(支持常见的图片格式)
+        allowed_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
+        filename_lower = file.filename.lower()
+        if not any(filename_lower.endswith(ext) for ext in allowed_extensions):
+            return jsonify({"error": "只支持图片文件(png, jpg, jpeg, gif, bmp, webp)"}), 400
+        
+        # 保存临时文件
+        filename = secure_filename(file.filename)
+        # 获取文件扩展名
+        file_ext = os.path.splitext(filename)[1] or '.png'
+        temp_signature = os.path.join("./temp", f"upload_signature_{uuid.uuid4()}{file_ext}")
+        file.save(temp_signature)
+        
+        # 上传到七牛云
+        file_key = f"UpImage/{uuid.uuid4()}{file_ext}"
+        signature_url = Qiniu.upload_to_qiniu(temp_signature, file_key)
+        
+        # 清理临时文件
+        try:
+            os.remove(temp_signature)
+        except:
+            pass
+        
+        return jsonify({
+            "success": True,
+            "signature_url": signature_url
+        })
+        
+    except Exception as e:
+        return jsonify({
+            "error": str(e),
+            "success": False,
+        }), 500
+
+
+@app.route('/drag_signature', methods=['POST'])
+def drag_signature():
+    """
+    在PDF上添加签名或电子章(支持拖放)
+    请求参数:
+    {
+        "pdf_url": "PDF文件URL",
+        "signatures": [
+            {
+                "signature_url": "签名/电子章URL",
+                "page_num": 页码(从0开始),
+                "x": X坐标(相对于页面左上角,单位:点),
+                "y": Y坐标(相对于页面左上角,单位:点),
+                "width": 宽度(可选,单位:点),
+                "height": 高度(可选,单位:点)
+            }
+        ]
+    }
+    """
+    try:
+        # 获取请求参数
+        data = request.get_json()
+        pdf_url = data.get('pdf_url')
+        signatures = data.get('signatures', [])
+        
+        if not pdf_url:
+            return jsonify({"error": "缺少pdf_url参数"}), 400
+        
+        if not signatures or len(signatures) == 0:
+            return jsonify({"error": "缺少signatures参数或签名列表为空"}), 400
+        
+        # 下载PDF文件
+        local_pdf = Qiniu.download_file(pdf_url)
+        
+        # 生成带签名的PDF
+        output_pdf = os.path.join("./temp", f"drag_signature_{uuid.uuid4()}.pdf")
+        
+        # 添加多个签名
+        add_multiple_signatures(local_pdf, output_pdf, signatures)
+        
+        # 上传到七牛云
+        file_key = f"UpImage/{uuid.uuid4()}.pdf"
+        new_pdf_url = Qiniu.upload_to_qiniu(output_pdf, file_key)
+        
+        # 清理临时文件
+        try:
+            os.remove(local_pdf)
+            os.remove(output_pdf)
+        except:
+            pass
+        
+        return jsonify({
+            "success": True,
+            "pdf_url": new_pdf_url
+        })
+        
+    except Exception as e:
+        return jsonify({
+            "error": str(e),
+            "success": False,
+        }), 500
+
+
 if __name__ == '__main__':
     print("项目地址:", os.path.dirname(__file__))
     if len(sys.argv) != 2:

+ 241 - 0
drag_signature.py

@@ -0,0 +1,241 @@
+import io
+import os
+import tempfile
+from reportlab.lib.pagesizes import A4
+from PyPDF2 import PdfReader, PdfWriter
+from reportlab.pdfgen import canvas
+from PIL import Image
+import logging
+import requests
+
+# 配置日志
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+def download_image_from_url(url, output_path):
+    """从URL下载图片到本地"""
+    response = requests.get(url, stream=True)
+    response.raise_for_status()
+    
+    # 判断文件类型
+    content_type = response.headers.get('content-type', '').lower()
+    if 'image' in content_type:
+        with open(output_path, 'wb') as f:
+            for chunk in response.iter_content(chunk_size=8192):
+                f.write(chunk)
+        return output_path
+    else:
+        raise Exception(f"不支持的图片类型: {content_type}")
+
+
+def add_signature_at_position(input_pdf, output_pdf, signature_url, page_num, x, y, width=None, height=None):
+    """
+    在PDF的指定位置添加签名或电子章
+    
+    参数:
+    - input_pdf: 输入PDF文件路径
+    - output_pdf: 输出PDF文件路径
+    - signature_url: 签名/电子章的URL(可以是图片)
+    - page_num: 页码(从0开始)
+    - x: X坐标(相对于PDF页面左下角,单位:点)
+    - y: Y坐标(相对于PDF页面左下角,单位:点)
+    - width: 签名宽度(可选,单位:点,默认43mm)
+    - height: 签名高度(可选,单位:点,默认按比例)
+    """
+    try:
+        # 下载签名图片
+        temp_signature = os.path.join(tempfile.gettempdir(), f"temp_signature_{os.urandom(8).hex()}")
+        try:
+            # 尝试下载为图片
+            download_image_from_url(signature_url, temp_signature)
+            signature_path = temp_signature
+        except Exception as e:
+            logger.error(f"下载签名失败: {e}")
+            raise Exception(f"无法下载签名图片: {e}")
+        
+        # 读取PDF
+        reader = PdfReader(input_pdf)
+        writer = PdfWriter()
+        a4_width, a4_height = A4  # (595.2, 841.68)
+        
+        # 确保页码有效
+        if page_num >= len(reader.pages):
+            raise Exception(f"页码超出范围,PDF共有{len(reader.pages)}页")
+        
+        # 处理每一页
+        for page_index in range(len(reader.pages)):
+            page = reader.pages[page_index]
+            
+            # 只在指定页面添加签名
+            if page_index == page_num:
+                # 创建覆盖层
+                packet = io.BytesIO()
+                can = canvas.Canvas(packet, pagesize=A4)
+                
+                # 打开图片并获取尺寸
+                try:
+                    img = Image.open(signature_path)
+                    img_width, img_height = img.size
+                    aspect_ratio = img_height / img_width
+                    
+                    # 设置签名尺寸
+                    if width is None:
+                        new_width = 43 * (72 / 25.4)  # 43mm转换为点
+                    else:
+                        new_width = width
+                    
+                    if height is None:
+                        new_height = new_width * aspect_ratio
+                    else:
+                        new_height = height
+                    
+                    # 转换坐标:前端传入的坐标通常是相对于页面左上角,需要转换为左下角
+                    # y坐标需要从页面顶部翻转到底部
+                    pdf_x = x
+                    pdf_y = a4_height - y - new_height  # 翻转Y坐标
+                    
+                    # 确保坐标在页面范围内
+                    pdf_x = max(0, min(pdf_x, a4_width - new_width))
+                    pdf_y = max(0, min(pdf_y, a4_height - new_height))
+                    
+                    # 绘制图片
+                    can.drawImage(signature_path, pdf_x, pdf_y, width=new_width, height=new_height, mask='auto')
+                    can.save()
+                    
+                    # 合并覆盖层到PDF页面
+                    packet.seek(0)
+                    overlay_pdf = PdfReader(packet)
+                    page.merge_page(overlay_pdf.pages[0])
+                except Exception as e:
+                    logger.error(f"处理图片失败: {e}")
+                    raise Exception(f"处理签名图片失败: {e}")
+            
+            writer.add_page(page)
+        
+        # 保存PDF
+        with open(output_pdf, "wb") as f:
+            writer.write(f)
+        
+        # 清理临时文件
+        try:
+            if os.path.exists(signature_path):
+                os.remove(signature_path)
+        except:
+            pass
+        
+        logger.info(f"成功添加签名到PDF: {output_pdf}")
+        return True
+        
+    except Exception as e:
+        logger.error(f"添加签名失败: {e}")
+        # 清理临时文件
+        try:
+            if 'signature_path' in locals() and os.path.exists(signature_path):
+                os.remove(signature_path)
+        except:
+            pass
+        raise
+
+
+def add_multiple_signatures(input_pdf, output_pdf, signatures):
+    """
+    在PDF的多个位置添加签名
+    
+    参数:
+    - input_pdf: 输入PDF文件路径
+    - output_pdf: 输出PDF文件路径
+    - signatures: 签名列表,每个元素为字典:
+        {
+            'signature_url': '签名URL',
+            'page_num': 页码(从0开始),
+            'x': X坐标,
+            'y': Y坐标,
+            'width': 宽度(可选),
+            'height': 高度(可选)
+        }
+    """
+    try:
+        # 读取PDF
+        reader = PdfReader(input_pdf)
+        writer = PdfWriter()
+        
+        # 按页码分组签名
+        signatures_by_page = {}
+        for sig in signatures:
+            page_num = sig['page_num']
+            if page_num not in signatures_by_page:
+                signatures_by_page[page_num] = []
+            signatures_by_page[page_num].append(sig)
+        
+        # 处理每一页
+        for page_index in range(len(reader.pages)):
+            page = reader.pages[page_index]
+            
+            # 如果这一页有签名,添加所有签名
+            if page_index in signatures_by_page:
+                # 获取PDF页面实际尺寸(单位:点)
+                page_box = page.mediabox
+                page_width = float(page_box.width)
+                page_height = float(page_box.height)
+                
+                # 创建覆盖层,使用PDF页面实际尺寸
+                packet = io.BytesIO()
+                can = canvas.Canvas(packet, pagesize=(page_width, page_height))
+                
+                for sig in signatures_by_page[page_index]:
+                    try:
+                        # 下载签名图片
+                        temp_signature = os.path.join(tempfile.gettempdir(), f"temp_sig_{os.urandom(8).hex()}")
+                        download_image_from_url(sig['signature_url'], temp_signature)
+                        
+                        # 打开图片并获取尺寸
+                        img = Image.open(temp_signature)
+                        img_width, img_height = img.size
+                        aspect_ratio = img_height / img_width
+                        
+                        # 设置签名尺寸(前端传入的已经是点坐标)
+                        new_width = sig.get('width', 43 * (72 / 25.4))
+                        new_height = sig.get('height', new_width * aspect_ratio)
+                        
+                        # 转换坐标:前端传入的y坐标是相对于页面左上角,需要转换为左下角
+                        # PDF坐标系:左下角为原点,y向上
+                        pdf_x = sig['x']
+                        pdf_y = page_height - sig['y'] - new_height
+                        
+                        # 确保坐标在页面范围内
+                        pdf_x = max(0, min(pdf_x, page_width - new_width))
+                        pdf_y = max(0, min(pdf_y, page_height - new_height))
+                        
+                        # 绘制图片
+                        can.drawImage(temp_signature, pdf_x, pdf_y, width=new_width, height=new_height, mask='auto')
+                        
+                        # 清理临时文件
+                        os.remove(temp_signature)
+                    except Exception as e:
+                        logger.error(f"添加签名失败: {e}")
+                        if os.path.exists(temp_signature):
+                            try:
+                                os.remove(temp_signature)
+                            except:
+                                pass
+                        continue
+                
+                can.save()
+                packet.seek(0)
+                overlay_pdf = PdfReader(packet)
+                page.merge_page(overlay_pdf.pages[0])
+            
+            writer.add_page(page)
+        
+        # 保存PDF
+        with open(output_pdf, "wb") as f:
+            writer.write(f)
+        
+        logger.info(f"成功添加多个签名到PDF: {output_pdf}")
+        return True
+        
+    except Exception as e:
+        logger.error(f"添加多个签名失败: {e}")
+        raise
+

+ 0 - 2
extract_table.py

@@ -3,8 +3,6 @@ import pdfplumber
 import re
 from datetime import datetime
 
-import tabula
-
 ALLOWED_EXTENSIONS = {'pdf'}