PyQt5+fitz实现图片与PDF互相转换

  • 前言
  • 主界面
  • 图片合并为PDF
    • 如何添加图片
    • 如何拖动item
    • 右键菜单
    • 合成PDF
    • PDF文件预览
  • PDF转图片
  • 结语

前言

为了方便实用的实现PDF和图片之间的相互转换,采用PyQt5作为作为界面设计,使用fitz来进行数据处理。其中PyQt5使用pip install PyQt5 进行安装。fitz使用pip install PyMuPDF (历史遗留问题导致与包名不同)进行安装。

主界面

主界面里的左侧为两个按钮,右边是操作区,通过点击按钮实现功能切换。
主界面代码如下:

class PdfTools(QWidget):def __init__(self):super().__init__()self.setup_UI()def setup_UI(self):#窗口名称及大小hbox = QHBoxLayout()self.setWindowTitle('PDF图片互转程序--by klxy')self.setWindowIcon(QIcon(':/klxy.ico'))self.setGeometry(300,300,500,600)#建立splittersplitterleft = QSplitter(Qt.Vertical)#按钮所在布局#建立堆叠布局stack = QStackedLayout()#创建堆叠布局#实例化子窗口self.itp = ITPPage()self.pti = PTIPage()#功能切换按键self.btn_itp = QPushButton("图\n片\n合\n并\n为\nP\nD\nF")self.btn_pti = QPushButton("P\nD\nF\n转\n图\n片")self.btn_itp.setToolTip("多张图片合并为一个PDF文件")self.btn_pti.setToolTip("一个PDF文件拆分成多张图片")self.btn_itp.setStyleSheet("QPushButton{font-family:'仿宋';font-size:14px;color:rgb(0,0,0,255);}")self.btn_pti.setStyleSheet("QPushButton{font-family:'仿宋';font-size:14px;color:rgb(0,0,0,255);}")#将按键添加到布局splitterleft.addWidget(self.btn_itp)splitterleft.addWidget(self.btn_pti)#splitterleft.setSizes([100,100,100])#用于确定每个控件宽度,不设置时,默认平均分配,等比放大缩小#将窗口添加到布局stack.addWidget(self.itp)stack.addWidget(self.pti)#事件绑定self.btn_itp.clicked.connect(lambda:(stack.setCurrentWidget(self.itp)))self.btn_pti.clicked.connect(lambda:(stack.setCurrentWidget(self.pti)))#设置整体布局hbox.addWidget(splitterleft)hbox.addLayout(stack)self.setLayout(hbox)
if __name__ == "__main__":app = QApplication(sys.argv)tools = PdfTools()tools.show()sys.exit(app.exec_())

主界面的布局是QHBoxLayout(),横向的布局,左侧是一个QSplitter(采用垂直布局)用来放置按钮,无论窗口怎么调节,按钮始终充满这个区域,右侧是一个堆叠布局(用于放置功能窗口),使用self.button.clicked.connect(lambda:(stack.setCurrentWidget(Qwidget)))来进行窗口的切换。

图片合并为PDF

首先是界面


代码如下:

class UI_ITP_page(QWidget):def setup_UI(self):self.setStyleSheet("background-color:Azure")self.setGeometry(300,300,200,200)self.setAcceptDrops(True)#设置拖拽允许self.setWindowTitle('ITP')box = QBoxLayout(QBoxLayout.TopToBottom)self.filecount = QLabel('已导入图片数量:无')#显示窗口图片张数self.filecount.setStyleSheet("background-color:LightPink")self.piclist = QListWidget()#listwidget窗口self.piclist.setGeometry(50,50,100,100)self.compose = QPushButton("合成PDF")self.compose.setStyleSheet("background-color:cyan;font-family:'仿宋';")self.clear = QPushButton("清空列表")self.look = QPushButton("预览PDF")#将控件按垂直布局排列box.addWidget(self.filecount)box.addWidget(self.piclist)box.addWidget(self.compose)box.addWidget(self.look)box.addWidget(self.clear)self.setLayout(box)

界面非常的简洁,一个用于显示图片张数的label,一个合成按钮,一个清空按钮,一个预览按钮,还有一个空白区域。

如何添加图片

