基于PyQt5实现的PDF小工具


基于PyQt5实现的PDF工具箱

  • 完整项目详见PDFTools
  • 功能:批量添加目录图像增强PDF拆分PDF合并

    文章目录

      • 基于PyQt5实现的PDF工具箱
    • 前言
    • 1. 使用模块
    • 2. 编辑环境搭建
    • 3.工具箱功能概述
    • 4.步骤解析
    • 5.项目组成
    • 6.主要代码展示 ,完整项目详见[PDFTools](https://github.com/devxzh/PDFTools)
    • 7.ToDo List
    • 8.结语
    • 9.参考

前言

PDF 是一种便携,易查找的电子书,在学习工作中我们可能需要翻阅大量的参考书籍,实体书往往贵且笨重,如果需要经常翻阅,那么可以购置纸质书。但是大多数时候我们可能只是需要查阅书中的某一部分,如此电子书便具有不可比拟的优势,互联网上有者大量的电子书籍,且多数以pdf格式流传,其中一部分为文字版,一部分为扫描版。这些文件一般为自制的盗版书籍,往往缺少目录,或者扫描不清晰。以致索引困难,观感极差。

市面上也有很多pdf编辑器,如Adobe Acrobat PDF,PDF Element,福昕PDF编辑器等,但他们大多价格昂贵,且没有批量添加目录的功能,虽有OCR增强的功能,但是比较耗时,且在年迈的PC上容易卡死。此工具没有使用OCR,仅对扫描页面的图片逐一增强来改善PDF清晰度。

1. 使用模块

  1. pyqt5pymupdfpython3.7qt designerQCandyUi(可选)
  2. 指定下载源。可后加-i https://pypi.tuna.tsinghua.edu.cn/simple,如
pip3 install pymupdf -i https://pypi.tuna.tsinghua.edu.cn/simple
pip3 install pyqt5,pyqt5-tools
  1. 在终端输入 designer 可启动 QT Designer
  2. 注意:如果在电脑上已经安装过Ananconda,则pyqt版本过高可能会导致Spyder无法打开,此时请使用如下命令,更换版本,(不要频繁的使用conda update)
pip3 uninstall pyqt5
pip3 install pyqt5==5.12.0

版本号如果有误,终端会列出可安装的版本,选择其一输入即可

2. 编辑环境搭建

  1. IDE:PyCharm Edu ,Spyder ,VS Code + Python 插件

  2. 上述工具择其一即可,其中PyCharm 配置后可一体化开发 配置教程-博客园,配置教程-知乎

  3. 我没有配置,逐一进行

  4. 用途 工具/方法
    绘制用户界面 (UI) QT Designer
    UI转Python代码 pyuic5
    编写Python 代码 PyCharm / VS Code
    执行代码 python3.7 (PyCharm / VS Code)
    打包为可执行文件 (可选) pyinstaller

3.工具箱功能概述

​ 1.主界面 (使用QCandyUi美化后的截图)

基于PyQt5实现的PDF小工具_第1张图片

​ 2.添加目录 (详见生成的 demo.txt)

基于PyQt5实现的PDF小工具_第2张图片

​ 3.文本增强

基于PyQt5实现的PDF小工具_第3张图片

​ 4.拆分文档

基于PyQt5实现的PDF小工具_第4张图片

​ 5.合并文档

基于PyQt5实现的PDF小工具_第5张图片

4.步骤解析

  1. 使用QT Designer 绘制界面(如上图) ,选择 QDialog without Button,拖拽控件,绘制后保存为.ui文件,我分别保存为PDFTools.ui add_UI.ui enhance_UI.ui merge_UI.ui split_UI.ui,绘制界面比较简单,但是控件命名应当添加适当的前缀或后缀加以区分。

  2. 使用pyuic5命令将ui文件转换为python代码,切换到项目文件夹,输入

    pyuic5 PDFTools.ui -o PDFTools.py
    

    依次对界面代码进行转换,生成后的py文件无需手动更改,当再次生成时会完全覆盖。

  3. 编写调用窗口和信号处理代码。pyqt延续了qt的设计思想,只要处理好信号与槽(可理解为触发事件与处理方法关联),那么编写项目也会得心应手。在编写过程中可查看qt类手册,pyqt中的方法大多与QT C++同名,但是少了丑陋的指针 ->,使代码不那么扎眼。具体见代码解析

  4. 推荐使用ipython对方法/类 进行测试

  5. 启动

    > python main.py
    
  6. 如果需要可进行打包

    pyinstaller --onefile --windowed --icon=PDF.ico main.py
    

5.项目组成

  1. 代码组成

    --------窗口信号处理----------
    main.py
    callAdd.py
    callEnhance.py
    callSplit.py
    callMerge.py
    --------窗口界面布局----------
    PDFTools.py
    add_UI.py
    enhance_UI.py
    split_UI.py
    merge_UI.py
    --------PDF处理函数----------
    addFunctins.py
    enhanceFunctions.py
    splitFunctions.py
    
  2. 用到的QT控件信号, 通过 connect 可关联方法

    控件 信号
    pushButton clicked
    spinBox valueChanged /editintFinished
    Slider sliderMoved / valueChanged
    radioButton toggled
    lineEdit textChanged
    checkBox stateChanged
    comboBox currentIndexChanged
  3. 用到的QT控件方法 ,大多数控件都有setText() 和text() 方法,不一一列举

    控件 方法
    textEdit setText() /text()
    lineEdit setText() / text()
    checkBox isChecked()
    tableWidget setItem()
    spinBox setReadOnly()
    lineEdit setReadOnly
  4. pymupdf中的方法

    名称 描述
    open 打开文件(pdf,图片)
    save 保存
    setToC 设置目录
    getPixmap 获取本页的图片
    insertPDF 插入PDF
    close 关闭
    pageCount 获取页码(属性)
  5. osPIL 中的方法

    方法 描述
    open 打开txt
    write 写入txt
    close 关闭文本
    strip 删除指定符号
    split 根据指定符号分割字符串
    len 计算长度,数量
    range 连续的数
    replace 替换指定字符串
    os.getcwd() 获取当前文件路径
    os.path.exists() 是否存在文件夹
    os.makedirs() 创建文件夹
    os.remove() 删除文件
    Image.open 打开图片
    ImageEnhance.Contrast 增强方法

6.主要代码展示 ,完整项目详见PDFTools

main.py展示了 创建类,关联信号与槽,创建窗口的一般途径

import ctypes
import sys

from PyQt5.QtWidgets import QApplication, QDialog

from callAdd import AddForm
from callEnhance import EnhanceForm
from callMerge import MergeForm
from callSplit import SplitForm
from PDFTools import Ui_Dialog  # or import *

class MyForm(QDialog):
    def __init__(self):
        super().__init__()
        self.ui=Ui_Dialog()
        self.ui.setupUi(self)
        self.ui.pushButton_add.clicked.connect(self.add_Window)
        self.ui.pushButton_enhance.clicked.connect(self.enhance_Window)
        self.ui.pushButton_split.clicked.connect(self.split_Window)
        self.ui.pushButton_merge.clicked.connect(self.merge_Window)
        self.show()
    
    def add_Window(self):
        self.w1=AddForm()
        self.w1.show()
        #self.hide()
        
    def enhance_Window(self):
        self.w2=EnhanceForm()
        self.w2.show()
       
    def split_Window(self):
        self.w3=SplitForm()
        self.w3.show()
        
    def merge_Window(self):
        self.w4=MergeForm()
        self.w4.show()
      
        
if __name__=="__main__":
    app=QApplication(sys.argv)

    #set taskbar icon
    myappid = 'mycompany.myproduct.subproduct.version' # arbitrary string
    ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)

    w=MyForm()
    w.show()
    sys.exit(app.exec_())

