使用python實(shí)現(xiàn)PDF本地化壓縮
用python做PDF壓縮
雖然現(xiàn)在有很多成熟的工具了,但是就是想自己搗鼓一下
在網(wǎng)上找了一圈,發(fā)現(xiàn)實(shí)現(xiàn)方法有兩種,一種是需要聯(lián)網(wǎng)上傳(TinyPNG的API)壓縮的,一種是本地用python算法
這里采用的是本地,基本的思路是
- 1、提取PDF內(nèi)容,保存成圖片
- 2、壓縮圖片
- 3、圖片合成PDF
- 4、新增加入多進(jìn)程和隊(duì)列的方式,加快壓縮
聯(lián)網(wǎng)上傳的我覺(jué)得直接用i love pdf這個(gè)網(wǎng)頁(yè),挺好用的,就不知道安不安全。。。
但是感覺(jué)壓縮出來(lái)的圖片不是很理想,就想找一個(gè)圖片壓縮算法替換上去
在網(wǎng)上找到一個(gè)python的圖片壓縮算法,說(shuō)是**“可能是最接近微信朋友圈的圖片壓縮算法”**
依賴安裝
先安裝庫(kù) fitz,再安裝庫(kù)pymupdf,地址:https://github.com/pymupdf/PyMuPDF/
pip install fitz pip install PyMuPDF pip install easygui # 用來(lái)彈出文件選擇框的,thinker的話會(huì)彈出兩個(gè)窗口怪怪的
縫合修改
CV大法用上
# -*- coding:utf-8 -*- # author: peng # file: mypdf.py # time: 2021/9/8 17:47 # desc:壓縮PDF,對(duì)純圖片的PDF效果效果較好,有文字內(nèi)容的可能會(huì)比較模糊,推薦高質(zhì)量的壓縮 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 # 時(shí)間計(jì)數(shù)裝飾器,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: # 其他的圖片就轉(zhuǎn)成JPEG self.img = self.img.convert("RGB") self.type = "JPEG" def computeScale(self): # 計(jì)算縮小的倍數(shù) 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() # 先調(diào)整大小,再調(diào)整品質(zhì) 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的頁(yè)數(shù) :param zooms: 值越大,分辨率越高,文件越清晰,列表內(nèi)兩個(gè)浮點(diǎn)數(shù),每個(gè)尺寸的縮放系數(shù),默認(rèn)為分辨率的2倍 :return: ''' if zooms is None: zooms = [2.0, 2.0] if os.path.exists('.pdf'): # 臨時(shí)文件,需為空 rmtree('.pdf') os.mkdir('.pdf') print(f"pdf頁(yè)數(shù)為 {totaling} \n創(chuàng)建臨時(shí)文件夾.....") for pg in range(totaling): page = doc[pg] print(f"\r{page}", end="") trans = fitz.Matrix(*zooms).preRotate(0) # 0為旋轉(zhuǎn)角度 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) # 打開(kāi)圖片 pdfbytes = imgdoc.convertToPDF() # 使用圖片創(chuàng)建單頁(yè)的 PDF os.remove(img) imgpdf = fitz.open("pdf", pdfbytes) doc.insertPDF(imgpdf) # 將當(dāng)前頁(yè)插入文檔 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("輸入壓縮等級(jí)1~3:") comp_level = input("壓縮等級(jí)(1=高畫(huà)質(zhì)50%,2=中畫(huà)質(zhì)70%,3=低畫(huà)質(zhì)80%):(輸入數(shù)字并按回車(chē)鍵)") # 用字典模擬Switch分支,注意輸入的值是str類型 ratio = {'1': 40, '2': 20, '3': 10} # 字典中沒(méi)有則默認(rèn) 低畫(huà)質(zhì)壓縮 return ratio.get(comp_level, 10) if __name__ == "__main__": print("請(qǐng)選擇需要壓縮的PDF文件") while True: '''打開(kāi)選擇文件夾對(duì)話框''' filepath = g.fileopenbox(title=u"選擇PDF", filetypes=['*.pdf']) if filepath == None: input("還未選擇文件,輸入任意鍵繼續(xù).......") continue else: filedir, filename = os.path.split(filepath) print(u'已選中文件【%s】' % (filename)) if filename.endswith(".pdf") == False: input("選擇的文件類型不對(duì),輸入任意鍵繼續(xù).......") 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('壓縮結(jié)果 %.2f M >>>> %.2f M'%(oldsize/(1024 * 1024),newsize/(1024 * 1024))) input(f"壓縮已完成,文件保存在改程序目錄下{filedir},如需繼續(xù)壓縮請(qǐng)按任意鍵")
效果
壓縮出來(lái)的結(jié)果:
當(dāng)然,不是所有的pdf壓縮都會(huì)變小。。。本身pdf文件小的,處理出來(lái)后可能會(huì)變大,原因應(yīng)該是圖片提取保存的時(shí)候圖片文件變大,所有壓縮進(jìn)去的時(shí)候也會(huì)變大。
新增多進(jìn)程
別的博客中說(shuō)到:“需要注意的是隊(duì)列中Queue.Queue是線程安全的,但并不是進(jìn)程安全,所以多進(jìn)程一般使用線程、進(jìn)程安全的multiprocessing.Queue(),而使用這個(gè)Queue如果數(shù)據(jù)量太大會(huì)導(dǎo)致進(jìn)程莫名卡?。ń^壁大坑來(lái)的),需要不斷地消費(fèi)。”
這里對(duì)代碼的修改部分有幾個(gè)小地方,提取圖片的參數(shù)變?yōu)閜df路徑(因?yàn)閐oc參數(shù)在進(jìn)程調(diào)用時(shí)會(huì)出錯(cuò)),隊(duì)列轉(zhuǎn)pdf內(nèi)部加入判斷隊(duì)列為空和取操作,這樣就簡(jiǎn)單實(shí)現(xiàn)了生產(chǎn)者-消費(fèi)者模式
from multiprocessing import Process, Queue # 提取成圖片 def covert2pic(filepath, qpaper, zooms=None): ''' :param filepath: pdf文件的位置 :param qpaper: 數(shù)據(jù)頁(yè)的隊(duì)列 :param zooms: 值越大,分辨率越高,文件越清晰,列表內(nèi)兩個(gè)浮點(diǎn)數(shù),每個(gè)尺寸的縮放系數(shù),默認(rèn)為分辨率的2倍 :return: ''' doc = fitz.open(filepath) totaling = doc.pageCount if zooms is None: zooms = [2.0, 2.0] if path.exists('.pdf'): # 臨時(shí)文件,需為空 rmtree('.pdf') mkdir('.pdf') print(f"pdf頁(yè)數(shù)為 {totaling} \n創(chuàng)建臨時(shí)文件夾.....") for pg in range(totaling): page = doc[pg] print(f"\r{page}", end="") trans = fitz.Matrix(*zooms).preRotate(0) # 0為旋轉(zhuǎn)角度 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(): # 如果隊(duì)列為空,則循環(huán)等待 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) # 打開(kāi)圖片 pdfbytes = imgdoc.convertToPDF() # 使用圖片創(chuàng)建單頁(yè)的 PDF remove(img) imgpdf = fitz.open("pdf", pdfbytes) doc2.insertPDF(imgpdf) # 將當(dāng)前頁(yè)插入文檔 if path.exists(obj): # 若pdf文件存在先刪除 remove(obj) doc2.save(obj) # 保存pdf文件 doc2.close() @runtime def pdfz(filepath, obj, ratio, totaling): # 參數(shù)傳遞變?yōu)閒ilepath qpaper = Queue() # 創(chuàng)建隊(duì)列 threads = [] #read_thread = threading.Thread(target=covert2pic, args=(doc, totaling, qpaper)) read_thread = Process(target=covert2pic, args=(filepath, qpaper)) ''' 多進(jìn)程這里傳參數(shù)不一定成功,參數(shù)需要可以序列化才行,這里如果傳doc的變量,會(huì)報(bào)錯(cuò)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() # 開(kāi)始執(zhí)行線程 for th in threads: th.join() print("結(jié)束")
最終多進(jìn)程會(huì)比單進(jìn)程節(jié)約大約30%的時(shí)間(節(jié)約了處理圖片和生成pdf的時(shí)間,就是函數(shù)pic2pdf)
缺點(diǎn)
使用的不是GUI界面,沒(méi)那么美觀,感覺(jué)也沒(méi)必要吧哈哈哈
提取文件的時(shí)候比較慢,想著多線程但是不會(huì),可能要對(duì)文件分塊,還是算了
用pyinstaller(本人在conda創(chuàng)建的虛擬環(huán)境下python2.6打包出來(lái)小一點(diǎn))打包出來(lái),文件大小差不多30M,而且打包之后運(yùn)行就沒(méi)那么流暢了,而且有個(gè)坑點(diǎn)
執(zhí)行過(guò)程在cmd黑窗口中打印信息時(shí),有時(shí),一不小心鼠標(biāo)點(diǎn)到了黑窗口里,程序就會(huì)暫停,要回車(chē)才能繼續(xù),網(wǎng)上的說(shuō)法是
“或許是cmd啟用了快速編輯模式導(dǎo)致的問(wèn)題。在快速編輯模式,鼠標(biāo)點(diǎn)擊cmd窗口時(shí),可以直接選擇窗口里的文本,如果此時(shí)cmd中運(yùn)行的進(jìn)程需要在cmd窗口中輸出信息,這個(gè)進(jìn)程就會(huì)被暫停,直到按下回車(chē)。”
方法補(bǔ)充
Python實(shí)現(xiàn)PDF文件壓縮
1. 原理PDF切分為圖片,根據(jù)壓縮率zoom壓縮圖片后保存本地;圖片合成PDF
2. 依賴PyMuPDF包
pip install PyMuPDF
3. 代碼
import fitz import os def covert2pic(zoom): if os.path.exists('.pdf'): # 臨時(shí)文件,需為空 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) # 打開(kāi)圖片 pdfbytes = imgdoc.convertToPDF() # 使用圖片創(chuàng)建單頁(yè)的 PDF os.remove(img) imgpdf = fitz.open("pdf", pdfbytes) doc.insertPDF(imgpdf) # 將當(dāng)前頁(yè)插入文檔 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 # 清晰度調(diào)節(jié),縮放比率 pdfz(sor, obj, zoom) os.removedirs('.pdf')
4. 使用
- 腳本和要壓縮的PDF需在同一路徑下
- sor變量為需要壓縮的文件
- zoom用于調(diào)整壓縮率
- 壓縮后使用PDF打印功能導(dǎo)出能夠進(jìn)一步壓縮
Python從PDF中提取圖片、壓縮PDF
安裝必要的庫(kù)
先安裝庫(kù) fitz,再安裝庫(kù)pymupdf,地址:https://github.com/pymupdf/PyMuPDF/
pip install fitz pip install pymupdf
源代碼
第一個(gè)pdf2pic從pdf中提取jpg文件的部分引用了別人的代碼
以下兩行doc.引用的注意了,不然會(huì)報(bào)錯(cuò)
lenXREF = doc.xref_length() text = doc.xref_object(i) # 定義對(duì)象字符串
另外加入了重新調(diào)整過(guò)大的照片尺寸,和保存照片的質(zhì)量,這里有個(gè)變量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)" # 使用正則表達(dá)式來(lái)查找圖片 checkIM = r"/Subtype(?= */Image)" doc = fitz.open(path) # 打開(kāi)pdf文件 imgcount = 0 # 圖片計(jì)數(shù) lenXREF = doc.xref_length() # 獲取對(duì)象數(shù)量長(zhǎng)度 # 打印PDF的信息 print("文件名:{}, 頁(yè)數(shù): {}, 對(duì)象: {}".format(path, len(doc), lenXREF - 1)) # 遍歷每一個(gè)對(duì)象 for i in range(1, lenXREF): text = doc.xref_object(i) # 定義對(duì)象字符串 isXObject = re.search(checkXO, text) # 使用正則表達(dá)式查看是否是對(duì)象 isImage = re.search(checkIM, text) # 使用正則表達(dá)式查看是否是圖片 if not isXObject or not isImage: # 如果不是對(duì)象也不是圖片,則continue continue imgcount += 1 pix = fitz.Pixmap(doc, i) # 生成圖像對(duì)象 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: # 否則先轉(zhuǎn)換CMYK pix0 = fitz.Pixmap(fitz.csRGB, pix) pix0.writeImage(os.path.join(pic_path, new_name)) pix0 = None pix = None # 釋放資源 print("提取了{(lán)}張圖片".format(imgcount)) os.startfile(pic_path)
下面這個(gè)rea是用來(lái)將文件夾內(nèi)的照片重新組合為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('功能完善中')
主程序中隨意加了一些判斷,如壓縮等級(jí)1、2、3等。
if __name__ == '__main__': print("Hello world!請(qǐng)先輸入壓縮等級(jí)1~3,然后在彈出的對(duì)話框中選擇需要壓縮的文件") comp_level = input("壓縮等級(jí)(1=高畫(huà)質(zhì),2=中畫(huà)質(zhì),3=低畫(huà)質(zhì)):(輸入數(shù)字并按回車(chē)鍵)") ratio = 10 if comp_level == "1": ratio = 20 elif comp_level == "2": ratio = 10 elif comp_level == "3": ratio = 5 '''打開(kāi)選擇文件夾對(duì)話框''' 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("壓縮完成,請(qǐng)關(guān)閉窗口。若壓縮等級(jí)不合適,請(qǐng)先刪除圖片和文件并重新打開(kāi)程序。")
到此這篇關(guān)于使用python實(shí)現(xiàn)PDF本地化壓縮的文章就介紹到這了,更多相關(guān)python PDF壓縮內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python+tkinter實(shí)現(xiàn)制作文章搜索軟件
無(wú)聊的時(shí)候做了一個(gè)搜索文章的軟件,有沒(méi)有更加的方便快捷不知道,好玩就行了。軟件是利用Python和tkinter實(shí)現(xiàn)的,感興趣的可以嘗試一下2022-10-10Python深度學(xué)習(xí)pytorch卷積神經(jīng)網(wǎng)絡(luò)LeNet
這篇文章主要為大家講解了Python深度學(xué)習(xí)中的pytorch卷積神經(jīng)網(wǎng)絡(luò)LeNet的示例解析,有需要的朋友可以借鑒參考下希望能夠有所幫助2021-10-10django實(shí)現(xiàn)后臺(tái)顯示媒體文件
這篇文章主要介紹了django實(shí)現(xiàn)后臺(tái)顯示媒體文件,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-04-04python使用elasticsearch的過(guò)程詳解
Elasticsearch 是一個(gè)開(kāi)源的搜索引擎,建立在一個(gè)全文搜索引擎庫(kù) Apache Lucene基礎(chǔ)之上,這篇文章主要介紹了python使用elasticsearch的詳細(xì)過(guò)程,需要的朋友可以參考下2024-03-03Python執(zhí)行外部命令subprocess的使用詳解
subeprocess模塊是python自帶的模塊,無(wú)需安裝,主要用來(lái)取代一些就的模塊或方法,本文通過(guò)實(shí)例代碼給大家分享Python執(zhí)行外部命令subprocess及使用方法,感興趣的朋友跟隨小編一起看看吧2021-05-05