self.setAcceptDrops(True)#设置控件接受拖拽
self.piclist.setGeometry(50,50,100,100)#设置窗口大小(这里随意设置)
self.piclist.setSpacing(10)#设置每个项的间隔
self.piclist.setIconSize(QSize(100,100))#图标默认值
def dragEnterEvent(self,e):if e.mimeData().hasUrls():e.accept()else:e.ignore()
def dropEvent(self, e):global linksif e.mimeData().hasUrls():e.accept()urls = e.mimeData().urls()links = []for u in urls:links.append(str(u.toLocalFile()))#print(str(u.toLocalFile()))else:e.ignore()self.additems()
def additems(self):for pic in links:if pic[-4:] in ['.jpg','.png','.gif','.bmp','.JPG','.PNG','.GIF','.BMP']:pix = QPixmap()#...pix.load(pic)#...item = QListWidgetItem(QIcon(pix.scaled(150,pix.height()/pix.width()*150,Qt.KeepAspectRatio, Qt.SmoothTransformation)),pic)#self.piclist.addItem(item)self.filecount.setText('已导入图片数量:'+str(self.piclist.count())+"张")self.compose.setEnabled(True)

这里开启了控件的拖入功能,然后重写了拖入的事件,在拖入的时候,通过dropEvent()处理拖入的对象,获取图片的地址添加到links,然后用pic[-4:] (文件地址的格式部分)筛选出图片,将图片加载为QPixmap对象,这里的QListWidgetItem(QIcon(pix.scaled(150,pix.height()/pix.width()*150,Qt.KeepAspectRatio, Qt.SmoothTransformation)),pic)很重要,一般来说可以直接使用QIcon(picpath)来加载图片,但是在加载比较大的图片(几Mb)的时候,图片的拖动会很卡顿,由于每次拖动变换图片顺序,都会重绘缩略图,所以这里对导入时的缩略图进行限制,可以保证流畅地拖动。

如何拖动item

self.piclist.setMovement(QListView.Free)#表示数据项可以随意移动
self.piclist.setDragDropMode(QAbstractItemView.InternalMove)#这一项保证拖动的时候不会进行复制

拖拽功能就这两个需要设置,如果只设置第一个,则每次拖动都会复制一个item

常量 含义
NoDragDrop 0 在wiew内不支持拖和放
DragOnly 1 在view内支持拖动
DropOnly 2 在view内接受放下
DragDrop 3 在view内支持拖动和放下
InternalMove 4 在view内接受它自己范围内的移动操作《不是拷贝)

右键菜单

self.piclist.setContextMenuPolicy(3)#设置右键菜单启用
self.piclist.customContextMenuRequested[QtCore.QPoint].connect(self.myMenu)
def myMenu(self,point):menu = QMenu()#建立一个菜单对象
if self.piclist.itemAt(point) is None:pass#在空白处点击右键无效
else:menu.addAction(QAction(u'顺时针转90°',self))menu.addAction(QAction(u'逆时针转90°',self))menu.addAction(QAction(u'删除',self))menu.triggered[QAction].connect(partial(self.changePic,point))menu.exec_(QCursor.pos())def picRotate(self,picitem,angle):image = QImage()picpath = picitem.text()image.load(picpath)trans = QTransform()trans.rotate(angle)image = image.transformed(trans)image.save(picpath)#将旋转后的图片替换原来的图片picitem.setIcon(QIcon(picpath))#刷新图标(这里可以对缩略图像素进行一下限制)
def changePic(self,p,e):    item = self.piclist.itemAt(p)if e.text() == '顺时针转90°':self.picRotate(item,90)if e.text() == '逆时针转90°':self.picRotate(item,-90)if e.text() == '删除':self.piclist.takeItem(self.piclist.indexFromItem(item).row())self.filecount.setText('已导入图片数量:'+str(self.piclist.count())+"张")

setContextMenuPolicy()启用右键菜单,用customContextMenuRequested对菜单进行绑定。

合成PDF

现在我们已经对图片排好序,调好角度,接下来就是合成PDF了。
这个动图有点花了,没办法,5M限制。

