使用python实现PDF本地化压缩
作者:随心~稳心
用python做PDF压缩
虽然现在有很多成熟的工具了,但是就是想自己捣鼓一下
在网上找了一圈,发现实现方法有两种,一种是需要联网上传(TinyPNG的API)压缩的,一种是本地用python算法
这里采用的是本地,基本的思路是
- 1、提取PDF内容,保存成图片
- 2、压缩图片
- 3、图片合成PDF
- 4、新增加入多进程和队列的方式,加快压缩
联网上传的我觉得直接用i love pdf这个网页,挺好用的,就不知道安不安全。。。
但是感觉压缩出来的图片不是很理想,就想找一个图片压缩算法替换上去
在网上找到一个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文件小的,处理出来后可能会变大,原因应该是图片提取保存的时候图片文件变大,所有压缩进去的时候也会变大。
新增多进程
别的博客中说到:“需要注意的是队列中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实现PDF文件压缩
1. 原理PDF切分为图片,根据压缩率zoom压缩图片后保存本地;图片合成PDF
2. 依赖PyMuPDF包
pip install PyMuPDF
3. 代码
import fitz import os def covert2pic(zoom): if os.path.exists('.pdf'): # 临时文件,需为空 os.removedirs('.pdf') os.mkdir('.pdf') for pg in range(totaling): page = doc[pg] zoom = int(zoom) #值越大,分辨率越高,文件越清晰 rotate = int(0) print(page) trans = fitz.Matrix(zoom / 100.0, zoom / 100.0).preRotate(rotate) pm = page.getPixmap(matrix=trans, alpha=False) lurl='.pdf/%s.jpg' % str(pg+1) pm.writePNG(lurl) doc.close() def pic2pdf(obj): doc = fitz.open() for pg in range(totaling): img = '.pdf/%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): # 若文件存在先删除 os.remove(obj) doc.save(obj) # 保存pdf文件 doc.close() def pdfz(sor, obj, zoom): covert2pic(zoom) pic2pdf(obj) if __name__ == "__main__": sor = "source.pdf" # 需要压缩的PDF文件 obj = "new" + sor doc = fitz.open(sor) totaling = doc.pageCount zoom = 200 # 清晰度调节,缩放比率 pdfz(sor, obj, zoom) os.removedirs('.pdf')
4. 使用
- 脚本和要压缩的PDF需在同一路径下
- sor变量为需要压缩的文件
- zoom用于调整压缩率
- 压缩后使用PDF打印功能导出能够进一步压缩
Python从PDF中提取图片、压缩PDF
安装必要的库
先安装库 fitz,再安装库pymupdf,地址:https://github.com/pymupdf/PyMuPDF/
pip install fitz pip install pymupdf
源代码
第一个pdf2pic从pdf中提取jpg文件的部分引用了别人的代码
以下两行doc.引用的注意了,不然会报错
lenXREF = doc.xref_length() text = doc.xref_object(i) # 定义对象字符串
另外加入了重新调整过大的照片尺寸,和保存照片的质量,这里有个变量comp_ratio
im = im.resize((1376, y_s), Image.ANTIALIAS) im.save(pic_path_d, quality=comp_ratio)
import fitz import re import os from PIL import Image from tkinter import filedialog def pdf2pic(path, pic_path, comp_ratio): checkXO = r"/Type(?= */XObject)" # 使用正则表达式来查找图片 checkIM = r"/Subtype(?= */Image)" doc = fitz.open(path) # 打开pdf文件 imgcount = 0 # 图片计数 lenXREF = doc.xref_length() # 获取对象数量长度 # 打印PDF的信息 print("文件名:{}, 页数: {}, 对象: {}".format(path, len(doc), lenXREF - 1)) # 遍历每一个对象 for i in range(1, lenXREF): text = doc.xref_object(i) # 定义对象字符串 isXObject = re.search(checkXO, text) # 使用正则表达式查看是否是对象 isImage = re.search(checkIM, text) # 使用正则表达式查看是否是图片 if not isXObject or not isImage: # 如果不是对象也不是图片,则continue continue imgcount += 1 pix = fitz.Pixmap(doc, i) # 生成图像对象 new_name = "pic{}.jpg".format(imgcount) # 生成图片的名称 print(new_name) if pix.n < 5: # 如果pix.n<5,可以直接存为PNG pic_path_d = os.path.join(pic_path, new_name) pix.writeImage(os.path.join(pic_path, new_name)) im = Image.open(pic_path_d) x, y = im.size if x > 1376: y_s = int(y * 1376 / x) im = im.resize((1376, y_s), Image.ANTIALIAS) im.save(pic_path_d, quality=comp_ratio) else: # 否则先转换CMYK pix0 = fitz.Pixmap(fitz.csRGB, pix) pix0.writeImage(os.path.join(pic_path, new_name)) pix0 = None pix = None # 释放资源 print("提取了{}张图片".format(imgcount)) os.startfile(pic_path)
下面这个rea是用来将文件夹内的照片重新组合为pdf文件
def rea(path, pdf_name): file_list = os.listdir(path) pic_name = [] im_list = [] for x in file_list: if "jpg" in x or 'png' in x or 'jpeg' in x: pic_name.append(x) pic_name.sort() new_pic = [] for x in pic_name: if "jpg" in x: new_pic.append(x) for x in pic_name: if "png" in x: new_pic.append(x) print("hec", new_pic) im1 = Image.open(os.path.join(path, new_pic[0])) new_pic.pop(0) for i in new_pic: img = Image.open(os.path.join(path, i)) # im_list.append(Image.open(i)) if img.mode == "RGBA": img = img.convert('RGB') im_list.append(img) else: im_list.append(img) im1.save(pdf_name, "PDF", resolution=100.0, save_all=True, append_images=im_list) print("输出文件名称:", pdf_name) def pdf_out(): print('功能完善中')
主程序中随意加了一些判断,如压缩等级1、2、3等。
if __name__ == '__main__': print("Hello world!请先输入压缩等级1~3,然后在弹出的对话框中选择需要压缩的文件") comp_level = input("压缩等级(1=高画质,2=中画质,3=低画质):(输入数字并按回车键)") ratio = 10 if comp_level == "1": ratio = 20 elif comp_level == "2": ratio = 10 elif comp_level == "3": ratio = 5 '''打开选择文件夹对话框''' filepath = filedialog.askopenfilename() # 获得选择好的文件 print('选择的PDF地址:', filepath) if os.path.exists("./pdf_output"): pass else: os.mkdir("./pdf_output") pic_path = str(os.getcwd()) + "\pdf_output" print('提取图片的输出地址:', pic_path ) pdf2pic(filepath, pic_path, comp_ratio=ratio) pdf_name = 'Compressed.pdf' if ".pdf" in pdf_name: rea(pic_path, pdf_name=pdf_name) else: rea(pic_path, pdf_name="{}.pdf".format(pdf_name)) print("压缩完成,请关闭窗口。若压缩等级不合适,请先删除图片和文件并重新打开程序。")
到此这篇关于使用python实现PDF本地化压缩的文章就介绍到这了,更多相关python PDF压缩内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!