主要功能代码部分

callAdd.py 工程中的四个callxxx.py均独立启动使用

# 添加目录事件主要逻辑
def start(self):
    if self.ui.lineEdit_PDF.text() != "" and self.ui.lineEdit_TXT.text() != "":
        self.ui.textEdit_progress.setText("可以")
        self.pdf=add2pdf(self.doc, self.txt, self.offsetNum)
        if self.pdf != None:
            newname=self.ui.lineEdit_PDF.text().replace(".pdf","-new.pdf")
            self.pdf.save(newname)
            self.ui.textEdit_progress.setText("目录添加成功")
            else:
                self.ui.textEdit_progress.setText("缺少文件")

addFunctions.py

def add2pdf(pdffile, txtfile, offset):
    """
    添加目录到PDF,其中文件应为打开状态
    :param: pdffile , textfile , offset
    :return:pdffile
    """
    lines = txtfile.readlines()
    toc = []
    for line in lines:
        if line[0] == '#' or len(line.split()) == 0:
            continue
        level = get_level(line)
        title = get_title(line)
        page = get_page(line)
        toc.append([level, title, page])
    pdffile.setToC(toc)
    return pdffile

callEnhance.py

#增强pdf 事件逻辑代码
def w2_start(self):
        oldFileName = self.ui.lineEdit_w2_openPDF.text()
        newFileName = oldFileName.replace(".pdf", "[enhanced].pdf")

        if oldFileName != '':
            doc = fitz.open(oldFileName)
            enhanceDoc = fitz.open()
            num = doc.pageCount
            baseBar = 1.0 / num * 100

            for i in range(num):
                imgpdf = getEnhancedPdf(doc[i], i, self.saveFlag,
                [self.colorValue, self.contrastValue, self.sharpnessValue,
                                         self.brightnessValue], 2)

                enhanceDoc.insertPDF(imgpdf)
                self.ui.label_w2_tips.setText("正在处理第 %d 页" % i)
                self.ui.progressBar_w2.setValue(i * baseBar)

            enhanceDoc.save(newFileName)
            doc.close()
            enhanceDoc.close()

            self.ui.progressBar_w2.setValue(100)
            self.ui.label_w2_tips.setText("处理完毕")