def pictopdf(self,pics):doc = fitz.open()#创建一个PDF文件for i,img in enumerate(pics):page = doc.new_page()imgdoc = fitz.open(img)pdfbytes = imgdoc.convertToPDF()imgpdf = fitz.open("pdf", pdfbytes)imgrect = imgpdf.page_cropbox(0)page.set_mediabox(fitz.Rect(0,0,1000,imgrect.y1/imgrect.x1*1000))r = fitz.Rect(0,0,page.rect.width,page.rect.height)#这里对图片的大小进行了统一的调节page.show_pdf_page(r,imgpdf,0,rotate = 0)filepath,name = os.path.split(pics[0])self.savepath = filepath+"/"+filepath.split("/")[-1]+".pdf"doc.save(self.savepath)doc.close()self.look.setEnabled(True)os.startfile(filepath)#打开pdf生成的文件夹

这里的合成方法来自fitz的示例文档。
1.创建一个PDF对象。
2.为每一张图片创建一个新的页面。
3.页面统一设置为同一宽度。
4.将图片放入页面,并按照宽度相等的原则缩放图片。
(这里打开PDF虽然图片的大小有变化,但是信息是没有压缩的,合成的PDF文件大小基本等于图片大小之和)

PDF文件预览

这一部分的代码主要参考自:@Tiny_Feng 链接: PyQt5(利用QWidget和QPainter类)实现图片的鼠标左键移动、中键缩放、右键还原等.

class showpdf(QWidget):def __init__(self,path):self.pdfpath = pathsuper().__init__()self.setup_UI()def setup_UI(self):       doc = fitz.open(self.pdfpath)self.setWindowTitle('PDF预览(清晰度与原图一致,此图仅供预览)--by klxy')#self.setWindowIcon(QIcon(':/klxy.ico'))self.ispressed = bool(False)  self.ULpositon = QPoint(0,0)self.setGeometry(800,50,500,1000)self.piclist = []        pos = QPoint(0,0)   for i,per in enumerate(doc):##print(per)pix = per.getPixmap()#print(pix)fmt = QImage.Format_RGBA8888 if pix.alpha else QImage.Format_RGB888qtimg = QImage(pix.samples, pix.width, pix.height, pix.stride, fmt)self.showpic = QPixmap()self.showpic = self.showpic.fromImage(qtimg)self.piclist.append(self.showpic)if i == 0:pos = self.showpic.rect().bottomRight()+QPoint(1,1)else:pos += self.showpic.rect().bottomLeft()+QPoint(0,1)#print(pos)self.mainpix = QPixmap(pos.x(),pos.y())self.scaledImg = self.mainpix.scaled(self.size())self.hwscale = pos.y()/pos.x()#print(self.hwscale)self.pixpainter = QPainter(self.mainpix)self.scalespeed = 15pos = QPoint(0,0)for pic in self.piclist:self.pixpainter.drawPixmap(pos,pic)pos = pos+pic.rect().bottomLeft()self.pixpainter.end()box = QBoxLayout(QBoxLayout.TopToBottom)self.setLayout(box)def paintEvent(self,e):painter = QPainter(self)painter.drawPixmap(self.ULpositon,self.scaledImg)painter.end()#print('yeah')def get_path(self,path):self.path = path#鼠标事件def mousePressEvent(self, event):if event.buttons() == QtCore.Qt.LeftButton:                            # 左键按下#print("鼠标左键单击")  # 响应测试语句self.isPressed = True;                                         # 左键按下(图片被点住),置Tureself.preMousePosition = event.pos()                                # 获取鼠标当前位置'''重载一下滚轮滚动事件'''def wheelEvent(self, event):angle=event.angleDelta() / 8                                           # 返回QPoint对象,为滚轮转过的数值,单位为1/8度angleX=angle.x()                                                       # 水平滚过的距离(此处用不上)angleY=angle.y()self.scalespeed = self.hwscale * self.scaledImg.width()/25if self.scaledImg.height()>10 and self.scaledImg.width()>10:                                                # 竖直滚过的距离if angleY > 0:                                                         # 滚轮上滚#print("鼠标中键上滚")  # 响应测试语句self.scaledImg = self.mainpix.scaled(self.scaledImg.width()+self.scalespeed,(self.scaledImg.width()+self.scalespeed)*self.hwscale)newWidth = event.x() - (self.scaledImg.width() * (event.x()-self.ULpositon.x()))/ (self.scaledImg.width()-self.scalespeed)newHeight = event.y() - (self.scaledImg.height() * (event.y()-self.ULpositon.y())) / (self.scaledImg.height()-self.scalespeed*self.hwscale)self.ULpositon = QPoint(newWidth, newHeight)                    # 更新偏移量self.repaint()                                                     # 重绘else:                                                                  # 滚轮下滚#print("鼠标中键下滚")  # 响应测试语句self.scaledImg = self.mainpix.scaled(self.scaledImg.width()-self.scalespeed,(self.scaledImg.width()-self.scalespeed)*self.hwscale)newWidth = event.x() - (self.scaledImg.width() * (event.x()-self.ULpositon.x()))/ (self.scaledImg.width()+self.scalespeed)newHeight = event.y() - (self.scaledImg.height() * (event.y()-self.ULpositon.y()))/ (self.scaledImg.height()+self.scalespeed*self.hwscale)self.ULpositon = QPoint(newWidth, newHeight)                    # 更新偏移量self.repaint()else:                                                    # 重绘self.scaledImg=self.mainpix.scaled(self.scaledImg.width()+10,self.scaledImg.height()+10*self.hwscale)'''重载一下鼠标键松开事件'''def mouseReleaseEvent(self, event):if event.buttons() == QtCore.Qt.LeftButton:                            # 左键释放self.isLeftPressed = False;  # 左键释放(图片被点住),置False#print("鼠标左键松开")  # 响应测试语句elif event.button() == Qt.RightButton:                                 # 右键释放self.ULpositon = QPoint(0, 0)                                   # 置为初值self.scaledImg = self.mainpix.scaled(self.size())                # 置为初值self.repaint()                                                     # 重绘#print("鼠标右键松开")  # 响应测试语句'''重载一下鼠标移动事件'''def mouseMoveEvent(self,event):if self.isPressed:                                                 # 左键按下#print("鼠标左键按下,移动鼠标")  # 响应测试语句self.endMousePosition = event.pos() - self.preMousePosition        # 鼠标当前位置-先前位置=单次偏移量self.ULpositon = self.ULpositon + self.endMousePosition      # 更新偏移量self.preMousePosition = event.pos()                                # 更新当前鼠标在窗口上的位置,下次移动用self.repaint()

