用python做PDF本地化压缩,新增多进程

用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的博客-CSDN博客

(2条消息) Python-从PDF中提取图片、压缩PDF_xinRCNN的博客-CSDN博客

但是感觉压缩出来的图片不是很理想,就想找一个图片压缩算法替换上去

在网上找到一个python的图片压缩算法,说是**“可能是最接近微信朋友圈的图片压缩算法”**

GitHub - Freefighter/Luban-Py: Python version of Luban(鲁班)—Image compression with efficiency very close to WeChat Moments/可能是最接近微信朋友圈的图片压缩算法

依赖安装

先安装库 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窗口鼠标点击后挂起不执行问题(禁止快速编辑模式)_浅醉樱花雨的专栏-CSDN博客

    加入代码:但是没用。。。输入的时候会输入不了,暂时不加了

    可以看下边的方法,只对cmd设置而已

    Windows下CMD(命令提示符)脚本运行过程中被阻塞_VisionLRJ的博客-CSDN博客_cmd阻塞

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值