前几天完成《 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 Imagedef 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_pathelse: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_pathelse:raise ValueError("提供的密码不正确")else:print("PDF文件未加密")return input_pathdef 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 为 72mat = 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 = 0current_part = []for img in images:if current_height + img.height > max_height:parts.append(current_part)current_part = [img]current_height = img.heightelse:current_part.append(img)current_height += img.heightif 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 = 0for img in part:combined_img.paste(img, (0, y_offset))y_offset += img.heightpart_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 = 0for img in images:combined_img.paste(img, (0, y_offset))y_offset += img.heightcombined_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_passwordencryption_method = fitz.PDF_ENCRYPT_AES_256permissions = 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.command == '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.command == 'merge':try:merge_pdfs(args.input_pdfs, args.output)except Exception as e:print(f"合并PDF过程中出错: {str(e)}")elif args.command == '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.command == '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.command == '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 工具轻松加功能、改代码