说一下思路:
1.PDF是很多张图片组成的,用fitz读取每一页的大小。
2.将每一页的大小加起来,就是画布的大小。
3.在画布上依次绘制出图像。
4.使用鼠标对图像进行缩放拖拽。
4.1.缩放时,鼠标所指位置不动,绘图的坐标和图像大小发生改变,坐标变换原理可以仔细阅读代码。

self.scalespeed = self.hwscale * self.scaledImg.width()/25#确保缩放速度一致,图像越大缩的越快

4.2.拖动只改变绘图的起点坐标。
4.3.需要注意的是对于图像不是正方形的情况,坐标的变换要根据比例来调整,这里有一个坑:

self.scalespeed是指每转一次,像素的缩放值,如果图像是正方形,那这个值就是确定的。但是对于矩形,每次缩放的值就需要调整了,由于坐标点的值是int类型,所以每次缩放都会有一些数据丢失,最后会导致图像变形,所以在每次重绘时,要根据图像的比例(self.hwscale)调整每次的缩放值以抵消数据丢失导致的图像变扁或者变高的问题。

self.scaledImg = self.mainpix.scaled(self.scaledImg.width()+self.scalespeed,(self.scaledImg.width()+self.scalespeed)*self.hwscale)newWidth = event.x() - (self.scaledImg.width() * (event.x()-self.ULpositon.x()))/ (self.scaledImg.width()-self.scalespeed)newHeight = event.y() - (self.scaledImg.height() * (event.y()-self.ULpositon.y())) / (self.scaledImg.height()-self.scalespeed*self.hwscale)

这里的高度变化是根据图像比例进行调节的,确保图像不会变形

PDF转图片

界面如下:

