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()