为了方便实用的实现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)
#建立splitter
splitterleft = 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)))来进行窗口的切换。
首先是界面
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 links
if 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)的时候,图片的拖动会很卡顿,由于每次拖动变换图片顺序,都会重绘缩略图,所以这里对导入时的缩略图进行限制,可以保证流畅地拖动。
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了。
这个动图有点花了,没办法,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文件大小基本等于图片大小之和)
这一部分的代码主要参考自:@Tiny_Feng 链接: PyQt5(利用QWidget和QPainter类)实现图片的鼠标左键移动、中键缩放、右键还原等.
class showpdf(QWidget):
def __init__(self,path):
self.pdfpath = path
super().__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_RGB888
qtimg = 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 = 15
pos = 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; # 左键按下(图片被点住),置Ture
self.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()/25
if 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要简单得多,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,file
self.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)*先隐藏,等待处理完成后在出现。
代码比较简单,写的比较简陋,充分发挥了缝合怪的特点,但功能上我觉得比较合适,简单好用就是好工具()。
源码:https://download.csdn.net/download/qq_42521874/16822356 给点。。。