使用multiprocessing解决PyMuPDF不支持多线程加载导致的界面卡死无响应问题,及一个PyQt5实现的简易PDF阅读器例子
最近在用PyMuPDF实现一个PDF阅读器,发现PyMuPDF在加载某些epub时耗时非常长,有的长达10几秒,会导致界面卡死无响应。
尝试用多线程后台加载,发现还是不能解决问题,和作者交流(issue链接 fitz.open blocks main thread even though I use it in a thread)后,作者说该库不支持真正的多线程,在多线程模式下也会阻塞主线程。
最后用multiprocessing解决该问题,我另外写了一个简单的PyQt5实现的PDF阅读器来说明如何解决该问题,效果图及代码如下。
#!python3 # -*- coding: utf-8 -*- # this script demostrates how to use PyMuPDF in multiprocessing to avoid unresponsive GUI when fitz.open costs a long time # author: yinkaisheng@live.com import os import sys import time import multiprocessing as mp import queue import fitz from PyQt5 import QtCore, QtGui, QtWidgets class DocForm(QtWidgets.QWidget): def __init__(self): super().__init__() self.process = None self.queNum = mp.Queue() self.queDoc = mp.Queue() self.pageCount = 0 self.curPageNum = 0 self.lastDir = '' self.timerSend = QtCore.QTimer(self) self.timerSend.timeout.connect(self.onTimerSendPageNum) self.timerGet = QtCore.QTimer(self) self.timerGet.timeout.connect(self.onTimerGetPage) self.timerWaiting = QtCore.QTimer(self) self.timerWaiting.timeout.connect(self.onTimerWaiting) self.initUI() def initUI(self): vbox = QtWidgets.QVBoxLayout() self.setLayout(vbox) hbox = QtWidgets.QHBoxLayout() self.btnOpen = QtWidgets.QPushButton('OpenDocument', self) self.btnOpen.clicked.connect(self.openDoc) hbox.addWidget(self.btnOpen) self.btnPlay = QtWidgets.QPushButton('PlayDocument', self) self.btnPlay.clicked.connect(self.playDoc) hbox.addWidget(self.btnPlay) self.btnStop = QtWidgets.QPushButton('Stop', self) self.btnStop.clicked.connect(self.stopPlay) hbox.addWidget(self.btnStop) self.label = QtWidgets.QLabel('0/0', self) self.label.setFont(QtGui.QFont('Verdana', 20)) hbox.addWidget(self.label) vbox.addLayout(hbox) self.labelImg = QtWidgets.QLabel('Document', self) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding) self.labelImg.setSizePolicy(sizePolicy) vbox.addWidget(self.labelImg) self.setGeometry(100, 100, 500, 600) self.setWindowTitle('PyMuPDF Document Player') self.show() def openDoc(self): path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open Document", self.lastDir, "All Supported Files (*.pdf;*.epub;*.xps;*.oxps;*.cbz;*.fb2);;PDF Files (*.pdf);;EPUB Files (*.epub);;XPS Files (*.xps);;OpenXPS Files (*.oxps);;CBZ Files (*.cbz);;FB2 Files (*.fb2)", options=QtWidgets.QFileDialog.Options()) if path: self.lastDir, self.file = os.path.split(path) if self.process: self.queNum.put(-1) # use -1 to notify the process to exit self.timerSend.stop() self.curPageNum = 0 self.pageCount = 0 self.process = mp.Process(target=openDocInProcess, args=(path, self.queNum, self.queDoc)) self.process.start() self.timerGet.start(40) self.label.setText('0/0') self.queNum.put(0) self.startTime = time.perf_counter() self.timerWaiting.start(40) def playDoc(self): self.timerSend.start(500) def stopPlay(self): self.timerSend.stop() def onTimerSendPageNum(self): if self.curPageNum < self.pageCount - 1: self.queNum.put(self.curPageNum + 1) else: self.timerSend.stop() def onTimerGetPage(self): try: ret = self.queDoc.get(False) if isinstance(ret, int): self.timerWaiting.stop() self.pageCount = ret self.label.setText('{}/{}'.format(self.curPageNum + 1, self.pageCount)) else:#tuple, pixmap info num, samples, width, height, stride, alpha = ret self.curPageNum = num self.label.setText('{}/{}'.format(self.curPageNum + 1, self.pageCount)) fmt = QtGui.QImage.Format_RGBA8888 if alpha else QtGui.QImage.Format_RGB888 qimg = QtGui.QImage(samples, width, height, stride, fmt) self.labelImg.setPixmap(QtGui.QPixmap.fromImage(qimg)) except queue.Empty as ex: pass def onTimerWaiting(self): self.labelImg.setText('Loading "{}", {:.2f}s'.format(self.file, time.perf_counter() - self.startTime)) def closeEvent(self, event): self.queNum.put(-1) event.accept() def openDocInProcess(path, queNum, quePageInfo): doc = fitz.open(path) quePageInfo.put(doc.pageCount) while True: num = queNum.get() if num < 0: break page = doc.loadPage(num) pix = page.getPixmap() quePageInfo.put((num, pix.samples, pix.width, pix.height, pix.stride, pix.alpha)) doc.close() print('process exit') if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) form = DocForm() sys.exit(app.exec_())