用python做PDF压缩
虽然现在有很多成熟的工具了,但是就是想自己捣鼓一下
在网上找了一圈,发现实现方法有两种,一种是需要联网上传(TinyPNG的API)压缩的,一种是本地用python算法
这里采用的是本地,基本的思路是
1、提取PDF内容,保存成图片
2、压缩图片
3、图片合成PDF
4、新增加入多进程和队列的方式,加快压缩
联网上传的我觉得直接用i love pdf这个网页,挺好用的,就不知道安不安全。。。
Compress PDF online. Same PDF quality less file size (ilovepdf.com)
这里参考了两篇博客的代码
(2条消息) Python实现PDF文件压缩_xinxinbupp的博客-优快云博客
(2条消息) Python-从PDF中提取图片、压缩PDF_xinRCNN的博客-优快云博客
但是感觉压缩出来的图片不是很理想,就想找一个图片压缩算法替换上去
在网上找到一个python的图片压缩算法,说是**“可能是最接近微信朋友圈的图片压缩算法”**
依赖安装
先安装库 fitz,再安装库pymupdf,地址:https://github.com/pymupdf/PyMuPDF/
pip install fitz
pip install PyMuPDF
pip install easygui # 用来弹出文件选择框的,thinker的话会弹出两个窗口怪怪的
缝合修改
CV大法用上
# -*- coding:utf-8 -*-
# author: peng
# file: mypdf.py
# time: 2021/9/8 17:47
# desc:压缩PDF,对纯图片的PDF效果效果较好,有文字内容的可能会比较模糊,推荐高质量的压缩
import fitz
from PIL import Image
import os
from shutil import copyfile, rmtree
from math import ceil
from time import strftime, localtime, time
import easygui as g
from functools import wraps
# 时间计数装饰器,func如果有return值,必须返回才能有值
def runtime(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(strftime("%Y-%m-%d %H:%M:%S", localtime()))
start = time()
func_return = func(*args, **kwargs)
end = time()
print(func.__name__, args[-1], args[-2], " spend time ", end - start, " sec")
return func_return
return wrapper
class Luban(object):
def __init__(self, quality, ignoreBy=102400):
self.ignoreBy = ignoreBy
self.quality = quality
def setPath(self, path):
self.path = path
def setTargetDir(self, foldername="target"):
self.dir, self.filename = os.path.split(self.path)
self.targetDir = os.path.join(self.dir, foldername)
if not os.path.exists(self.targetDir):
os.makedirs(self.targetDir)
self.targetPath = os.path.join(self.targetDir, "c_" + self.filename)
def load(self):
self.img = Image.open(self.path)
if self.img.mode == "RGB":
self.type = "JPEG"
elif self.img.mode == "RGBA":
self.type = "PNG"
else: # 其他的图片就转成JPEG
self.img = self.img.convert("RGB")
self.type = "JPEG"
def computeScale(self):
# 计算缩小的倍数
srcWidth, srcHeight = self.img.size
srcWidth = srcWidth + 1 if srcWidth % 2 == 1 else srcWidth
srcHeight = srcHeight + 1 if srcHeight % 2 == 1 else srcHeight
longSide = max(srcWidth, srcHeight)
shortSide = min(srcWidth, srcHeight)
scale = shortSide / longSide
if (scale <= 1 and scale > 0.5625):
if (longSide < 1664):
return 1
elif (longSide < 4990):
return 2
elif (longSide > 4990 and longSide < 10240):
return 4
else:
return max(1, longSide // 1280)
elif (scale <= 0.5625 and scale > 0.5):
return max(1, longSide // 1280)
else:
return ceil(longSide / (1280.0 / scale))
def compress(self):
self.setTargetDir()
# 先调整大小,再调整品质
if os.path.getsize(self.path) <= self.ignoreBy:
copyfile(self.path, self.targetPath)
else:
self.load()
scale = self.computeScale()
srcWidth, srcHeight = self.img.size
cache = self.img.resize((srcWidth // scale, srcHeight // scale),
Image.ANTIALIAS)
cache.save(self.targetPath, self.type, quality=self.quality)
# 提取成图片
def covert2pic(doc, totaling, zooms=None):
'''
:param totaling: pdf的页数
:param zooms: 值越大,分辨率越高,文件越清晰,列表内两个浮点数,每个尺寸的缩放系数,默认为分辨率的2倍
:return:
'''
if zooms is None:
zooms = [2.0, 2.0]
if os.path.exists('.pdf'): # 临时文件,需为空
rmtree('.pdf')
os.mkdir('.pdf')
print(f"pdf页数为 {totaling} \n创建临时文件夹.....")
for pg in range(totaling):
page = doc[pg]
print(f"\r{page}", end="")
trans = fitz.Matrix(*zooms).preRotate(0) # 0为旋转角度
pm = page.getPixmap(matrix=trans, alpha=False)
lurl = '.pdf/%s.jpg' % str(pg + 1)
pm.writePNG(lurl) #保存
doc.close()
# 图片合成pdf
def pic2pdf(obj, ratio, totaling):
doc = fitz.open()
compressor = Luban(quality=ratio)
for pg in range(totaling):
path = '.pdf/%s.jpg' % str(pg + 1)
compressor.setPath(path)
compressor.compress()
print(f"\r 插入图片 {pg + 1}/{totaling} 中......", end="")
img = '.pdf/target/c_%s.jpg' % str(pg + 1)
imgdoc = fitz.open(img) # 打开图片
pdfbytes = imgdoc.convertToPDF() # 使用图片创建单页的 PDF
os.remove(img)
imgpdf = fitz.open("pdf", pdfbytes)
doc.insertPDF(imgpdf) # 将当前页插入文档
if os.path.exists(obj): # 若pdf文件存在先删除
os.remove(obj)
doc.save(obj) # 保存pdf文件
doc.close()
@runtime
def pdfz(doc, obj, ratio, totaling):
covert2pic(doc, totaling)
pic2pdf(obj, ratio, totaling)
def pic_quality():
print("输入压缩等级1~3:")
comp_level = input("压缩等级(1=高画质50%,2=中画质70%,3=低画质80%):(输入数字并按回车键)")
# 用字典模拟Switch分支,注意输入的值是str类型
ratio = {'1': 40, '2': 20, '3': 10}
# 字典中没有则默认 低画质压缩
return ratio.get(comp_level, 10)
if __name__ == "__main__":
print("请选择需要压缩的PDF文件")
while True:
'''打开选择文件夹对话框'''
filepath = g.fileopenbox(title=u"选择PDF", filetypes=['*.pdf'])
if filepath == None:
input("还未选择文件,输入任意键继续.......")
continue
else:
filedir, filename = os.path.split(filepath)
print(u'已选中文件【%s】' % (filename))
if filename.endswith(".pdf") == False:
input("选择的文件类型不对,输入任意键继续.......")
continue
ratio = pic_quality()
obj = "new_" + filename
doc = fitz.open(filepath)
totaling = doc.pageCount
pdfz(doc, obj, ratio, totaling)
rmtree('.pdf')
oldsize = os.stat(filepath).st_size
newsize = os.stat(obj).st_size
print('压缩结果 %.2f M >>>> %.2f M'%(oldsize/(1024 * 1024),newsize/(1024 * 1024)))
input(f"压缩已完成,文件保存在改程序目录下{filedir},如需继续压缩请按任意键")
效果
压缩出来的结果:
当然,不是所有的pdf压缩都会变小。。。本身pdf文件小的,处理出来后可能会变大,原因应该是图片提取保存的时候图片文件变大,所有压缩进去的时候也会变大。
新增多进程
在使用过多线程时,发现速度没什么提升,因为这个程序CPU和IO都有占用,大家可以测试在多线程和多进程下哪个速度快就采用哪个
Python中单线程、多线程和多进程的效率对比实验
Python并发编程之多进程
别的博客中说到:“需要注意的是队列中Queue.Queue是线程安全的,但并不是进程安全,所以多进程一般使用线程、进程安全的multiprocessing.Queue(),而使用这个Queue如果数据量太大会导致进程莫名卡住(绝壁大坑来的),需要不断地消费。”
这里对代码的修改部分有几个小地方,提取图片的参数变为pdf路径(因为doc参数在进程调用时会出错),队列
转pdf内部加入判断队列为空和取操作,这样就简单实现了生产者-消费者模式
from multiprocessing import Process, Queue
# 提取成图片
def covert2pic(filepath, qpaper, zooms=None):
'''
:param filepath: pdf文件的位置
:param qpaper: 数据页的队列
:param zooms: 值越大,分辨率越高,文件越清晰,列表内两个浮点数,每个尺寸的缩放系数,默认为分辨率的2倍
:return:
'''
doc = fitz.open(filepath)
totaling = doc.pageCount
if zooms is None:
zooms = [2.0, 2.0]
if path.exists('.pdf'): # 临时文件,需为空
rmtree('.pdf')
mkdir('.pdf')
print(f"pdf页数为 {totaling} \n创建临时文件夹.....")
for pg in range(totaling):
page = doc[pg]
print(f"\r{page}", end="")
trans = fitz.Matrix(*zooms).preRotate(0) # 0为旋转角度
pm = page.getPixmap(matrix=trans, alpha=False)
lurl = '.pdf/%s.jpg' % str(pg + 1)
pm.writePNG(lurl) # 保存
qpaper.put(pg)
doc.close()
# 图片合成pdf
def pic2pdf(obj, ratio, qpaper, totaling):
doc2 = fitz.open()
compressor = Luban(quality=ratio)
for pg in range(totaling):
picpath = '.pdf/%s.jpg' % str(pg + 1)
compressor.setPath(picpath)
while qpaper.empty():
# 如果队列为空,则循环等待
pass
qpaper.get()
compressor.compress()
print(f"\r 插入图片 {pg + 1}/{totaling} 中......", end="")
img = '.pdf/target/c_%s.jpg' % str(pg + 1)
imgdoc = fitz.open(img) # 打开图片
pdfbytes = imgdoc.convertToPDF() # 使用图片创建单页的 PDF
remove(img)
imgpdf = fitz.open("pdf", pdfbytes)
doc2.insertPDF(imgpdf) # 将当前页插入文档
if path.exists(obj): # 若pdf文件存在先删除
remove(obj)
doc2.save(obj) # 保存pdf文件
doc2.close()
@runtime
def pdfz(filepath, obj, ratio, totaling):
# 参数传递变为filepath
qpaper = Queue() # 创建队列
threads = []
#read_thread = threading.Thread(target=covert2pic, args=(doc, totaling, qpaper))
read_thread = Process(target=covert2pic, args=(filepath, qpaper))
'''
多进程这里传参数不一定成功,参数需要可以序列化才行,这里如果传doc的变量,会报错WeakValueDictionary.__init__.<locals>.remove
'''
threads.append(read_thread)
#write_thread = threading.Thread(target=pic2pdf, args=(obj, ratio, totaling, qpaper))
write_thread = Process(target=pic2pdf, args=(obj, ratio, qpaper, totaling))
threads.append(write_thread)
for th in threads:
th.start() # 开始执行线程
for th in threads:
th.join()
print("结束")
最终多进程会比单进程节约大约30%的时间(节约了处理图片和生成pdf的时间,就是函数pic2pdf)
缺点
-
使用的不是GUI界面,没那么美观,感觉也没必要吧哈哈哈
-
提取文件的时候比较慢,想着多线程但是不会,可能要对文件分块,还是算了
-
用pyinstaller(本人在conda创建的虚拟环境下python2.6打包出来小一点)打包出来,文件大小差不多30M,而且打包之后运行就没那么流畅了,而且有个坑点
执行过程在cmd黑窗口中打印信息时,有时,一不小心鼠标点到了黑窗口里,程序就会暂停,要回车才能继续,网上的说法是
“或许是cmd启用了快速编辑模式导致的问题。在快速编辑模式,鼠标点击cmd窗口时,可以直接选择窗口里的文本,如果此时cmd中运行的进程需要在cmd窗口中输出信息,这个进程就会被暂停,直到按下回车。”
解决方法:Python 解决cmd窗口鼠标点击后挂起不执行问题(禁止快速编辑模式)_浅醉樱花雨的专栏-优快云博客
加入代码:但是没用。。。输入的时候会输入不了,暂时不加了
可以看下边的方法,只对cmd设置而已