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