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