主页 > 创业  > 

用PyMuPDF和Pillow打造PDF超级工具

用PyMuPDF和Pillow打造PDF超级工具

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

依赖库

要使用这个工具,您需要安装以下 Python 库: 

PyMuPDF (fitz) 

Pillow (PIL) 

您可以使用以下命令安装这些库: 

pip install PyMuPDF Pillow

 

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

 

代码 import argparse import os import fitz  # PyMuPDF import io from PIL import Image def try_remove_pdf_password(input_path, password=None):     doc = fitz.open(input_path)     if doc.is_encrypted:         if password is None:             # 尝试无密码解密             if doc.authenticate(""):                 print("PDF文件已成功解密(无需密码)")                 return input_path             else:                 raise ValueError("PDF文件已加密,需要密码")         else:             if doc.authenticate(password):                 output_path = input_path.replace('.pdf', '_decrypted.pdf')                 doc.save(output_path)                 print(f"已解密的PDF保存到: {output_path}")                 return output_path             else:                 raise ValueError("提供的密码不正确")     else:         print("PDF文件未加密")         return input_path def parse_page_ranges(page_range, total_pages):     if not page_range:         return list(range(1, total_pages + 1))  # 如果没有指定页码,返回所有页面     pages = set()     ranges = page_range.split(',')     for r in ranges:         if '-' in r:             start, end = map(int, r.split('-'))             pages.update(range(start, min(end + 1, total_pages + 1)))         else:             page = int(r)             if 1 <= page <= total_pages:                 pages.add(page)     return sorted(list(pages)) def process_pdf(input_path, page_range, output_path, dpi=300, split_pages=False, password=None):     doc = fitz.open(input_path)     if doc.is_encrypted:         if not doc.authenticate(password):             raise ValueError("密码不正确")     total_pages = len(doc)     pages_to_process = parse_page_ranges(page_range, total_pages)     _, file_extension = os.path.splitext(output_path)     output_format = file_extension[1:].lower()     if output_format in ['jpg', 'jpeg', 'png']:         zoom = dpi / 72  # 默认 DPI 为 72         mat = fitz.Matrix(zoom, zoom)         if split_pages:             base_name, ext = os.path.splitext(output_path)             for page_num in pages_to_process:                 page = doc[page_num - 1]                 pix = page.get_pixmap(matrix=mat, alpha=False)                 img = Image.open(io.BytesIO(pix.tobytes()))                 page_output_path = f"{base_name}_{page_num}{ext}"                 img.save(page_output_path)                 print(f"输出文件已保存到: {page_output_path}")         else:             images = []             for page_num in pages_to_process:                 page = doc[page_num - 1]                 pix = page.get_pixmap(matrix=mat, alpha=False)                 img = Image.open(io.BytesIO(pix.tobytes()))                 images.append(img)             if len(images) == 1:                 images[0].save(output_path)             else:                 # 计算总高度和最大宽度                 total_height = sum(img.height for img in images)                 max_width = max(img.width for img in images)                 # 如果总高度超过限制,分割图像                 max_height = 65000  # PIL的最大支持高度                 if total_height > max_height:                     parts = []                     current_height = 0                     current_part = []                     for img in images:                         if current_height + img.height > max_height:                             parts.append(current_part)                             current_part = [img]                             current_height = img.height                         else:                             current_part.append(img)                             current_height += img.height                     if current_part:                         parts.append(current_part)                     # 保存每个部分                     base_name, ext = os.path.splitext(output_path)                     for i, part in enumerate(parts):                         part_height = sum(img.height for img in part)                         combined_img = Image.new('RGB', (max_width, part_height), (255, 255, 255))                         y_offset = 0                         for img in part:                             combined_img.paste(img, (0, y_offset))                             y_offset += img.height                         part_output_path = f"{base_name}_part{i + 1}{ext}"                         combined_img.save(part_output_path)                         print(f"输出文件(部分 {i + 1})已保存到: {part_output_path}")                 else:                     # 如果总高度没有超过限制,按原方式处理                     combined_img = Image.new('RGB', (max_width, total_height), (255, 255, 255))                     y_offset = 0                     for img in images:                         combined_img.paste(img, (0, y_offset))                         y_offset += img.height                     combined_img.save(output_path)                     print(f"输出文件已保存到: {output_path}")     elif output_format == 'pdf':         new_doc = fitz.open()         for page_num in pages_to_process:             new_doc.insert_pdf(doc, from_page=page_num - 1, to_page=page_num - 1)         new_doc.save(output_path)         print(f"输出文件已保存到: {output_path}")     else:         raise ValueError(f"不支持的输出格式: {output_format}")     doc.close() def merge_pdfs(input_pdfs, output_path):     merged_doc = fitz.open()     for pdf_path in input_pdfs:         with fitz.open(pdf_path) as doc:             merged_doc.insert_pdf(doc)     merged_doc.save(output_path)     print(f"合并的PDF文件已保存到: {output_path}") def extract_images_from_pdf(pdf_path, page_range_str, output_directory):     doc = fitz.open(pdf_path)     total_pages = len(doc)     pages_to_process = parse_page_ranges(page_range_str, total_pages)     if not os.path.exists(output_directory):         os.makedirs(output_directory)     for page_num in pages_to_process:         page = doc[page_num - 1]         image_list = page.get_images()         for img_index, img in enumerate(image_list):             xref = img[0]             base_image = doc.extract_image(xref)             image_bytes = base_image["image"]             # 获取图片格式             image_format = base_image["ext"]             # 使用 PIL 打开图片             image = Image.open(io.BytesIO(image_bytes))             # 保存图片             image_filename = f"page_{page_num}_image_{img_index + 1}.{image_format}"             image_path = os.path.join(output_directory, image_filename)             image.save(image_path)             print(f"已保存图片: {image_path}")     print(f"所有图片已提取到目录: {output_directory}") def encrypt_pdf(input_path, output_path, user_password, owner_password=None):     doc = fitz.open(input_path)     if owner_password is None:         owner_password = user_password     encryption_method = fitz.PDF_ENCRYPT_AES_256     permissions = int(         fitz.PDF_PERM_ACCESSIBILITY         | fitz.PDF_PERM_PRINT         | fitz.PDF_PERM_COPY         | fitz.PDF_PERM_ANNOTATE     )     doc.save(         output_path,         encryption=encryption_method,         user_pw=user_password,         owner_pw=owner_password,         permissions=permissions     )     print(f"已加密的PDF保存到: {output_path}") def decrypt_pdf(input_path, output_path, password):     doc = fitz.open(input_path)     if doc.is_encrypted:         if doc.authenticate(password):             doc.save(output_path)             print(f"已解密的PDF保存到: {output_path}")         else:             raise ValueError("密码不正确")     else:         print("PDF文件未加密")         doc.save(output_path)         print(f"PDF文件已复制到: {output_path}") def main():     parser = argparse.ArgumentParser(description="处理PDF文件:选择页面并输出为JPG、PNG或PDF,或合并多个PDF,或提取图片")     subparsers = parser.add_subparsers(dest='command', help='可用的命令')     # 处理单个PDF的命令     process_parser = subparsers.add_parser('process', help='处理单个PDF文件')     process_parser.add_argument("input_pdf", help="输入PDF文件的路径")     process_parser.add_argument("page_range", help="要处理的页面范围,例如 '1,3-5,7-9'")     process_parser.add_argument("output", help="输出文件的路径(支持.jpg, .jpeg, .png, .pdf)")     process_parser.add_argument("-d", "--dpi", type=int, default=300, help="图像DPI (仅用于jpg和png输出,默认: 300)")     process_parser.add_argument("-p", "--password", help="PDF密码(如果PDF加密)")     process_parser.add_argument("-s", "--split-pages", action='store_true', help="按每页生成单独的JPG或PNG文件")     # 合并PDF的命令     merge_parser = subparsers.add_parser('merge', help='合并多个PDF文件')     merge_parser.add_argument("input_pdfs", nargs='+', help="要合并的PDF文件路径列表")     merge_parser.add_argument("output", help="输出的合并PDF文件路径")     # 提取图片的命令     extract_parser = subparsers.add_parser('extract', help='从PDF中提取图片')     extract_parser.add_argument("input_pdf", help="输入PDF文件的路径")     extract_parser.add_argument("page_range", help="要提取图片的页面范围,例如 '1,3-5,7-9'")     extract_parser.add_argument("output_directory", help="保存提取图片的目录路径")     extract_parser.add_argument("-p", "--password", help="PDF密码(如果PDF加密)")     # 加密PDF的命令     encrypt_parser = subparsers.add_parser('encrypt', help='加密PDF文件')     encrypt_parser.add_argument("input_pdf", help="输入PDF文件的路径")     encrypt_parser.add_argument("output_pdf", help="输出加密PDF文件的路径")     encrypt_parser.add_argument("user_password", help="用户密码")     encrypt_parser.add_argument("-o", "--owner_password", help="所有者密码(如果不提供,将与用户密码相同)")     # 解密PDF的命令     decrypt_parser = subparsers.add_parser('decrypt', help='解密PDF文件')     decrypt_parser.add_argument("input_pdf", help="输入加密PDF文件的路径")     decrypt_parser.add_argument("output_pdf", help="输出解密PDF文件的路径")     decrypt_parser.add_argument("password", help="PDF密码")     args = parser.parse_args()     if args mand == 'process':         try:             decrypted_pdf_path = try_remove_pdf_password(args.input_pdf, args.password)             process_pdf(decrypted_pdf_path, args.page_range, args.output, args.dpi, args.split_pages)             if decrypted_pdf_path != args.input_pdf:                 os.remove(decrypted_pdf_path)         except ValueError as e:             if "PDF文件已加密,需要密码" in str(e):                 password = input("请输入PDF密码: ")                 try:                     decrypted_pdf_path = try_remove_pdf_password(args.input_pdf, password)                     process_pdf(decrypted_pdf_path, args.page_range, args.output, args.dpi, args.split_pages)                 except ValueError as e:                     print(f"处理过程中出错: {str(e)}")             else:                 print(f"处理过程中出错: {str(e)}")         except Exception as e:             print(f"处理过程中出错: {str(e)}")     elif args mand == 'merge':         try:             merge_pdfs(args.input_pdfs, args.output)         except Exception as e:             print(f"合并PDF过程中出错: {str(e)}")     elif args mand == 'extract':         try:             decrypted_pdf_path = try_remove_pdf_password(args.input_pdf, args.password)             extract_images_from_pdf(decrypted_pdf_path, args.page_range, args.output_directory)             if decrypted_pdf_path != args.input_pdf:                 os.remove(decrypted_pdf_path)         except ValueError as e:             if "PDF文件已加密,需要密码" in str(e):                 password = input("请输入PDF密码: ")                 try:                     decrypted_pdf_path = try_remove_pdf_password(args.input_pdf, password)                     extract_images_from_pdf(decrypted_pdf_path, args.page_range, args.output_directory)                 except ValueError as e:                     print(f"处理过程中出错: {str(e)}")             else:                 print(f"处理过程中出错: {str(e)}")         except Exception as e:             print(f"处理过程中出错: {str(e)}")     elif args mand == 'encrypt':         try:             encrypt_pdf(args.input_pdf, args.output_pdf, args.user_password, args.owner_password)         except Exception as e:             print(f"加密PDF过程中出错: {str(e)}")     elif args mand == 'decrypt':         try:             decrypt_pdf(args.input_pdf, args.output_pdf, args.password)         except Exception as e:             print(f"解密PDF过程中出错: {str(e)}") if __name__ == "__main__":     main() 使用方法 1. 处理 PDF 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 文件 

