用 PyMuPDF 和 Pillow 打造 PDF 超等工具

打印 上一主题 下一主题

主题 904|帖子 904|积分 2712

前几天完成《 AI 时代,怎样用 Python 脚本轻松搞定 PDF 需求?》,有朋友反馈利用到 poppler 这个工具照旧有点麻烦,能不能就安装 python 库就可以办理,我让 AI 根据需求,重写了一版本,包括处理 PDF 页面、合并 PDF、提取图片、加密和解密 PDF 文件。 


依赖库

要利用这个工具,您需要安装以下 Python 库: 
PyMuPDF (fitz) 
Pillow (PIL) 
您可以利用以下命令安装这些库: 
  1. pip install PyMuPDF Pillow
复制代码
 
功能概述



  • 处理 PDF:选择特定页面并转换为图片或新的 PDF
  • 合并 PDF:将多个 PDF 文件合并为一个
  • 提取图片:从 PDF 中提取图片
  • 加密 PDF:为 PDF 文件添加密码保护
  • 解密 PDF:移除 PDF 文件的密码保护
 
代码

  1. import argparse
  2. import os
  3. import fitz  # PyMuPDF
  4. import io
  5. from PIL import Image
  6. def try_remove_pdf_password(input_path, password=None):
  7.     doc = fitz.open(input_path)
  8.     if doc.is_encrypted:
  9.         if password is None:
  10.             # 尝试无密码解密
  11.             if doc.authenticate(""):
  12.                 print("PDF文件已成功解密(无需密码)")
  13.                 return input_path
  14.             else:
  15.                 raise ValueError("PDF文件已加密,需要密码")
  16.         else:
  17.             if doc.authenticate(password):
  18.                 output_path = input_path.replace('.pdf', '_decrypted.pdf')
  19.                 doc.save(output_path)
  20.                 print(f"已解密的PDF保存到: {output_path}")
  21.                 return output_path
  22.             else:
  23.                 raise ValueError("提供的密码不正确")
  24.     else:
  25.         print("PDF文件未加密")
  26.         return input_path
  27. def parse_page_ranges(page_range, total_pages):
  28.     if not page_range:
  29.         return list(range(1, total_pages + 1))  # 如果没有指定页码,返回所有页面
  30.     pages = set()
  31.     ranges = page_range.split(',')
  32.     for r in ranges:
  33.         if '-' in r:
  34.             start, end = map(int, r.split('-'))
  35.             pages.update(range(start, min(end + 1, total_pages + 1)))
  36.         else:
  37.             page = int(r)
  38.             if 1 <= page <= total_pages:
  39.                 pages.add(page)
  40.     return sorted(list(pages))
  41. def process_pdf(input_path, page_range, output_path, dpi=300, split_pages=False, password=None):
  42.     doc = fitz.open(input_path)
  43.     if doc.is_encrypted:
  44.         if not doc.authenticate(password):
  45.             raise ValueError("密码不正确")
  46.     total_pages = len(doc)
  47.     pages_to_process = parse_page_ranges(page_range, total_pages)
  48.     _, file_extension = os.path.splitext(output_path)
  49.     output_format = file_extension[1:].lower()
  50.     if output_format in ['jpg', 'jpeg', 'png']:
  51.         zoom = dpi / 72  # 默认 DPI 为 72
  52.         mat = fitz.Matrix(zoom, zoom)
  53.         if split_pages:
  54.             base_name, ext = os.path.splitext(output_path)
  55.             for page_num in pages_to_process:
  56.                 page = doc[page_num - 1]
  57.                 pix = page.get_pixmap(matrix=mat, alpha=False)
  58.                 img = Image.open(io.BytesIO(pix.tobytes()))
  59.                 page_output_path = f"{base_name}_{page_num}{ext}"
  60.                 img.save(page_output_path)
  61.                 print(f"输出文件已保存到: {page_output_path}")
  62.         else:
  63.             images = []
  64.             for page_num in pages_to_process:
  65.                 page = doc[page_num - 1]
  66.                 pix = page.get_pixmap(matrix=mat, alpha=False)
  67.                 img = Image.open(io.BytesIO(pix.tobytes()))
  68.                 images.append(img)
  69.             if len(images) == 1:
  70.                 images[0].save(output_path)
  71.             else:
  72.                 # 计算总高度和最大宽度
  73.                 total_height = sum(img.height for img in images)
  74.                 max_width = max(img.width for img in images)
  75.                 # 如果总高度超过限制,分割图像
  76.                 max_height = 65000  # PIL的最大支持高度
  77.                 if total_height > max_height:
  78.                     parts = []
  79.                     current_height = 0
  80.                     current_part = []
  81.                     for img in images:
  82.                         if current_height + img.height > max_height:
  83.                             parts.append(current_part)
  84.                             current_part = [img]
  85.                             current_height = img.height
  86.                         else:
  87.                             current_part.append(img)
  88.                             current_height += img.height
  89.                     if current_part:
  90.                         parts.append(current_part)
  91.                     # 保存每个部分
  92.                     base_name, ext = os.path.splitext(output_path)
  93.                     for i, part in enumerate(parts):
  94.                         part_height = sum(img.height for img in part)
  95.                         combined_img = Image.new('RGB', (max_width, part_height), (255, 255, 255))
  96.                         y_offset = 0
  97.                         for img in part:
  98.                             combined_img.paste(img, (0, y_offset))
  99.                             y_offset += img.height
  100.                         part_output_path = f"{base_name}_part{i + 1}{ext}"
  101.                         combined_img.save(part_output_path)
  102.                         print(f"输出文件(部分 {i + 1})已保存到: {part_output_path}")
  103.                 else:
  104.                     # 如果总高度没有超过限制,按原方式处理
  105.                     combined_img = Image.new('RGB', (max_width, total_height), (255, 255, 255))
  106.                     y_offset = 0
  107.                     for img in images:
  108.                         combined_img.paste(img, (0, y_offset))
  109.                         y_offset += img.height
  110.                     combined_img.save(output_path)
  111.                     print(f"输出文件已保存到: {output_path}")
  112.     elif output_format == 'pdf':
  113.         new_doc = fitz.open()
  114.         for page_num in pages_to_process:
  115.             new_doc.insert_pdf(doc, from_page=page_num - 1, to_page=page_num - 1)
  116.         new_doc.save(output_path)
  117.         print(f"输出文件已保存到: {output_path}")
  118.     else:
  119.         raise ValueError(f"不支持的输出格式: {output_format}")
  120.     doc.close()
  121. def merge_pdfs(input_pdfs, output_path):
  122.     merged_doc = fitz.open()
  123.     for pdf_path in input_pdfs:
  124.         with fitz.open(pdf_path) as doc:
  125.             merged_doc.insert_pdf(doc)
  126.     merged_doc.save(output_path)
  127.     print(f"合并的PDF文件已保存到: {output_path}")
  128. def extract_images_from_pdf(pdf_path, page_range_str, output_directory):
  129.     doc = fitz.open(pdf_path)
  130.     total_pages = len(doc)
  131.     pages_to_process = parse_page_ranges(page_range_str, total_pages)
  132.     if not os.path.exists(output_directory):
  133.         os.makedirs(output_directory)
  134.     for page_num in pages_to_process:
  135.         page = doc[page_num - 1]
  136.         image_list = page.get_images()
  137.         for img_index, img in enumerate(image_list):
  138.             xref = img[0]
  139.             base_image = doc.extract_image(xref)
  140.             image_bytes = base_image["image"]
  141.             # 获取图片格式
  142.             image_format = base_image["ext"]
  143.             # 使用 PIL 打开图片
  144.             image = Image.open(io.BytesIO(image_bytes))
  145.             # 保存图片
  146.             image_filename = f"page_{page_num}_image_{img_index + 1}.{image_format}"
  147.             image_path = os.path.join(output_directory, image_filename)
  148.             image.save(image_path)
  149.             print(f"已保存图片: {image_path}")
  150.     print(f"所有图片已提取到目录: {output_directory}")
  151. def encrypt_pdf(input_path, output_path, user_password, owner_password=None):
  152.     doc = fitz.open(input_path)
  153.     if owner_password is None:
  154.         owner_password = user_password
  155.     encryption_method = fitz.PDF_ENCRYPT_AES_256
  156.     permissions = int(
  157.         fitz.PDF_PERM_ACCESSIBILITY
  158.         | fitz.PDF_PERM_PRINT
  159.         | fitz.PDF_PERM_COPY
  160.         | fitz.PDF_PERM_ANNOTATE
  161.     )
  162.     doc.save(
  163.         output_path,
  164.         encryption=encryption_method,
  165.         user_pw=user_password,
  166.         owner_pw=owner_password,
  167.         permissions=permissions
  168.     )
  169.     print(f"已加密的PDF保存到: {output_path}")
  170. def decrypt_pdf(input_path, output_path, password):
  171.     doc = fitz.open(input_path)
  172.     if doc.is_encrypted:
  173.         if doc.authenticate(password):
  174.             doc.save(output_path)
  175.             print(f"已解密的PDF保存到: {output_path}")
  176.         else:
  177.             raise ValueError("密码不正确")
  178.     else:
  179.         print("PDF文件未加密")
  180.         doc.save(output_path)
  181.         print(f"PDF文件已复制到: {output_path}")
  182. def main():
  183.     parser = argparse.ArgumentParser(description="处理PDF文件:选择页面并输出为JPG、PNG或PDF,或合并多个PDF,或提取图片")
  184.     subparsers = parser.add_subparsers(dest='command', help='可用的命令')
  185.     # 处理单个PDF的命令
  186.     process_parser = subparsers.add_parser('process', help='处理单个PDF文件')
  187.     process_parser.add_argument("input_pdf", help="输入PDF文件的路径")
  188.     process_parser.add_argument("page_range", help="要处理的页面范围,例如 '1,3-5,7-9'")
  189.     process_parser.add_argument("output", help="输出文件的路径(支持.jpg, .jpeg, .png, .pdf)")
  190.     process_parser.add_argument("-d", "--dpi", type=int, default=300, help="图像DPI (仅用于jpg和png输出,默认: 300)")
  191.     process_parser.add_argument("-p", "--password", help="PDF密码(如果PDF加密)")
  192.     process_parser.add_argument("-s", "--split-pages", action='store_true', help="按每页生成单独的JPG或PNG文件")
  193.     # 合并PDF的命令
  194.     merge_parser = subparsers.add_parser('merge', help='合并多个PDF文件')
  195.     merge_parser.add_argument("input_pdfs", nargs='+', help="要合并的PDF文件路径列表")
  196.     merge_parser.add_argument("output", help="输出的合并PDF文件路径")
  197.     # 提取图片的命令
  198.     extract_parser = subparsers.add_parser('extract', help='从PDF中提取图片')
  199.     extract_parser.add_argument("input_pdf", help="输入PDF文件的路径")
  200.     extract_parser.add_argument("page_range", help="要提取图片的页面范围,例如 '1,3-5,7-9'")
  201.     extract_parser.add_argument("output_directory", help="保存提取图片的目录路径")
  202.     extract_parser.add_argument("-p", "--password", help="PDF密码(如果PDF加密)")
  203.     # 加密PDF的命令
  204.     encrypt_parser = subparsers.add_parser('encrypt', help='加密PDF文件')
  205.     encrypt_parser.add_argument("input_pdf", help="输入PDF文件的路径")
  206.     encrypt_parser.add_argument("output_pdf", help="输出加密PDF文件的路径")
  207.     encrypt_parser.add_argument("user_password", help="用户密码")
  208.     encrypt_parser.add_argument("-o", "--owner_password", help="所有者密码(如果不提供,将与用户密码相同)")
  209.     # 解密PDF的命令
  210.     decrypt_parser = subparsers.add_parser('decrypt', help='解密PDF文件')
  211.     decrypt_parser.add_argument("input_pdf", help="输入加密PDF文件的路径")
  212.     decrypt_parser.add_argument("output_pdf", help="输出解密PDF文件的路径")
  213.     decrypt_parser.add_argument("password", help="PDF密码")
  214.     args = parser.parse_args()
  215.     if args.command == 'process':
  216.         try:
  217.             decrypted_pdf_path = try_remove_pdf_password(args.input_pdf, args.password)
  218.             process_pdf(decrypted_pdf_path, args.page_range, args.output, args.dpi, args.split_pages)
  219.             if decrypted_pdf_path != args.input_pdf:
  220.                 os.remove(decrypted_pdf_path)
  221.         except ValueError as e:
  222.             if "PDF文件已加密,需要密码" in str(e):
  223.                 password = input("请输入PDF密码: ")
  224.                 try:
  225.                     decrypted_pdf_path = try_remove_pdf_password(args.input_pdf, password)
  226.                     process_pdf(decrypted_pdf_path, args.page_range, args.output, args.dpi, args.split_pages)
  227.                 except ValueError as e:
  228.                     print(f"处理过程中出错: {str(e)}")
  229.             else:
  230.                 print(f"处理过程中出错: {str(e)}")
  231.         except Exception as e:
  232.             print(f"处理过程中出错: {str(e)}")
  233.     elif args.command == 'merge':
  234.         try:
  235.             merge_pdfs(args.input_pdfs, args.output)
  236.         except Exception as e:
  237.             print(f"合并PDF过程中出错: {str(e)}")
  238.     elif args.command == 'extract':
  239.         try:
  240.             decrypted_pdf_path = try_remove_pdf_password(args.input_pdf, args.password)
  241.             extract_images_from_pdf(decrypted_pdf_path, args.page_range, args.output_directory)
  242.             if decrypted_pdf_path != args.input_pdf:
  243.                 os.remove(decrypted_pdf_path)
  244.         except ValueError as e:
  245.             if "PDF文件已加密,需要密码" in str(e):
  246.                 password = input("请输入PDF密码: ")
  247.                 try:
  248.                     decrypted_pdf_path = try_remove_pdf_password(args.input_pdf, password)
  249.                     extract_images_from_pdf(decrypted_pdf_path, args.page_range, args.output_directory)
  250.                 except ValueError as e:
  251.                     print(f"处理过程中出错: {str(e)}")
  252.             else:
  253.                 print(f"处理过程中出错: {str(e)}")
  254.         except Exception as e:
  255.             print(f"处理过程中出错: {str(e)}")
  256.     elif args.command == 'encrypt':
  257.         try:
  258.             encrypt_pdf(args.input_pdf, args.output_pdf, args.user_password, args.owner_password)
  259.         except Exception as e:
  260.             print(f"加密PDF过程中出错: {str(e)}")
  261.     elif args.command == 'decrypt':
  262.         try:
  263.             decrypt_pdf(args.input_pdf, args.output_pdf, args.password)
  264.         except Exception as e:
  265.             print(f"解密PDF过程中出错: {str(e)}")
  266. if __name__ == "__main__":
  267.     main()