enhanceFunctions.py

"""
===============================================================================
    ImageEnhance.Color(image)      色彩平衡。 0-黑白,1-原图(可为小数,可 > 1)
    ImageEnhance.Contrast(image)   对比度。   0-灰色图像,1-原图
    ImageEnhance.Brightness(image) 亮度。     0.0-黑色图像,1-原图
    ImageEnhacne.Sharpness(image)  锐化。0.0是模糊图像,1.0是原始图像,2.0是锐化图像
===============================================================================
    上述类的增强方法都为 enhance(factor), 显示用show(),见下例子
===============================================================================
    from PIL import Image,ImageEnhance

    img=Image.open("name.png")
    # 下面的 Contrast 可换为 Color, Brightness, Sharpness 之一, 其他不变
    enhanceImg=ImageEnhance.Contrast(img)
    newImage=enhanceImg.enhance(2)
    newImage.show()
    newImage.save("new.png")
===============================================================================
"""

import fitz
import os
from PIL import Image, ImageEnhance

# 设置缩放及旋转角度
def setZoom(zoom_xy):  # 设置为 2
    rotate = int(0)  # 设置图片的旋转角度
    zoom_x = zoom_xy  # 设置图片相对于PDF文件在X轴上的缩放比例
    zoom_y = zoom_xy  # 设置图片相对于PDF文件在Y轴上的缩放比例
    trans = fitz.Matrix(zoom_x, zoom_y).preRotate(rotate)
    return trans


