实际工作中,经常需要对大量的图片合并为一个PDF文件,以便文件的传输和保存。网上有许多图片生成PDF的工具。其实利用Python的Pillow组件,我们也可以用pyqt5制作一个有模有样的PDF生成工具。

一 、关于PIL组件:

Pillow是python下功能强大的图像处理组件(前身为PIL组件),提供了强大的图像处理功能,本应用中主要用到他的图像格式转换功能(把图片改为RGB、8位彩色或灰度模式),同时用它的存储功能,把多个图片保存到一个PDF文件中。
如没有安装Pillow,可以命令行下运行:pip install pillow或pip3 install pillow即可。

二 、实现功能

1、将指定文件夹中的所有图片文件合并生成一个PDF文件。
2、支持jpg、png和bmp图片格式,自动过滤掉其它格式文件
3、应用中实现图片预览,方便用户清除无用图片,或调整图片的放置顺序。
4、通过拖动,实现图片文件顺序的调整
5、输入或选择保存PDF的文件名和路径
6、指定PDF中图片模式(RGB,8位彩色或灰度,满足对不同用户对PDF文件大小的要求。

三 、应用界面

应用界面采用pycharm+5开发,布局为常用的盒式布局结构:整个窗口使用一个垂直布局,下面包括五个水平布局。界面结构如下:

python fitz 图片合并一起_qt5

主要操作过程:

1、点击第一行的…按钮后,弹出文件夹选择对话框,在选择文件夹后,从文件夹中读取所有PNG、JPG图像文件,并把文件名加载到第二行左侧的列表中。

2、通过点击选中列表中的文件名,可以在第二行右侧预览出该文件对应的图像。

python fitz 图片合并一起_qt5_02

3、通过拖动列表中的文件名,可以改变生成PDF的文件顺序

4、通过点击选中列表中的文件名,可以点击列表下方的称除按钮从列表中移除该图片(但不删除文件)

5、通过在第三行的文本框中输入路径及文件文件名,或点后面的…按钮选择文件,确定文件的保存位置和文件名。

6、在第三行后面的色彩模式中选择图片在PDF文件中的保存模式。

python fitz 图片合并一起_qt5_03

7、点击立即生成,完成后即可生成指定的PDF文件。

python fitz 图片合并一起_python_04

生成的pdf文件在指定的文件下。

python fitz 图片合并一起_qt5_05

打开后可以看到PDF中的文件按我们在列表中指定的顺序生成,模式为我们要求的灰度模式。

python fitz 图片合并一起_pillow_06

四 、代码及说明

代码如下,注释中解释的比较清楚,不再赘述。

'''
一个将多个图片合并为一个PDF文件的python工具
#################################################################
主要功能:
1、用于将指定文件夹中的所有图片文件合并生成一个PDF文件。
2、支持jpg、png和bmp图片格式,自动过滤掉其它格式
3、实现图片预览
4、通过拖动,实现图片文件顺序的调整
5、输入或选择保存PDF的文件名和路径
6、指定PDF中图片模式(RGB,8位彩色或灰度,满足对不同用户对PDF文件大小的要求。
#################################################################
界面介绍:
界面使用box布局,一个垂直QVBox布局,包含个多行水平QHBox布局:
第1行:一个显示原始图片文件所在位置的文本框,一个打开文件夹选择对话框按钮,一个重新加截按钮,
第2行:左侧为一个显示指定文件夹内图片文件名的列表框,下面为一个删除按钮,右侧为一个图片,用于预览当前选中的图片文件
第4行:一个文本框,用于输入PDF文件保存位置,一个按钮用于选择文件保存位置,一个下拉列表,用于选择文件保存模式(RGB、8位彩色或灰度)
第5行:一个文本框,用于显示处理进度和处理信息。
第6行:一个居中的开始处理按钮
##################################################################
'''
import os
import sys

from PyQt5.QtCore import pyqtSignal, QThread
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (QWidget, QPushButton,
                             QHBoxLayout, QVBoxLayout, QApplication, QLabel,
                             QLineEdit, QTextEdit, QComboBox, QFileDialog,
                             QListWidget, QListWidgetItem, QAbstractItemView
                             )
from PIL import Image

class Thread_do(QThread):  # 定义线程类
    # 定义带参数一个信号及所需参数
    _signal =pyqtSignal(str)
    #定义几个成员,用于传递参数,包括:源文件夹,目标文件名,保存模式和图像列表
    _picpath=""
    _trgfile=""
    _picmode=""
    _piclist=[]

    #重写类线程类的初步化方法,用于将传入的几个参数保存到类成员中
    def __init__(self,_path,_trg,_mode,_piclist):
        super().__init__()
        self._picpath=_path
        self._trgfile=_trg
        self._picmode=_mode
        self._piclist=_piclist

    def run(self):#线程的执行方法
       list_images=[]#定义一个列表,用于保存每个图像对象
       for i in range(len(self._piclist)):
           fullPicname=self._picpath+"/"+self._piclist[i]
           if self._picmode=="32位真彩":
               list_images.append(Image.open(fullPicname).convert("RGB"))#32位真彩
           if self._picmode == "8位彩色":
               list_images.append(Image.open(fullPicname).convert("P"))  # 8位彩色
           if self._picmode == "灰度":
               list_images.append(Image.open(fullPicname).convert("L"))  # 灰度

           self._signal.emit("处理..."+fullPicname+"...完成")  ##返回处理
       # 因为PIL保存为PDF的格式为:
       #第一个图像对象.save(目标文件名,保存参数,其它图片对象列表)
       #所以要把第一个图像对象从列表中剔除,并赋值给另一个图像对象(im1)
       im1=list_images.pop(0)
       im1.save(self._trgfile, save_all=True, append_images=list_images)
       self._signal.emit("全部完成")##返回全部完成信号


class Example(QWidget):

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        #定义布局
        vbox = QVBoxLayout()
        hbox_line1 = QHBoxLayout()
        hbox_line2 = QHBoxLayout()
        hbox_line3 = QHBoxLayout()
        hbox_line4 = QHBoxLayout()
        hbox_line5 = QHBoxLayout()

        #第一行,一个标签,一个文件框,一个打开对话框按钮,一个重载按钮
        ######################################################
        self.txt_SrcPath = QLineEdit("D:/")  # 原始文件所在位置,默认为C:/
        self.txt_SrcPath.setReadOnly(True)#设置为不可编辑
        btn_openDiagSrcPath = QPushButton("...")  # 选择源文件夹按钮
        #绑定按钮的打开事件
        btn_openDiagSrcPath.clicked.connect(self.showDialog_Src)
        hbox_line1.addWidget(QLabel("图片路径:"))
        hbox_line1.addWidget(self.txt_SrcPath)
        hbox_line1.addWidget(btn_openDiagSrcPath)


        #第二行,一个垂直布局(上面为一个图片文件列表,下面为操作按钮)一个用于显示当前选中的图片
        vbox_s1=QVBoxLayout()
        self.pic_listbox = QListWidget(self)
        self.pic_listbox.setMaximumHeight(300)
        self.pic_listbox.setMinimumHeight(300)
        # 使列表框中的元素可拖动并设置拖动模式
        self.pic_listbox.setAcceptDrops(True)
        self.pic_listbox.setDragDropMode(QAbstractItemView.InternalMove)
        #绑定列表框中列表项点击的事件(更新预览图片的显示内容)
        self.pic_listbox.itemClicked.connect(self.listItemClick)
        btn_del=QPushButton("移除选中的文件")
        #绑定删除按钮的事件
        btn_del.clicked.connect(self.deleteListItem)

        vbox_s1.addWidget(self.pic_listbox)
        vbox_s1.addWidget(btn_del)
        hbox_line2.addLayout(vbox_s1)

        # 添加一个图片,用于显示选中的图片
        self.lbl_Image = QLabel(self)
        #固定预览图片的大小,可以进一步细化为根据宽度自动适应图片比例
        self.lbl_Image.setMinimumSize(480,360)
        self.lbl_Image.setMaximumSize(480,360)
        self.lbl_Image.setText("暂未选择文件")
        self.lbl_Image.setScaledContents(True)
        hbox_line2.addWidget(self.lbl_Image)

        #第3行,一个保存PDF存放位置的文本框,一个按钮用于打开文件选择对话框,
        # 一个下拉列表:选择PDF保存模式:真彩、8位彩色或灰度。
        self.txt_PDFPath = QLineEdit("D:/newFile.pdf")  # PDF文件默认保存位置和文件名
        btn_openDiagPDFFile = QPushButton("...")  # 选择目标文件按钮
        # 绑定按钮的打开事件
        btn_openDiagPDFFile.clicked.connect(self.showDialog_Tgt)
        #下拉列表框:图片模式:
        self.list_PicFmt = QComboBox()  # 定义下拉框,并添加三个图片格式选项
        self.list_PicFmt.addItem("32位真彩")
        self.list_PicFmt.addItem("8位彩色")
        self.list_PicFmt.addItem("灰度")
        self.list_PicFmt.setMaximumWidth(140)
        self.list_PicFmt.setMinimumWidth(140)
        hbox_line3.addWidget(QLabel("PDF路径和文件名:"))
        hbox_line3.addWidget(self.txt_PDFPath)
        hbox_line3.addWidget(btn_openDiagPDFFile)
        hbox_line3.addWidget(QLabel("PDF文件色彩模式:"))
        hbox_line3.addWidget(self.list_PicFmt)

        #第四行,处理信息文本框
        self.txt_PressInof = QTextEdit()  # 处理信息文本框
        hbox_line4.addWidget(self.txt_PressInof)
        #第五行,生成按钮
        self.btn_do = QPushButton("立即生成")  # 处理按钮
        hbox_line5.addWidget(self.btn_do)
        #绑定生成按钮的事件
        self.btn_do.clicked.connect(self.process_Pic)
        #把几个水平布局加入到垂直布局器中
        vbox.addLayout(hbox_line1)
        vbox.addLayout(hbox_line2)
        vbox.addLayout(hbox_line3)
        vbox.addLayout(hbox_line4)
        vbox.addLayout(hbox_line5)

        self.setLayout(vbox)
        #设置窗口大小(1027*720)及初始化位置(100,100)
        self.setGeometry(100, 100, 1024, 720)
        self.setWindowTitle('PIC2PDF')#设置标题

        self.show()

    def showDialog_Src(self):  # 选择源文件夹对话框,把选中的文件夹路径保存在txt_srcpath中
        folder_path = QFileDialog.getExistingDirectory(self, "选择原始图片文件夹")
        self.txt_SrcPath.setText(folder_path)
        self.loadPic(folder_path)
        itemCount=self.pic_listbox.count()
        #self.lbl_Image.setText(f'共有{itemCount}张可用图片')
        #self.statusBar().showMessage(f'共有{itemCount}张可用图片')

    def showDialog_Tgt(self):#选择PDF文件的保存路径及文件名
        pdffile_name = QFileDialog.getOpenFileName(self, '选择文件', 'D:/newFile.pdf')
        if pdffile_name[0]:
            self.txt_PDFPath.setText(pdffile_name[0])

    def listItemClick(self):#列表框中列表项的点击事件,在预览图片中显示该文件
        sel_item=self.pic_listbox.selectedItems()
        sel_file=os.path.join(self.txt_SrcPath.text(), sel_item[0].text())
        pixmap = QPixmap(sel_file)  # 按指定路径找到图片
        self.lbl_Image.setPixmap(pixmap)  # 在label上显示图片
        self.lbl_Image.setScaledContents(True)  # 让图片自适应label大小

    def deleteListItem(self):#删除列表中某一项的事件
        current_row = self.pic_listbox.currentRow()
        if current_row != -1:
            item = self.pic_listbox.takeItem(current_row)
            del item
            self.pic_listbox.setCurrentRow(-1)
            self.lbl_Image.clear()
            self.lbl_Image.setText("暂未选择文件")

    def loadPic(self,_path):#把指定文件夹中的图片加载到列表框中的事件,在howDialog_Src函数中调用
        self.pic_listbox.clear()
        for f in os.listdir(_path):
            #如果扩展名为jpg,bmp或PNG,就添加到列表框中,注意区分大小写
             if f.endswith(".jpg") or f.endswith(".png") or f.endswith(".bmp") \
                     or  f.endswith(".JPG") or f.endswith(".PNG") or f.endswith(".BMP"):
                self.pic_listbox.addItem(QListWidgetItem(f))

    def process_Pic(self):  # 处理按钮的点击事件
        # 取得参数,需要几个参数:
        # 1、图片所在路径
        # 2、pdf文件名及保存位置
        # 3、保存模式(原色、灰度或黑白)
        # 4、需处理图片文件名列表
        path=self.txt_SrcPath.text()#图片路径文件夹名
        trg=self.txt_PDFPath.text()#需生成的PDF文件名及路径
        mode=self.list_PicFmt.currentText()#参数2,保存模式,原色灰度或黑白

        list_pic=[]#定义一个列表,用于保存需处理图片的路径
        for i in range(self.pic_listbox.count()):
            file_name=self.pic_listbox.item(i).text()
            list_pic.append(file_name)

        curText = "开始生成..."
        self.txt_PressInof.setText(curText)
        # 生成过程中,改变按钮的状态为不可用,且按钮文本变为正在生成
        self.btn_do.setEnabled(False)
        self.btn_do.setText("正在生成...")
        # 定义线程实例并将参数传入
        self.thread_do = Thread_do(path,trg,mode,list_pic)
        # 信号连接,如果收到信号,就执行对应的函数
        self.thread_do._signal.connect(self.update_TxtInfo)
        # 启动线程实例
        self.thread_do.start()

    def update_TxtInfo(self, _info):  # 定义收到线程返回信号的方法
        if _info == "全部完成":  # 如收到全部完成,恢复按钮的状态
            self.btn_do.setEnabled(True)
            self.btn_do.setText("开始生成")
        else:  # 否则更新文本框的内容
            self.txt_PressInof.append(_info)

#################################################################
# 主程序
###############################################################
if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

五 、生成可执行文件

使用python的PyInstaller组件来把python文件生成exe文件。如没有安装过Pyinstaller组年,可以在命令行下运行pip install pyinstaller来安装。

在命令行模式下运行以下代码(以下两种方式均可,我个人比较喜欢第二种)

1、Pyinstall xxxx.py把xxxx.py打包为可执行文件。打包后生成两个文件夹,一个build,为临时文件,完成后可删除,一个dist,里面为可执行文件及支持库文件。

注意:xxxx.py文件名中不能有小数点。

2、Pyinstaller -F xxxx.py:把xxxx.py打包为单独一个可执行文件,可执行文件中已经包括运行所需库文件。

示例:如本应用的python代码文件为pic2pdf.py,我们把这个文件放到d:/pic2pdf文件夹下,则可在命令行模式下进入该文件夹,并运行:

pyinstaller -F pic2pdf.py

python fitz 图片合并一起_python fitz 图片合并一起_07

结果如下图:

python fitz 图片合并一起_python_08

当出现如上成功结束的字样,就能关闭命令行窗口了。

在我的电脑中找到这个文件夹(d:\pic2pdf),就能看到里面多了几个文件和文件夹。

python fitz 图片合并一起_python fitz 图片合并一起_09

进入dist文件夹,就能看到生成的pic2pdf.exe文件了,运行一下,效果和在pycharm中测试时的完全一致。

python fitz 图片合并一起_qt5_10

六 、源码下载

完整源码及生成的exe文件可从:下载。

七、补充:

这个工具其实还有许多可完善的地方,包括:设计一个漂亮的图标,充分利用pillow的强大功能,实现对图片的翻转、映像、剪裁等更复杂的功能。这些就有待码友们进一步完善了。