PySimpleGUI/DemoPrograms/Demo_DOC_Viewer_PIL.py

238 lines
7.5 KiB
Python
Raw Permalink Normal View History

2018-09-04 16:55:25 +00:00
"""
@created: 2018-08-19 18:00:00
@author: (c) 2018 Jorj X. McKie
Display a PyMuPDF Document using Tkinter
-------------------------------------------------------------------------------
Dependencies:
-------------
PyMuPDF, PySimpleGUI (requires Python 3), Tkinter, PIL
License:
--------
GNU GPL V3+
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:
* Use of PIL improves response times by a factor 3 or more.
* Zooming is now flexible: only one button serves as a toggle. Arrow keys can
be used for moving 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 window.
2018-09-04 16:55:25 +00:00
To improve paging performance, we are not directly creating pixmaps from
pages, but instead from the fitz.DisplayList of the page. A 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.
"""
import sys
import fitz
import sys
import PySimpleGUI as sg
2018-09-04 16:55:25 +00:00
import tkinter as tk
from PIL import Image, ImageTk
import time
if len(sys.argv) == 1:
fname = sg.popup_get_file('Document Browser', 'Document file to open',
no_window=True,
file_types=(
("PDF Files", "*.pdf"),
("XPS Files", "*.*xps"),
("Epub Files", "*.epub"),
("Fiction Books", "*.fb2"),
("Comic Books", "*.cbz"),
("HTML", "*.htm*")
# add more document types here
)
)
2018-09-04 16:55:25 +00:00
else:
fname = sys.argv[1]
if not fname:
sg.popup("Cancelling:", "No filename supplied")
2018-09-04 16:55:25 +00:00
raise SystemExit("Cancelled: no filename supplied")
doc = fitz.open(fname)
page_count = len(doc)
# used for response time statistics only
fitz_img_time = 0.0
tk_img_time = 0.0
img_count = 1
2018-09-04 16:55:25 +00:00
# allocate storage for page display lists
dlist_tab = [None] * page_count
title = "PyMuPDF display of '%s', pages: %i" % (fname, page_count)
def get_page(pno, zoom=False, max_size=None, first=False):
2018-09-04 16:55:25 +00:00
"""Return a PNG image for a document page number.
"""
dlist = dlist_tab[pno] # get display list of page number
if not dlist: # create if not yet there
dlist_tab[pno] = doc[pno].getDisplayList()
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 total page
pix = dlist.getPixmap(matrix=mat_0, alpha=False)
2018-09-04 16:55:25 +00:00
else:
mp = r.tl + (r.br - r.tl) * 0.5 # page rect center
w2 = r.width / 2
h2 = r.height / 2
clip = r * 0.5
tl = zoom[0] # old top-left
tl.x += zoom[1] * (w2 / 2)
tl.x = max(0, tl.x)
tl.x = min(w2, tl.x)
tl.y += zoom[2] * (h2 / 2)
tl.y = max(0, tl.y)
tl.y = min(h2, tl.y)
clip = fitz.Rect(tl, tl.x + w2, tl.y + h2)
mat = mat_0 * fitz.Matrix(2, 2) # zoom matrix
pix = dlist.getPixmap(alpha=False, matrix=mat, clip=clip)
if first: # first call: tkinter still inactive
img = pix.getPNGData() # so use fitz png output
else: # else take tk photo image
pilimg = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
img = ImageTk.PhotoImage(pilimg)
return img, clip.tl # return image, clip position
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
cur_page = 0
data, clip_pos = get_page(cur_page, zoom=False, max_size=max_size, first=True)
2018-09-04 16:55:25 +00:00
image_elem = sg.Image(data=data)
2018-09-04 16:55:25 +00:00
goto = sg.InputText(str(cur_page + 1), size=(5, 1), key="-PageNumber-")
2018-09-04 16:55:25 +00:00
layout = [[
sg.ReadButton('Next'),
sg.ReadButton('Prev'),
2018-09-04 16:55:25 +00:00
sg.Text('Page:'),
goto,
sg.Text('(%i)' % page_count),
sg.ReadButton('Zoom'),
2018-09-04 16:55:25 +00:00
sg.Text('(toggle on/off, use arrows to navigate while zooming)'),
],
[image_elem],
]
window = sg.Window(title, layout, return_keyboard_events=True,
location=(0, 0), use_default_focus=False, no_titlebar=False)
2018-09-04 16:55:25 +00:00
# now define the buttons / events we want to handle
enter_buttons = [chr(13), "Return:13"]
quit_buttons = ["Escape:27", chr(27)]
next_buttons = ["Next", "Next:34", "MouseWheel:Down"]
prev_buttons = ["Prev", "Prior:33", "MouseWheel:Up"]
Up = "Up:38"
Left = "Left:37"
2018-09-04 16:55:25 +00:00
Right = "Right:39"
Down = "Down:40"
2018-09-04 16:55:25 +00:00
zoom_buttons = ["Zoom", Up, Down, Left, Right]
# all the buttons we will handle
my_keys = enter_buttons + next_buttons + prev_buttons + zoom_buttons
# old page store and zoom toggle
old_page = 0
old_zoom = False
while True:
event, value = window.read()
if event == sg.WIN_CLOSED and (value is None or value['-PageNumber-'] is None):
2018-09-04 16:55:25 +00:00
break
if event in quit_buttons:
2018-09-04 16:55:25 +00:00
break
zoom_pressed = False
zoom = False
if event in enter_buttons:
2018-09-04 16:55:25 +00:00
try:
cur_page = int(value['-PageNumber-']) - 1 # check if valid
2018-09-04 16:55:25 +00:00
while cur_page < 0:
cur_page += page_count
except:
cur_page = 0 # this guy's trying to fool me
elif event in next_buttons:
2018-09-04 16:55:25 +00:00
cur_page += 1
elif event in prev_buttons:
2018-09-04 16:55:25 +00:00
cur_page -= 1
elif event == Up:
2018-09-04 16:55:25 +00:00
zoom = (clip_pos, 0, -1)
elif event == Down:
2018-09-04 16:55:25 +00:00
zoom = (clip_pos, 0, 1)
elif event == Left:
2018-09-04 16:55:25 +00:00
zoom = (clip_pos, -1, 0)
elif event == Right:
2018-09-04 16:55:25 +00:00
zoom = (clip_pos, 1, 0)
elif event == "Zoom":
2018-09-04 16:55:25 +00:00
zoom_pressed = True
zoom = (clip_pos, 0, 0)
# sanitize page number
if cur_page >= page_count: # wrap around
cur_page = 0
while cur_page < 0: # pages > 0 look nicer
cur_page += page_count
if zoom_pressed and old_zoom:
zoom = zoom_pressed = old_zoom = False
t0 = time.perf_counter()
data, clip_pos = get_page(cur_page, zoom=zoom, max_size=max_size,
first=False)
2018-09-04 16:55:25 +00:00
t1 = time.perf_counter()
image_elem.update(data=data)
2018-09-04 16:55:25 +00:00
t2 = time.perf_counter()
fitz_img_time += t1 - t0
tk_img_time += t2 - t1
img_count += 1
2018-09-04 16:55:25 +00:00
old_page = cur_page
old_zoom = zoom_pressed or zoom
# update page number field
if event in my_keys:
goto.update(str(cur_page + 1))
2018-09-04 16:55:25 +00:00
window.close()
2018-09-04 16:55:25 +00:00
# print some response time statistics
if img_count > 0:
print("response times for '%s'" % doc.name)
print("%.4f" % (fitz_img_time/img_count), "sec fitz avg. image time")
print("%.4f" % (tk_img_time/img_count), "sec tk avg. image time")
print("%.4f" % ((fitz_img_time + tk_img_time)/img_count), "sec avg. total time")
print(img_count, "images read")
print(page_count, "pages")