1.背景:
在办公场景中,不可避免地会遇到大型PDF文件的情况。其一是某些PDF文件由图片扫描而来,本身占据空间确实较大;其二是这种PDF文件一般页码较多,且大概率为书籍扫描而来。这就导致在某些文件占用空间大小有限制或者传输速率有限的情况下对这类大型PDF文件操作会比较困难,故而引出了PDF压缩的需求。
2.可行工具:
- PDF压缩网站(首推):https://www.ilovepdf.com/compress_pdf
压缩效果很不错,功能也全,比较适合用于处理少数PDF文件,缺点是压缩质量不能完全自定义 - 写一个PDF压缩的Python脚本,用脚本执行,兼具单文件出里和文件夹批处理功能,如下↓↓↓
3.代码:
先放源码,源码如下,若有调参需要自行根据注释修改即可;
若不想使用脚本,而想通过python程序执行,自行在程序内写入路径即可:
#!/usr/bin/python3
# Author: Zeed
# 11/09/2023
import os
import sys
import fitz
import argparse
from io import BytesIO
from PIL import Image
from tqdm import tqdm
def pdf_compress(input_path, output_folder_path, _dpi=200, _type="png", method=0):
"""
本方法适用于纯图片型(包含文字型图片)的PDF文档压缩,可复制型的文字类的PDF文档不建议使用本方法
:param input_path: 文件名全路径
:param _dpi: 转化后图片的像素(范围72-600),默认150,想要清晰点,可以设置成高一点,这个参数直接影响PDF文件大小
测试: 纯图片PDF文件(即单个页面就是一个图片,内容不可复制)
300dpi,压缩率约为30-50%,即原来大小的30-50%,基本无损,看不出来压缩后导致的分辨率差异
200dpi,压缩率约为20-30%,轻微有损
150dpi,压缩率约为5-10%,有损,但是基本不影响图片形文字的阅读
:param _type: 保存格式,默认为png,其他:JPEG, PNM, PGM, PPM, PBM, PAM, PSD, PS
:param method: int,图像压缩方法,只支持下面3个选项,默认值是0
0 : `MEDIANCUT` (median cut)
1 : `MAXCOVERAGE` (maximum coverage)
2 : `FASTOCTREE` (fast octree)
:return:
"""
merges = []
_file = None
with fitz.open(input_path) as doc:
for i, page in tqdm(
enumerate(doc.pages(), start=0), desc=f"{input_path} 压缩处理中,请耐心等待"
):
img = page.get_pixmap(dpi=_dpi) # 将PDF页面转化为图片
img_bytes = img.pil_tobytes(format=_type) # 将图片转为为bytes对象
image = Image.open(BytesIO(img_bytes)) # 将bytes对象转为PIL格式的图片对象
if i == 0:
_file = image # 取第一张图片用于创建PDF文档的首页
pix: Image.Image = image.quantize(colors=256, method=method).convert(
"RGB"
) # 单张图片压缩处理
merges.append(pix) # 组装pdf
pdf_name = os.path.split(input_path)[-1] # 提取单纯的文件名称,带pdf后缀,但是没有绝对路径
output_path = os.path.join(output_folder_path, pdf_name.rsplit(".")[-2])
_file.save(
f"{output_path}_by_{_dpi}dpi.pdf",
"pdf", # 用PIL自带的功能保存为PDF格式文件
save_all=True,
append_images=merges[1:],
)
print(f"{input_path} was compressed into {output_path}_by_{_dpi}dpi.pdf!")
def pdf_folder_compress(
input_path, output_folder_path, _dpi=200, _type="png", method=0
):
item_paths = os.listdir(input_path) # 提取所有文件信息
item_paths = [
item_path
for item_path in item_paths
if item_path.split(".")[-1].lower() == "pdf"
] # 找出所有的PDF文件
for item_path in item_paths: # 对每个pdf操作
item_input_path = os.path.join(input_path, item_path) # 这里一定要注意路径的拼接
pdf_compress(item_input_path, output_folder_path, _dpi=200)
def main():
"""
根据传入的是单pdf文件还是pdf文件夹选用不同的压缩方法
"""
output_folder_path = "output_pdf"
# 判断输出文件夹是否存在
if os.path.exists(output_folder_path): # 输出文件夹存在
print("output_pdf folder already existed.")
else: # 输出文件夹不存在
os.mkdir(output_folder_path)
print("output_pdf folder has been created.")
# 创建接受参数的argparse类
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"-f", "--file", help="Relative or absolute path of the input PDF file name"
)
parser.add_argument("-r", "--route", help="Relative or absolute path PDF folder")
args = parser.parse_args()
if not args.file and not args.route:
print("错误:请输入pdf的文件名或pdf所在文件夹")
sys.exit(1)
if args.file and args.route:
print("错误:要么压缩单一文件,要么批量压缩")
if args.file:
pdf_compress(args.file, output_folder_path)
if args.route:
pdf_folder_compress(args.route, output_folder_path)
if __name__ == "__main__":
main()
先在环境下装好两个库
4.说明
- 需要在相应的Python环境中安装fitz(即PyMuPDF库)和tqdm库。
pip install PyMuPDF
pip install tqdm
- 脚本采取命令行驱动,在进入相应的python环境后,根据命令格式不同对单个pdf文件或者包含多个pdf文件的文件夹进行压缩。最终所有压缩过的pdf文件将储存在同目录下的output_pdf文件夹中。
不懂虚拟环境的同学还是直接根据源程序修改吧,我放在最后了。会用普通终端的也可以试一试命令行调用
# 进入虚拟环境
conda activate {env}
# 进入脚本所在文件夹,或者直接绝对路径调用
cd C:\{script}
# 压缩单个PDF
python pdf_compress.py -f {file_name}
# 批量压缩PDF
python pdf_compress.py -r {folder_path}
- 该脚本的压缩原理是PDF转图片,图片降低分辨率后再转PDF,默认分辨率是200,目前最大能做到500M文件压缩为30M文件的效果。如有更高或者更低的压缩需要,进入脚本修改相应dpi即可。
def pdf_compress(input_path, output_folder_path, **_dpi=200**, _type="png", method=0):
5.非脚本运行
代码如下↓
#!/usr/bin/python3
# Author: Zeed
# 11/09/2023
import os
import sys
import fitz
import argparse
from io import BytesIO
from PIL import Image
from tqdm import tqdm
def pdf_compress(input_path, output_folder_path, _dpi=200, _type="png", method=0):
'''
本方法适用于纯图片型(包含文字型图片)的PDF文档压缩,可复制型的文字类的PDF文档不建议使用本方法
:param input_path: 文件名全路径
:param _dpi: 转化后图片的像素(范围72-600),默认150,想要清晰点,可以设置成高一点,这个参数直接影响PDF文件大小
测试: 纯图片PDF文件(即单个页面就是一个图片,内容不可复制)
300dpi,压缩率约为30-50%,即原来大小的30-50%,基本无损,看不出来压缩后导致的分辨率差异
200dpi,压缩率约为20-30%,轻微有损
150dpi,压缩率约为5-10%,有损,但是基本不影响图片形文字的阅读
:param _type: 保存格式,默认为png,其他:JPEG, PNM, PGM, PPM, PBM, PAM, PSD, PS
:param method: int,图像压缩方法,只支持下面3个选项,默认值是0
0 : `MEDIANCUT` (median cut)
1 : `MAXCOVERAGE` (maximum coverage)
2 : `FASTOCTREE` (fast octree)
:return:
'''
merges = []
_file = None
with fitz.open(input_path) as doc:
for i, page in tqdm(enumerate(doc.pages(), start=0) , desc=f"{input_path} 压缩处理中,请耐心等待"):
img = page.get_pixmap(dpi=_dpi) # 将PDF页面转化为图片
img_bytes = img.pil_tobytes(format=_type) # 将图片转为为bytes对象
image = Image.open(BytesIO(img_bytes)) # 将bytes对象转为PIL格式的图片对象
if i == 0:
_file = image # 取第一张图片用于创建PDF文档的首页
pix: Image.Image = image.quantize(colors=256, method=method).convert('RGB') # 单张图片压缩处理
merges.append(pix) # 组装pdf
pdf_name = os.path.split(input_path)[-1] # 提取单纯的文件名称,带pdf后缀,但是没有绝对路径
output_path = os.path.join(output_folder_path,pdf_name.rsplit('.')[-2])
_file.save(f"{output_path}_by_{_dpi}dpi.pdf",
"pdf", # 用PIL自带的功能保存为PDF格式文件
save_all=True,
append_images=merges[1:])
print(f"{input_path} was compressed into {output_path}_by_{_dpi}dpi.pdf!")
def pdf_folder_compress(input_path, output_folder_path, _dpi=200, _type="png", method=0):
item_paths = os.listdir(input_path) # 提取所有文件信息
item_paths = [item_path for item_path in item_paths if item_path.split('.')[-1].lower() == 'pdf'] # 找出所有的PDF文件
for item_path in item_paths: # 对每个pdf操作
item_input_path = os.path.join(input_path,item_path) # 这里一定要注意路径的拼接
pdf_compress(item_input_path, output_folder_path, _dpi=200)
def main():
"""
根据传入的是单pdf文件还是pdf文件夹选用不同的压缩方法
"""
input_path = 'input_path' # 改这里
output_folder_path = 'output_pdf'
# 判断输出文件夹是否存在
if os.path.exists(output_folder_path): # 输出文件夹存在
print("output_pdf folder already existed.")
else: # 输出文件夹不存在,创建一个文件夹
os.mkdir(output_folder_path)
print("output_pdf folder has been created.")
if input_path.rsplit('.')[-1]== 'pdf': # 判断是否是单个pdf文件
pdf_compress(input_path, output_folder_path)
else:
pdf_folder_compress(input_path, output_folder_path)
if __name__ == "__main__":
main()