def getEnhancedPdf(docPage, index, saveFlag=False, factor=[1.0, 1.0, 1.0, 1.0], zoom_xy=2):
    """
    获取图像,并增加对比度
    :param : docPage: doc[i]
    :param :factor : color, contrast, sharpness, brightness
    :param :zoom_xy : enlargement factor
    :return : a page of enhanced pdf 
    """
    pix = docPage.getPixmap(matrix=setZoom(zoom_xy), alpha=False)
    img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

    enhanceTemp1 = ImageEnhance.Color(img)
    pilImg1 = enhanceTemp1.enhance(factor[0])  # PIL img

    enhanceTemp2 = ImageEnhance.Contrast(pilImg1)
    pilImg2 = enhanceTemp2.enhance(factor[1])  # PIL img

    enhanceTemp3 = ImageEnhance.Sharpness(pilImg2)
    pilImg3 = enhanceTemp3.enhance(factor[2])  # PIL img

    enhanceTemp4 = ImageEnhance.Brightness(pilImg3)
    pilImg4 = enhanceTemp4.enhance(factor[3])  # PIL img

    # pilImg.show()
    folder = os.getcwd() + '/split'  # 创建文件夹
    if not os.path.exists(folder):
        os.makedirs(folder)
    imgName = folder + "/splitImage(" + "%04d" % index + ").png"
    pilImg4.save(imgName)
    fitImg = fitz.open(imgName)
    if saveFlag == False:
        os.remove(imgName)
    pdfBytes = fitImg.convertToPDF()
    imgpdf = fitz.open("pdf", pdfBytes)
    return imgpdf

callSplit.py

def w3_startSplit(self):
    if self.splitMode==0 or self.pathStr=='':
    self.tipStr="未添加pdf文件 或 未设置拆分模式 ! "
    elif self.splitMode==1:
    self.tipStr=split_pdf_same_page(self.pathStr,self.pageSame)
    elif self.splitMode==2:
    self.tipStr=split_pdf_custom_page(self.pathStr,self.pageStr)
    else:
    pass
    self.ui.label_w3_tip.setText(self.tipStr)

splitFunctions.py

# -*- coding: utf-8 -*-
"""
Created on Fri Jul 19 09:18:18 2020
@author: cherish
"""

import fitz

def split_pdf_same_page(pathStr, page):
    """
    将pdf 拆分为 多个pdf文件,每page页为一部分
    :param: pathStr :pdf路径的字符串, page : int 分割区间
    :return: true
    """
    doc = fitz.open(pathStr)
    # doc1 = fitz.open() # 空 pdf 文件
    docPage = doc.pageCount
    if page > docPage or page ==0 :
        return "错误 : 页码超出范围,或 分割区间为0 !"

    num = docPage//page  # 可以拆成 page 页的部分
    for i in range(num):  # 0 到 num-1
        doc1 = fitz.open()  # 空 pdf 文件
        doc1.insertPDF(doc, from_page=page*i, to_page=page*(i+1)-1, start_at=-1)
        partName = '[part'+str(i)+'].pdf'
        newName = pathStr.replace(".pdf", partName)
        doc1.save(newName)
        doc1.close()

    surplus = docPage % page  # 最后少于 page 的部分
    if surplus != 0:
        doc1 = fitz.open()  # 空 pdf 文件
        doc1.insertPDF(doc, from_page=page*num, to_page=docPage-1, start_at=-1)
        partName = '[part'+str(num)+'].pdf'
        newName = pathStr.replace(".pdf", partName)
        doc1.save(newName)
        doc1.close()
    return "拆分成功"


def split_pdf_custom_page(pathStr, pageStr):
    """
    将pdf拆分为自定义页码的多个pdf文件
    :param: str ,pdf路径的字符串, pageStr: 自定义页码的字符串
    :return: Error page string / True
    """
    doc = fitz.open(pathStr)
    docPage = doc.pageCount
    pageStr = pageStr.replace(',', ',')  # 如果是中文逗号
    pageList = pageStr.split(",")

    for page in pageList:
        pse = page.split('-')  # page start and end
        ps = int(pse[0])
        pe = int(pse[0]) if len(pse) == 1 else int(pse[1])
        if ps > pe or pe > docPage:
            return "请检查>  "+ page + " 是否有误 !"  # error: page part

        doc1 = fitz.open()  # 创建 空pdf文件
        doc1.insertPDF(doc, from_page=ps-1, to_page=pe-1, start_at=-1)
        partName = '[page'+str(ps)+'-' + str(pe)+'].pdf'
        newName = pathStr.replace(".pdf", partName)
        doc1.save(newName)
        doc1.close()
    return "拆分成功"

