使用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)上傳的我覺得直接用i love pdf這個(gè)網(wǎng)頁,挺好用的,就不知道安不安全。。。
但是感覺壓縮出來的圖片不是很理想,就想找一個(gè)圖片壓縮算法替換上去
在網(wǎng)上找到一個(gè)python的圖片壓縮算法,說是**“可能是最接近微信朋友圈的圖片壓縮算法”**
依賴安裝
先安裝庫 fitz,再安裝庫pymupdf,地址:https://github.com/pymupdf/PyMuPDF/
pip install fitz pip install PyMuPDF pip install easygui # 用來彈出文件選擇框的,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的頁數(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頁數(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) # 打開圖片
pdfbytes = imgdoc.convertToPDF() # 使用圖片創(chuàng)建單頁的 PDF
os.remove(img)
imgpdf = fitz.open("pdf", pdfbytes)
doc.insertPDF(imgpdf) # 將當(dāng)前頁插入文檔
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=高畫質(zhì)50%,2=中畫質(zhì)70%,3=低畫質(zhì)80%):(輸入數(shù)字并按回車鍵)")
# 用字典模擬Switch分支,注意輸入的值是str類型
ratio = {'1': 40, '2': 20, '3': 10}
# 字典中沒有則默認(rèn) 低畫質(zhì)壓縮
return ratio.get(comp_level, 10)
if __name__ == "__main__":
print("請選擇需要壓縮的PDF文件")
while True:
'''打開選擇文件夾對(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ù)壓縮請按任意鍵")
效果

壓縮出來的結(jié)果:

當(dāng)然,不是所有的pdf壓縮都會(huì)變小。。。本身pdf文件小的,處理出來后可能會(huì)變大,原因應(yīng)該是圖片提取保存的時(shí)候圖片文件變大,所有壓縮進(jìn)去的時(shí)候也會(huì)變大。
新增多進(jìn)程
別的博客中說到:“需要注意的是隊(duì)列中Queue.Queue是線程安全的,但并不是進(jìn)程安全,所以多進(jìn)程一般使用線程、進(jìn)程安全的multiprocessing.Queue(),而使用這個(gè)Queue如果數(shù)據(jù)量太大會(huì)導(dǎo)致進(jìn)程莫名卡?。ń^壁大坑來的),需要不斷地消費(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ì)列為空和取操作,這樣就簡單實(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ù)頁的隊(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頁數(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) # 打開圖片
pdfbytes = imgdoc.convertToPDF() # 使用圖片創(chuàng)建單頁的 PDF
remove(img)
imgpdf = fitz.open("pdf", pdfbytes)
doc2.insertPDF(imgpdf) # 將當(dāng)前頁插入文檔
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() # 開始執(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界面,沒那么美觀,感覺也沒必要吧哈哈哈
提取文件的時(shí)候比較慢,想著多線程但是不會(huì),可能要對(duì)文件分塊,還是算了
用pyinstaller(本人在conda創(chuàng)建的虛擬環(huán)境下python2.6打包出來小一點(diǎn))打包出來,文件大小差不多30M,而且打包之后運(yùn)行就沒那么流暢了,而且有個(gè)坑點(diǎn)
執(zhí)行過程在cmd黑窗口中打印信息時(shí),有時(shí),一不小心鼠標(biāo)點(diǎn)到了黑窗口里,程序就會(huì)暫停,要回車才能繼續(xù),網(wǎng)上的說法是
“或許是cmd啟用了快速編輯模式導(dǎo)致的問題。在快速編輯模式,鼠標(biāo)點(diǎn)擊cmd窗口時(shí),可以直接選擇窗口里的文本,如果此時(shí)cmd中運(yùn)行的進(jìn)程需要在cmd窗口中輸出信息,這個(gè)進(jìn)程就會(huì)被暫停,直到按下回車。”
方法補(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) # 打開圖片
pdfbytes = imgdoc.convertToPDF() # 使用圖片創(chuàng)建單頁的 PDF
os.remove(img)
imgpdf = fitz.open("pdf", pdfbytes)
doc.insertPDF(imgpdf) # 將當(dāng)前頁插入文檔
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
安裝必要的庫
先安裝庫 fitz,再安裝庫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)整過大的照片尺寸,和保存照片的質(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á)式來查找圖片
checkIM = r"/Subtype(?= */Image)"
doc = fitz.open(path) # 打開pdf文件
imgcount = 0 # 圖片計(jì)數(shù)
lenXREF = doc.xref_length() # 獲取對(duì)象數(shù)量長度
# 打印PDF的信息
print("文件名:{}, 頁數(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是用來將文件夾內(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!請先輸入壓縮等級(jí)1~3,然后在彈出的對(duì)話框中選擇需要壓縮的文件")
comp_level = input("壓縮等級(jí)(1=高畫質(zhì),2=中畫質(zhì),3=低畫質(zhì)):(輸入數(shù)字并按回車鍵)")
ratio = 10
if comp_level == "1":
ratio = 20
elif comp_level == "2":
ratio = 10
elif comp_level == "3":
ratio = 5
'''打開選擇文件夾對(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("壓縮完成,請關(guān)閉窗口。若壓縮等級(jí)不合適,請先刪除圖片和文件并重新打開程序。")到此這篇關(guān)于使用python實(shí)現(xiàn)PDF本地化壓縮的文章就介紹到這了,更多相關(guān)python PDF壓縮內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python manim實(shí)現(xiàn)排序算法動(dòng)畫示例
這篇文章主要為大家介紹了python manim實(shí)現(xiàn)排序算法動(dòng)畫示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
pycharm Tab鍵設(shè)置成4個(gè)空格的操作
這篇文章主要介紹了pycharm Tab鍵設(shè)置成4個(gè)空格的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-02-02
Python sql注入 過濾字符串的非法字符實(shí)例
這篇文章主要介紹了Python sql注入 過濾字符串的非法字符實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-04-04
Python網(wǎng)絡(luò)編程之TCP套接字簡單用法示例
這篇文章主要介紹了Python網(wǎng)絡(luò)編程之TCP套接字簡單用法,結(jié)合實(shí)例形式分析了TCP套接字的功能及客戶端、服務(wù)器端具體實(shí)現(xiàn)方法,需要的朋友可以參考下2018-04-04
在Python中通過機(jī)器學(xué)習(xí)實(shí)現(xiàn)人體姿勢估計(jì)
姿態(tài)檢測是計(jì)算機(jī)視覺領(lǐng)域的一個(gè)活躍研究領(lǐng)域。這篇文章將為大家介紹在Python中如何利用機(jī)器學(xué)習(xí)進(jìn)行人體姿勢估計(jì),感興趣的小伙伴可以了解一下2021-12-12
Python sklearn中的.fit與.predict的用法說明
這篇文章主要介紹了Python sklearn中的.fit與.predict的用法說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-06-06
如何用pandas讀取一個(gè)文件或某個(gè)文件夾下所有文件
這篇文章主要介紹了如何用pandas讀取一個(gè)文件或某個(gè)文件夾下所有文件問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02