复制代码
利用方法

1. 处理 PDF

  1. python pdf_tool.py process[options]
复制代码


  • 参数说明:
: 输入 PDF 文件的路径 
: 要处理的页面范围,例如 '1,3-5,7-9' 
: 输出文件的路径(支持 。jpg, .jpeg, .png, .pdf)  


  • 选项:
-d, --dpi: 设置图像 DPI(默认:300) 
-p, --password: PDF 密码(如果 PDF 加密) 
-s, --split-pages: 按每页生成单独的 JPG 或 PNG 文件 


  • 示例:
  1. python pdf_tool.py process input.pdf 1,3-5 output.png -d 200 -s
复制代码
 
2. 合并 PDF

  1. python pdf_tool.py merge
复制代码


  • 参数说明:
: 要合并的 PDF 文件路径列表 
: 输出的合并 PDF 文件路径 


  • 示例:
  1. python pdf_tool.py merge file1.pdf file2.pdf file3.pdf merged.pdf
复制代码
 
3. 提取图片

  1. python pdf_tool.py extract[options]
复制代码


  • 参数说明:
: 输入 PDF 文件的路径 
: 要提取图片的页面范围,例如 '1,3-5,7-9' 
: 生存提取图片的目录路径 


  • 选项:
-p, --password: PDF 密码(如果 PDF 加密) 


  • 示例:
  1. python pdf_tool.py extract document.pdf 1-5 ./images -p mypassword