这一部分的功能相较于图片转PDF要简单得多,UI也不复杂。
一个QPushButton用于导入文件;
一个Qlabel显示导入文件名称;
一个滑动条(slider)调节图像质量,默认最高;
界面代码如下:

        vbox = QVBoxLayout()self.setGeometry(300,300,100,100)self.setWindowTitle('PTI')ptiAct.setStatusTip('Pdf file to Pictures')ptiAct.triggered.connect(qApp.quit)self.pti_btn = QPushButton('打开文件',self)self.pti_btn.setToolTip('打开需要提取的PDF文件')self.label = QLabel(self)self.overed = QProgressBar(self)#设置进度条self.overed.setMinimum(0)self.overed.setMaximum(100)self.overed.setValue(0)self.pti_btn.clicked.connect(self.open_file)#打开文件夹选取self.slider = QSlider(Qt.Horizontal)#滑条的设置self.slider.setMinimum(1)self.slider.setMaximum(5)self.slider.setSingleStep(1)self.slider.setValue(5)self.slider.setTickPosition(QSlider.TicksBelow)self.slider.setTickInterval(1)self.sliderlabel = QLabel(self)self.sliderlabel.setText("质量等级:5")self.slider.valueChanged.connect(self.zl_changed)self.deal = QPushButton('开始操作',self)self.deal.setEnabled(False)self.deal.clicked.connect(self.dealpdf)self.aimfiles = QPushButton('文件已生成',self)self.aimfiles.clicked.connect(self.openaim)self.aimfiles.setVisible(False)vbox.addWidget(self.pti_btn)vbox.addWidget(self.label)vbox.addWidget(self.slider)vbox.addWidget(self.sliderlabel)vbox.addWidget(self.overed)vbox.addWidget(self.deal)vbox.addWidget(self.aimfiles)self.setLayout(vbox)#self.setStyleSheet("background-color:green")def open_file(self):global dir,fileself.aimfiles.setVisible(False)self.label.setText('')self.overed.setValue(0)self.PdfFilepath,type = QFileDialog.getOpenFileName(self,"打开PDF文件",os.getcwd(),"PDF Files(*.pdf)")   dir,file = os.path.split(self.PdfFilepath)self.label.setText("文件名称:"+file)self.deal.setEnabled(True)def dealpdf(self):self.pdf_image(self.PdfFilepath,dir,self.slider.value(),self.slider.value(),0)def pdf_image(self,pdfPath,imgPath,zoom_x,zoom_y,rotation_angle):print("sb")print(pdfPath,imgPath)# 打开PDF文件pdf = fitz.open(pdfPath)# 逐页读取PDF#创建文件夹if not os.path.exists(imgPath+'/'+file[0:-4]):os.mkdir(imgPath+'/'+file[0:-4])for i,page in enumerate(pdf):# 设置缩放和旋转系数trans = fitz.Matrix(zoom_x, zoom_y).preRotate(rotation_angle)pm = page.getPixmap(matrix=trans, alpha=False)# 开始写图像pm.writePNG(imgPath+'/'+file[0:-4]+'/'+str(i)+".png")self.overed.setValue((i+1)/len(pdf)*100)pdf.close() self.aimfiles.setVisible(True)def zl_changed(self):self.sliderlabel.setText("质量等级:"+str(self.slider.value()))def openaim(self):os.startfile(self.PdfFilepath[0:-4])

PDF转图片的代码参考自fitz
演示一下效果:
在提取完成后,最下方会出现文件夹的按钮,按下可以打开文件夹,这里主要使用*setVisible(False)*先隐藏,等待处理完成后在出现。

结语

