drag_signature.py 12 KB

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