merge_UI.py 设置表格列宽,使用pyuic后会覆盖

self.tableWidget_w4_fileList.setColumnWidth(0, 450)
self.tableWidget_w4_fileList.setColumnWidth(1, 70)

callMerge.py

def w4_start(self):
    if self.rowCount != 0:
    doc = fitz.open(self.ui.tableWidget_w4_fileList.item(0, 0).text())
    basebar = int(1.0 / self.rowCount * 100)

    for i in range(1, self.rowCount):
    doc1 = fitz.open(self.ui.tableWidget_w4_fileList.item(i, 0).text())
    doc.insertPDF(doc1)
    self.ui.progressBar_w4_bar.setValue(i * basebar)
    self.ui.progressBar_w4_bar.setValue(100)
    doc.save("merge.pdf")
    doc.close()

demo.txt 该文件是存放目录的文件,要求和格式见下,在打开添加目录窗口的同时会自动生成该文件

# =========================================================================
# 
# 建议在如下网站搜集目录:
#           1.京东图书 https://book.jd.com/
#           2.豆瓣读书 https://book.douban.com/
#           3.当当图书 http://book.dangdang.com/
#           4.文泉书局 https://wqbook.wqxuetang.com/
# 
# =========================================================================
# 标准格式1 如下:(空行不影响)
# 特征 :两部分构成: 标题 + 空格 + 页码
# 标题中含 '第' 和 '章' 的会识别为一级标题,其他为二级标题
# =========================================================================

第1章概述     1
什么是OpenCV        1
OpenCV怎么用        2
什么是计算机视觉     3
OpenCV的起源        6
OpenCV的结构    7
使用IPP来加速OpenCV     8
谁拥有OpenCV    9
下载和安装OpenCV    9

# ==========================================================================
# 标准格式2 如下 
# 特征 :章序/节序 + 空格 + 标题 + 空格 + 页码 (空格用于区分各元素)
# 无 章节序 的 默认识别为 二级标题 ,若想设置为一级标题,请在前加 '@ ',
# 一般 需要区分的是 前言 目录 附录 参考文献 这些 
# 节序中有一个点 表二级标题(如 6.1 ),两个点 表三级标题(如 1.2.3),以此类推
# ==========================================================================

第6章 支持向量机 121
6.1 间隔与支持向量 121
6.2 对偶问题 123
6.3 核函数 126
6.4 软间隔与正则化 129
6.5 支持向量回归 133
6.6 核方法 137
6.7 阅读材料 139
习题 141
休息一会儿 145
@ 参考文献 520

# ==========================================================================

7.ToDo List

- [ ] 自定义样式表
- [ ] 自动爬取目录
- [ ] img2pdf

8.结语

第一次使用pyqt5写一个完整的项目,用时大概六天。前期绘制界面一天。后期逐一完善各个功能四天,写文档,改bug一天。最大的感受就是Python语法友好,轮子很全。本次项目也是熟悉pyqt的过程。写完本应用基本掌握了常用的控件,信号,槽。整体而言,使用pyqt5编写一些小工具还是很方便的。至于执行效率,一般的小项目基本体现不出来。

PyQt5优点:相较于QT Creator,python代码比较优雅;相较于 C# Winform/WPF ,python拥有较多的库。可用样式表setStyleSheet美化控件

缺点:QT Designer 可设置参数偏少,需要使用代码设置,控件不够美观,打包文件偏大。

9.参考

  • pymupdf
  • qt class
  • Qt5 Python GUI Programming Cookbook 2018

你可能感兴趣的:(PyQt5,笔记)