-
Notifications
You must be signed in to change notification settings - Fork 140
/
browse.py
289 lines (229 loc) · 8.4 KB
/
browse.py
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
"""
Display a document using Tkinter
-------------------------------------------------------------------------------
License: GNU AGPL V3.0
(c) 2022-2024 Artifex Software Inc
Usage
-----
python browse.py [input.pdf]
Description
-----------
Get filename and start displaying page 1. Please note that all file types
of MuPDF are supported (including EPUB e-books and HTML files for example).
Pages can be directly jumped to, or buttons can be used for paging.
This version contains enhancements:
* PIL no longer needed
* Zooming is now flexible: only one button serves as a toggle. Keyboard arrow keys can
be used for moving through the window when zooming.
We also interpret keyboard events (PageDown / PageUp) and mouse wheel actions
to support paging as if a button was clicked. Similarly, we do not include
a 'Quit' button. Instead, the ESCAPE key can be used, or cancelling the form.
To improve paging performance, we are not directly creating pixmaps from
pages, but instead from the fitz.DisplayList of the page. Each display list
will be stored in a list and looked up by page number. This way, zooming
pixmaps and page re-visits will re-use a once-created display list.
Improvements as of version 1.23.*
Supporting document types Mobi and text files.
Dependencies
------------
PyMuPDF v1.14.5+, PySimpleGUI, tkinter
"""
import sys
import fitz
print(fitz.__doc__)
if not tuple(map(int, fitz.VersionBind.split("."))) >= (1, 14, 5):
raise SystemExit("need PyMuPDF v1.14.5 for this script")
if sys.platform == "win32":
import ctypes
ctypes.windll.shcore.SetProcessDpiAwareness(2)
import PySimpleGUI as sg
import tkinter as tk
if len(sys.argv) == 1:
fname = sg.PopupGetFile(
"Select file and filetype to open:",
title="PyMuPDF Document Browser",
file_types=(
("PDF Files", "*.pdf"),
("XPS Files", "*.*xps"),
("Epub Files", "*.epub"),
("Mobi Files", "*.mobi"),
("Fiction Books", "*.fb2"),
("Comic Books", "*.cbz"),
("HTML", "*.htm*"),
("Text", "*.txt"),
("Text", "*.text"),
("Text", "*.log"),
("Text", "*.py"),
("Text", "*.h"),
("Text", "*.c"),
("Text", "*.cpp"),
("Text", "*.cs"),
# add more document types here
),
)
else:
fname = sys.argv[1]
if not fname:
sg.Popup("Cancelling:", "No filename supplied")
sys.exit("Cancelled: no filename supplied")
if fname.endswith((".py", ".c", ".cpp", ".cs", ".h")):
ftype = "txt"
else:
ftype = None
doc = fitz.open(fname, filetype=ftype)
if not ftype is None:
doc.layout(width=600, height=800, fontsize=12)
page_count = len(doc)
# allocate storage for page display lists
dlist_tab = [None] * page_count
title = "PyMuPDF display of '%s', pages: %i" % (fname, page_count)
# ------------------------------------------------------------------------------
# read the page data
# ------------------------------------------------------------------------------
def get_page(pno, zoom=False, max_size=None):
"""Return a tkinter.PhotoImage or a PNG image for a document page number.
:arg int pno: 0-based page number
:arg zoom: top-left of old clip rect, and one of -1, 0, +1 for dim. x or y
to indicate the arrow key pressed
:arg max_size: (width, height) of available image area
:arg bool first: if True, we cannot use tkinter
"""
dlist = dlist_tab[pno] # get display list of page number
if not dlist: # create if not yet there
dlist_tab[pno] = doc[pno].get_displaylist()
dlist = dlist_tab[pno]
r = dlist.rect # the page rectangle
clip = r
# ensure image fits screen:
# exploit, but do not exceed width or height
zoom_0 = 1
if max_size:
zoom_0 = min(1, max_size[0] / r.width, max_size[1] / r.height)
if zoom_0 == 1:
zoom_0 = min(max_size[0] / r.width, max_size[1] / r.height)
mat_0 = fitz.Matrix(zoom_0, zoom_0)
if not zoom: # show the total page
pix = dlist.get_pixmap(matrix=mat_0, alpha=False)
else:
w2 = r.width / 2 # we need these ...
h2 = r.height / 2 # a few times
clip = r * 0.5 # clip rect size is a quarter page
tl = zoom[0] # old top-left
tl.x += zoom[1] * (w2 / 2) # adjust topl-left ...
tl.x = max(0, tl.x) # according to ...
tl.x = min(w2, tl.x) # arrow key ...
tl.y += zoom[2] * (h2 / 2) # provided, but ...
tl.y = max(0, tl.y) # stay within ...
tl.y = min(h2, tl.y) # the page rect
clip = fitz.Rect(tl, tl.x + w2, tl.y + h2)
# clip rect is ready, now fill it
mat = mat_0 * fitz.Matrix(2, 2) # zoom matrix
pix = dlist.get_pixmap(alpha=False, matrix=mat, clip=clip)
img = pix.tobytes("ppm") # make PPM image from pixmap for tkinter
return img, clip.tl # return image, clip position
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# get physical screen dimension to determine the page image max size
# ------------------------------------------------------------------------------
root = tk.Tk()
max_width = root.winfo_screenwidth() - 20
max_height = root.winfo_screenheight() - 135
max_size = (max_width, max_height)
root.destroy()
del root
# ------------------------------------------------------------------------------
form = sg.FlexForm(
title, return_keyboard_events=True, location=(0, 0), use_default_focus=False
)
cur_page = 0
data, clip_pos = get_page(
cur_page, # read first page
zoom=False, # not zooming yet
max_size=max_size, # image max dim
)
image_elem = sg.Image(data=data) # make image element
goto = sg.InputText(
str(cur_page + 1), size=(5, 1), do_not_clear=True, key="PageNumber"
) # for display & input
layout = [ # the form layout
[
sg.ReadFormButton("Next"),
sg.ReadFormButton("Prior"),
sg.Text("Page:"),
goto,
sg.Text("(%i)" % page_count),
sg.ReadFormButton("Zoom"),
sg.Text("(toggle on/off, use arrows to navigate while zooming)"),
],
[image_elem],
]
form.Layout(layout) # define the form
# define the buttons / events we want to handle
def is_Enter(btn):
return btn.startswith("Return:") or btn == chr(13)
def is_Quit(btn):
return btn == chr(27) or btn.startswith("Escape:")
def is_Next(btn):
return btn.startswith("Next") or btn == "MouseWheel:Down"
def is_Prior(btn):
return btn.startswith("Prior") or btn == "MouseWheel:Up"
def is_Up(btn):
return btn.startswith("Up:")
def is_Down(btn):
return btn.startswith("Down:")
def is_Left(btn):
return btn.startswith("Left:")
def is_Right(btn):
return btn.startswith("Right:")
def is_Zoom(btn):
return btn.startswith("Zoom")
def is_MyKeys(btn):
return any((is_Enter(btn), is_Next(btn), is_Prior(btn), is_Zoom(btn)))
# old page store and zoom toggle
old_page = 0
old_zoom = False
zoom_active = False
while True:
btn, value = form.Read()
if btn is None and (value is None or value["PageNumber"] is None):
break
if is_Quit(btn):
break
zoom_pressed = False
zoom = False
if is_Enter(btn):
try:
cur_page = int(value["PageNumber"]) - 1 # check if valid
except:
cur_page = 0 # this guy's trying to fool me
elif is_Next(btn):
cur_page += 1
elif is_Prior(btn):
cur_page -= 1
elif is_Up(btn) and zoom_active:
zoom = (clip_pos, 0, -1)
elif is_Down(btn) and zoom_active:
zoom = (clip_pos, 0, 1)
elif is_Left(btn) and zoom_active:
zoom = (clip_pos, -1, 0)
elif is_Right(btn) and zoom_active:
zoom = (clip_pos, 1, 0)
elif is_Zoom(btn):
zoom_pressed = True
if not zoom_active:
zoom = (clip_pos, 0, 0)
# sanitize page number
while cur_page >= page_count: # wrap around
cur_page -= page_count
while cur_page < 0: # pages < 0 are valid but look bad
cur_page += page_count
if zoom_pressed and zoom_active:
zoom = zoom_pressed = zoom_active = False
data, clip_pos = get_page(cur_page, zoom=zoom, max_size=max_size)
image_elem.Update(data=data)
old_page = cur_page
old_zoom = zoom
zoom_active = zoom_pressed or zoom
# update page number field
if is_MyKeys(btn):
goto.Update(str(cur_page + 1))