复制代码
 
4. 加密 PDF

  1. python pdf_tool.py encrypt[options]
复制代码


  • 参数说明:
: 输入 PDF 文件的路径 
: 输出加密 PDF 文件的路径 
: 用户密码 


  • 选项:
-o, --owner_password: 所有者密码(如果不提供,将与用户密码相同) 


  • 示例:
  1. python pdf_tool.py encrypt input.pdf encrypted.pdf userpass -o ownerpass
复制代码
 
5. 解密 PDF

  1. python pdf_tool.py decrypt
复制代码


  • 参数说明:
: 输入加密 PDF 文件的路径 
: 输出解密 PDF 文件的路径 
: PDF 密码 


  • 示例:
  1. python pdf_tool.py decrypt encrypted.pdf decrypted.pdf mypassword
复制代码


相关阅读
基于 DeepSeek+AutoGen 的智能体协作系统
AI 时代,怎样用 Python 脚本轻松搞定 PDF 需求?
DeepSeek V3 vs R1:到底哪个更恰当你?全面临比来袭
深度揭秘:怎样用一句话让 DeepSeek 优化你的代码
手把手教你用 DeepSeek 和 VSCode 开启 AI 辅助编程之旅
零基础小白的编程入门:用 AI 工具轻松加功能、改代码

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

郭卫东

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表