示例: python pdf_tool.py process input.pdf 1,3-5 output.png -d 200 -s

 

2. 合并 PDF python pdf_tool.py merge 参数说明:

: 要合并的 PDF 文件路径列表 

: 输出的合并 PDF 文件路径 

示例: python pdf_tool.py merge file1.pdf file2.pdf file3.pdf merged.pdf

 

3. 提取图片 python pdf_tool.py extract[options] 参数说明:

: 输入 PDF 文件的路径 

: 要提取图片的页面范围,例如 '1,3-5,7-9' 

: 保存提取图片的目录路径 

选项:

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

示例: python pdf_tool.py extract document.pdf 1-5 ./images -p mypassword

 

4. 加密 PDF python pdf_tool.py encrypt[options] 参数说明:

: 输入 PDF 文件的路径 

: 输出加密 PDF 文件的路径 

: 用户密码 

选项:

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

示例: python pdf_tool.py encrypt input.pdf encrypted.pdf userpass -o ownerpass

 

5. 解密 PDF python pdf_tool.py decrypt 参数说明:

: 输入加密 PDF 文件的路径 

: 输出解密 PDF 文件的路径 

: PDF 密码 

示例: python pdf_tool.py decrypt encrypted.pdf decrypted.pdf mypassword


相关阅读

基于 DeepSeek+AutoGen 的智能体协作系统

AI 时代,如何用 Python 脚本轻松搞定 PDF 需求?

DeepSeek V3 vs R1:到底哪个更适合你?全面对比来袭

深度揭秘:如何用一句话让 DeepSeek 优化你的代码

手把手教你用 DeepSeek 和 VSCode 开启 AI 辅助编程之旅

零基础小白的编程入门:用 AI 工具轻松加功能、改代码

标签:

用PyMuPDF和Pillow打造PDF超级工具由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“用PyMuPDF和Pillow打造PDF超级工具