drag_signature.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import io
  2. import os
  3. import tempfile
  4. from reportlab.lib.pagesizes import A4
  5. from PyPDF2 import PdfReader, PdfWriter
  6. from reportlab.pdfgen import canvas
  7. from PIL import Image
  8. import logging
  9. import requests
  10. # 配置日志
  11. logging.basicConfig(level=logging.INFO)
  12. logger = logging.getLogger(__name__)
  13. def download_image_from_url(url, output_path):
  14. """从URL下载图片到本地"""
  15. response = requests.get(url, stream=True)
  16. response.raise_for_status()
  17. # 判断文件类型
  18. content_type = response.headers.get('content-type', '').lower()
  19. if 'image' in content_type:
  20. with open(output_path, 'wb') as f:
  21. for chunk in response.iter_content(chunk_size=8192):
  22. f.write(chunk)
  23. return output_path
  24. else:
  25. raise Exception(f"不支持的图片类型: {content_type}")
  26. def add_signature_at_position(input_pdf, output_pdf, signature_url, page_num, x, y, width=None, height=None):
  27. """
  28. 在PDF的指定位置添加签名或电子章
  29. 参数:
  30. - input_pdf: 输入PDF文件路径
  31. - output_pdf: 输出PDF文件路径
  32. - signature_url: 签名/电子章的URL(可以是图片)
  33. - page_num: 页码(从0开始)
  34. - x: X坐标(相对于PDF页面左下角,单位:点)
  35. - y: Y坐标(相对于PDF页面左下角,单位:点)
  36. - width: 签名宽度(可选,单位:点,默认43mm)
  37. - height: 签名高度(可选,单位:点,默认按比例)
  38. """
  39. try:
  40. # 下载签名图片
  41. temp_signature = os.path.join(tempfile.gettempdir(), f"temp_signature_{os.urandom(8).hex()}")
  42. try:
  43. # 尝试下载为图片
  44. download_image_from_url(signature_url, temp_signature)
  45. signature_path = temp_signature
  46. except Exception as e:
  47. logger.error(f"下载签名失败: {e}")
  48. raise Exception(f"无法下载签名图片: {e}")
  49. # 读取PDF
  50. reader = PdfReader(input_pdf)
  51. writer = PdfWriter()
  52. a4_width, a4_height = A4 # (595.2, 841.68)
  53. # 确保页码有效
  54. if page_num >= len(reader.pages):
  55. raise Exception(f"页码超出范围,PDF共有{len(reader.pages)}页")
  56. # 处理每一页
  57. for page_index in range(len(reader.pages)):
  58. page = reader.pages[page_index]
  59. # 只在指定页面添加签名
  60. if page_index == page_num:
  61. # 创建覆盖层
  62. packet = io.BytesIO()
  63. can = canvas.Canvas(packet, pagesize=A4)
  64. # 打开图片并获取尺寸
  65. try:
  66. img = Image.open(signature_path)
  67. img_width, img_height = img.size
  68. aspect_ratio = img_height / img_width
  69. # 设置签名尺寸
  70. if width is None:
  71. new_width = 43 * (72 / 25.4) # 43mm转换为点
  72. else:
  73. new_width = width
  74. if height is None:
  75. new_height = new_width * aspect_ratio
  76. else:
  77. new_height = height
  78. # 转换坐标:前端传入的坐标通常是相对于页面左上角,需要转换为左下角
  79. # y坐标需要从页面顶部翻转到底部
  80. pdf_x = x
  81. pdf_y = a4_height - y - new_height # 翻转Y坐标
  82. # 确保坐标在页面范围内
  83. pdf_x = max(0, min(pdf_x, a4_width - new_width))
  84. pdf_y = max(0, min(pdf_y, a4_height - new_height))
  85. # 绘制图片
  86. can.drawImage(signature_path, pdf_x, pdf_y, width=new_width, height=new_height, mask='auto')
  87. can.save()
  88. # 合并覆盖层到PDF页面
  89. packet.seek(0)
  90. overlay_pdf = PdfReader(packet)
  91. page.merge_page(overlay_pdf.pages[0])
  92. except Exception as e:
  93. logger.error(f"处理图片失败: {e}")
  94. raise Exception(f"处理签名图片失败: {e}")
  95. writer.add_page(page)
  96. # 保存PDF
  97. with open(output_pdf, "wb") as f:
  98. writer.write(f)
  99. # 清理临时文件
  100. try:
  101. if os.path.exists(signature_path):
  102. os.remove(signature_path)
  103. except:
  104. pass
  105. logger.info(f"成功添加签名到PDF: {output_pdf}")
  106. return True
  107. except Exception as e:
  108. logger.error(f"添加签名失败: {e}")
  109. # 清理临时文件
  110. try:
  111. if 'signature_path' in locals() and os.path.exists(signature_path):
  112. os.remove(signature_path)
  113. except:
  114. pass
  115. raise
  116. def add_multiple_signatures(input_pdf, output_pdf, signatures):
  117. """
  118. 在PDF的多个位置添加签名
  119. 参数:
  120. - input_pdf: 输入PDF文件路径
  121. - output_pdf: 输出PDF文件路径
  122. - signatures: 签名列表,每个元素为字典:
  123. {
  124. 'signature_url': '签名URL',
  125. 'page_num': 页码(从0开始),
  126. 'x': X坐标,
  127. 'y': Y坐标,
  128. 'width': 宽度(可选),
  129. 'height': 高度(可选)
  130. }
  131. """
  132. try:
  133. # 读取PDF
  134. reader = PdfReader(input_pdf)
  135. writer = PdfWriter()
  136. # 按页码分组签名
  137. signatures_by_page = {}
  138. for sig in signatures:
  139. page_num = sig['page_num']
  140. if page_num not in signatures_by_page:
  141. signatures_by_page[page_num] = []
  142. signatures_by_page[page_num].append(sig)
  143. # 处理每一页
  144. for page_index in range(len(reader.pages)):
  145. page = reader.pages[page_index]
  146. # 如果这一页有签名,添加所有签名
  147. if page_index in signatures_by_page:
  148. # 获取PDF页面实际尺寸(单位:点)
  149. page_box = page.mediabox
  150. page_width = float(page_box.width)
  151. page_height = float(page_box.height)
  152. # 创建覆盖层,使用PDF页面实际尺寸
  153. packet = io.BytesIO()
  154. can = canvas.Canvas(packet, pagesize=(page_width, page_height))
  155. for sig in signatures_by_page[page_index]:
  156. try:
  157. # 下载签名图片
  158. temp_signature = os.path.join(tempfile.gettempdir(), f"temp_sig_{os.urandom(8).hex()}")
  159. download_image_from_url(sig['signature_url'], temp_signature)
  160. # 打开图片并获取尺寸
  161. img = Image.open(temp_signature)
  162. img_width, img_height = img.size
  163. aspect_ratio = img_height / img_width
  164. # 设置签名尺寸(前端传入的已经是点坐标)
  165. new_width = sig.get('width', 43 * (72 / 25.4))
  166. new_height = sig.get('height', new_width * aspect_ratio)
  167. # 转换坐标:前端传入的y坐标是相对于页面左上角,需要转换为左下角
  168. # PDF坐标系:左下角为原点,y向上
  169. pdf_x = sig['x']
  170. pdf_y = page_height - sig['y'] - new_height
  171. # 确保坐标在页面范围内
  172. pdf_x = max(0, min(pdf_x, page_width - new_width))
  173. pdf_y = max(0, min(pdf_y, page_height - new_height))
  174. # 绘制图片
  175. can.drawImage(temp_signature, pdf_x, pdf_y, width=new_width, height=new_height, mask='auto')
  176. # 清理临时文件
  177. os.remove(temp_signature)
  178. except Exception as e:
  179. logger.error(f"添加签名失败: {e}")
  180. if os.path.exists(temp_signature):
  181. try:
  182. os.remove(temp_signature)
  183. except:
  184. pass
  185. continue
  186. can.save()
  187. packet.seek(0)
  188. overlay_pdf = PdfReader(packet)
  189. page.merge_page(overlay_pdf.pages[0])
  190. writer.add_page(page)
  191. # 保存PDF
  192. with open(output_pdf, "wb") as f:
  193. writer.write(f)
  194. logger.info(f"成功添加多个签名到PDF: {output_pdf}")
  195. return True
  196. except Exception as e:
  197. logger.error(f"添加多个签名失败: {e}")
  198. raise