代码比较简单,写的比较简陋,充分发挥了缝合怪的特点,但功能上我觉得比较合适,简单好用就是好工具(

PyQt5+fitz实现图片与PDF互相转换相关推荐

  1. jpg图片转PDF怎么转换?这三个小技巧好用又简单

    众所周知,PDF文件格式具有较高的稳定性,即在不同的设备打开PDF文件,其中的排版.字体.字号不易出现错乱的情况,因此我们一般会将资料存储为PDF文件格式,方便阅读与传输.word文档转PDF文件,大 ...

  2. 图片转PDF怎么转换

    图片转PDF怎么转换?我们在日常办公中,图片和PDF是比较常用的两种格式文件,他们都有一些共同的特点,那就是方便查阅,而图片很多情况下在手机端查阅更方便,但是在电脑上不论是传输还是保存更多的朋友比较喜 ...

  3. 使用python在实现图片(包括扫描件的图片类pdf)转换成word文档过程中的常见问题

    pdf有两类,一类是别人用word转pdf,你想转过来那种,带有光标那种,计算机能轻松识别,转换相对简单很多.第二类,即图片类pdf,也就是平常工作中看到的各种扫描件,它的识别相对要复杂一些,但又常常 ...

  4. itext pdf转图片_图片转PDF怎么转换?可以试试这个PDF转换软件

    图片转PDF怎么转?图片虽然是一种很方便的文件,但是也比较占内存,尤其是现在的图片分辨率都很高,一张图片的体积就很大了.而PDF文件有个优点就是体积小,当我们有大量图片需要保存时,可以选择将图片转换成 ...

  5. PDF文件如何导出成图片,PDF如何转换成图片

    现在的pdf应用得很广泛,由于它可以不依赖操作系统的语言和字体及显示设备,阅读起来很方便.我们在工作中几乎每天都会使用到PDF文件,有时候我们需要将PDF文件导出成图片格式的文件,这样更方便我们使用. ...

  6. 图片转PDF怎么转换?快学习这三种免费转换方法!

    图像转PDF功能是指将图像文件转换为PDF文件的过程.PDF(PortableDocumentFormat)它是一种文件类型,可以存储许多元素,如文本.图像和报告.PDF文档具有跨平台.可打印.可搜索 ...

  7. 能将pdf完美转换成txt格式的方法

     pdf运用得到了推广,所以在很多场合我们都可能遇到一些pdf文档,使用直接复制的方式没有办法将里面的文字内容复制出来,仔细检查发现,主要的原因是由于pdf文档本身是图片,这类文档当然不能直接复制内容 ...

  8. Python批量转换文件夹下图片为PDF

    前言 最近遇到需要批量把图片转换成pdf的需求,要求转换之后输出到对应的转化目录下,由于需要转换的图片文件非常多,这就萌生了想要写个小程序的想法. 实现思路: (1)先弹窗选择对应的文件夹路径 (2) ...

  9. python 怎么将数组转为列表_图片转换成pdf格式怎么操作?什么软件能将图片转为pdf?...

    伙伴们好,你们知道如何把图片转为pdf格式吗?前一阵子我参加了一个家居行业大会,在会议上拍摄了不少会议照片,包括主持人讲话.嘉宾出席.观众提问.产品推广等环节都拍摄了不同的角度.拍摄好后,需要传送给写 ...

最新文章

  1. 【实用】ABAP ALV单元格修改信息提示
  2. 修改ALSM_EXCEL_TO_INTERNAL_TABLE的限制
  3. java中动态顺序死锁问题
  4. 自己动手之使用反射和泛型,动态读取XML创建类实例并赋值
  5. 体验.NET5 RC1极致性能,你也要“卧槽”!
  6. java面试总结(第一天)
  7. cocoachina上很酷的帖子
  8. IEC61850的Read请求报文件MMS PDU解码
  9. 在Postfix里给邮箱定虚拟别名
  10. 安科瑞智能照明控制系统在医院行业的应用
  11. java wtc_通过Java来调用WTC服务 | 学步园
  12. linux将时间转换成毫秒数,linux – 将jiffies转换为毫秒
  13. 查看wifi连接路由器的MAC地址
  14. sklearn线性回归,支持向量机SVR回归,随机森林回归,神经网络回归参数解释及示例
  15. 业务消息中心系统设计与实现(一)
  16. Synergy 使用
  17. sns jointplot 和 子图
  18. Google镜像代理地址:
  19. LJJ王国的致富修路计划 sdut oj
  20. 零死角玩转Android6.0系统Healthd深入分析

热门文章

  1. 辛东方:papi酱凭什么能够走红 背后的炒作令人发抖
  2. 43 岁硅谷技术大拿命丧街头,真凶落网:熟人作案,是 Expand IT 创始人
  3. 能闻到“银幕里的馄饨香”《小门神》用阿里云渲染好莱坞级特效
  4. PWM脉宽调制信号转模拟电流电压4-20ma0-5v10v隔离变送器
  5. android swf 播放器代码,Android Flash swf播放器源码(2016),技术稳定可以商用—— BY softboy...
  6. LocalDB 声称以后对于中文乱码的问题
  7. JS实现倒计时三秒跳转后到新页面
  8. 美术初学者画速写人物遇到的问题有哪些
  9. 重要!Google Play评分规则更新,提升近期评论权重
  10. 产品总监晋升之路(1):选育育留之产品经理胜任力模型