|
|
@@ -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
|
|
|
+
|