1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
|
#! /usr/bin/env python3
'''
Basic PDF viewer using PyQt and MuPDF's Python bindings.
Hot-keys in main window:
+= zooms in
-_ zoom out
0 reset zoom.
Up/down, page-up/down Scroll current page.
Shift page-up/down Move to next/prev page.
Command-line usage:
-h
--help
Show this help.
<path>
Show specified PDF file.
Example usage:
These examples build+install the MuPDF Python bindings into a Python
virtual environment, which enables this script's 'import mupdf' to work
without having to set PYTHONPATH.
Linux:
> python3 -m venv pylocal
> . pylocal/bin/activate
(pylocal) > pip install libclang pyqt5
(pylocal) > cd .../mupdf
(pylocal) > python setup.py install
(pylocal) > python scripts/mupdfwrap_gui.py
Windows (in a Cmd terminal):
> py -m venv pylocal
> pylocal\Scripts\activate
(pylocal) > pip install libclang pyqt5
(pylocal) > cd ...\mupdf
(pylocal) > python setup.py install
(pylocal) > python scripts\mupdfwrap_gui.py
OpenBSD:
# It seems that pip can't install py1t5 or libclang so instead we
# install system packages and use --system-site-packages.]
> sudo pkg_add py3-llvm py3-qt5
> python3 -m venv --system-site-packages pylocal
> . pylocal/bin/activate
(pylocal) > cd .../mupdf
(pylocal) > python setup.py install
(pylocal) > python scripts/mupdfwrap_gui.py
'''
import os
import sys
import mupdf
import PyQt5
import PyQt5.Qt
import PyQt5.QtCore
import PyQt5.QtWidgets
class MainWindow(PyQt5.QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Set up default state. Zooming works by incrementing self.zoom by +/-
# 1 then using magnification = 2**(self.zoom/self.zoom_multiple).
#
self.page_number = None
self.zoom_multiple = 4
self.zoom = 0
# Create Qt widgets.
#
self.central_widget = PyQt5.QtWidgets.QLabel(self)
self.scroll_area = PyQt5.QtWidgets.QScrollArea()
self.scroll_area.setWidget(self.central_widget)
self.scroll_area.setWidgetResizable(True)
self.setCentralWidget(self.scroll_area)
self.central_widget.setToolTip(
'+= zoom in.\n'
'-_ zoom out.\n'
'0 zoom reset.\n'
'Shift-page-up prev page.\n'
'Shift-page-down next page.\n'
)
# Create menus.
#
# Need to store menu actions in self, otherwise they appear to get
# destructed and so don't appear in the menu.
#
self.menu_file_open = PyQt5.QtWidgets.QAction('&Open...')
self.menu_file_open.setToolTip('Open a new PDF.')
self.menu_file_open.triggered.connect(self.open_)
self.menu_file_open.setShortcut(PyQt5.QtGui.QKeySequence("Ctrl+O"))
self.menu_file_show_html = PyQt5.QtWidgets.QAction('&Show html')
self.menu_file_show_html.setToolTip('Convert to HTML and show in separate window.')
self.menu_file_show_html.triggered.connect(self.show_html)
self.menu_file_quit = PyQt5.QtWidgets.QAction('&Quit')
self.menu_file_quit.setToolTip('Exit the application.')
self.menu_file_quit.triggered.connect(self.quit)
self.menu_file_quit.setShortcut(PyQt5.QtGui.QKeySequence("Ctrl+Q"))
menu_file = self.menuBar().addMenu('&File')
menu_file.setToolTipsVisible(True)
menu_file.addAction(self.menu_file_open)
menu_file.addAction(self.menu_file_show_html)
menu_file.addAction(self.menu_file_quit)
def keyPressEvent(self, event):
if self.page_number is None:
#print(f'self.page_number is None')
return
#print(f'event.key()={event.key()}')
# Qt Seems to intercept up/down and page-up/down itself.
modifiers = PyQt5.QtWidgets.QApplication.keyboardModifiers()
#print(f'modifiers={modifiers}')
shift = (modifiers == PyQt5.QtCore.Qt.ShiftModifier)
if 0:
pass
elif shift and event.key() == PyQt5.Qt.Qt.Key_PageUp:
self.goto_page(page_number=self.page_number - 1)
elif shift and event.key() == PyQt5.Qt.Qt.Key_PageDown:
self.goto_page(page_number=self.page_number + 1)
elif event.key() in (ord('='), ord('+')):
self.goto_page(zoom=self.zoom + 1)
elif event.key() in (ord('-'), ord('_')):
self.goto_page(zoom=self.zoom - 1)
elif event.key() == (ord('0')):
self.goto_page(zoom=0)
def resizeEvent(self, event):
self.goto_page(self.page_number, self.zoom)
def show_html(self):
'''
Convert to HTML using Extract, and show in new window using
PyQt5.QtWebKitWidgets.QWebView.
'''
buffer_ = self.page.fz_new_buffer_from_page_with_format(
format="docx",
options="html",
transform=mupdf.FzMatrix(1, 0, 0, 1, 0, 0),
cookie=mupdf.FzCookie(),
)
html_content = buffer_.fz_buffer_extract().decode('utf8')
# Show in a new window using Qt's QWebView.
self.webview = PyQt5.QtWebKitWidgets.QWebView()
self.webview.setHtml(html_content)
self.webview.show()
def open_(self):
'''
Opens new PDF file, using Qt file-chooser dialogue.
'''
path, _ = PyQt5.QtWidgets.QFileDialog.getOpenFileName(self, 'Open', filter='*.pdf')
if path:
self.open_path(path)
def open_path(self, path):
path = os.path.abspath(path)
try:
self.document = mupdf.FzDocument(path)
except Exception as e:
print(f'Failed to open path={path!r}: {e}')
return
self.setWindowTitle(path)
self.goto_page(page_number=0, zoom=0)
def quit(self):
# fixme: should probably use qt to exit?
sys.exit()
def goto_page(self, page_number=None, zoom=None):
'''
Updates display to show specified page number and zoom level,
defaulting to current values if None.
Updates self.page_number and self.zoom if we are successful.
'''
# Recreate the bitmap that we are displaying. We should probably use a
# mupdf.FzDisplayList to avoid processing the page each time we need to
# change zoom etc.
#
# We can run out of memory for large zoom values; should probably only
# create bitmap for the visible region (or maybe slightly larger than
# the visible region to allow for some limited scrolling?).
#
if page_number is None:
page_number = self.page_number
if zoom is None:
zoom = self.zoom
if page_number is None or page_number < 0 or page_number >= self.document.fz_count_pages():
return
self.page = mupdf.FzPage(self.document, page_number)
page_rect = self.page.fz_bound_page()
z = 2**(zoom / self.zoom_multiple)
# For now we always use 'fit width' view semantics.
#
# Using -2 here avoids always-present horizontal scrollbar; not sure
# why...
z *= (self.centralWidget().size().width() - 2) / (page_rect.x1 - page_rect.x0)
# Need to preserve the pixmap after we return because the Qt image will
# refer to it, so we use self.pixmap.
try:
self.pixmap = self.page.fz_new_pixmap_from_page_contents(
ctm=mupdf.FzMatrix(z, 0, 0, z, 0, 0),
cs=mupdf.FzColorspace(mupdf.FzColorspace.Fixed_RGB),
alpha=0,
)
except Exception as e:
print(f'self.page.fz_new_pixmap_from_page_contents() failed: {e}')
return
image = PyQt5.QtGui.QImage(
int(self.pixmap.fz_pixmap_samples()),
self.pixmap.fz_pixmap_width(),
self.pixmap.fz_pixmap_height(),
self.pixmap.fz_pixmap_stride(),
PyQt5.QtGui.QImage.Format_RGB888,
);
qpixmap = PyQt5.QtGui.QPixmap.fromImage(image)
self.central_widget.setPixmap(qpixmap)
self.page_number = page_number
self.zoom = zoom
def main():
app = PyQt5.QtWidgets.QApplication([])
main_window = MainWindow()
args = iter(sys.argv[1:])
while 1:
try:
arg = next(args)
except StopIteration:
break
if arg.startswith('-'):
if arg in ('-h', '--help'):
print(__doc__)
return
elif arg == '--html':
main_window.show_html()
else:
raise Exception(f'Unrecognised option {arg!r}')
else:
main_window.open_path(arg)
main_window.show()
app.exec_()
if __name__ == '__main__':
main()
|