diff --git a/Demo_PDF_Viewer.py b/Demo_PDF_Viewer.py index 85825bcc..ecd60c9c 100644 --- a/Demo_PDF_Viewer.py +++ b/Demo_PDF_Viewer.py @@ -1,79 +1,183 @@ +""" +@created: 2018-08-19 18:00:00 + +@author: (c) 2018 Jorj X. McKie + +Display a PyMuPDF Document using Tkinter +------------------------------------------------------------------------------- + +Dependencies: +------------- +PyMuPDF, PySimpleGUI > v2.9.0, Tkinter with Tk v8.6+, Python 3 + + +License: +-------- +GNU GPL V3+ + +Description +------------ +Read filename from command line and start display with page 1. +Pages can be directly jumped to, or buttons for paging can be used. +For experimental / demonstration purposes, we have included options to zoom +into the four page quadrants (top-left, bottom-right, etc.). + +We also interpret keyboard events to support paging by PageDown / PageUp +keys as if the resp. buttons were 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. 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 PySimpleGUI as sg +from binascii import hexlify -try: +if len(sys.argv) == 1: + rc, fname = sg.GetFileBox('PDF Browser', 'PDF file to open', file_types=(("PDF Files", "*.pdf"),)) + if rc is False: + sg.MsgBoxCancel('Cancelling') + exit(0) +else: fname = sys.argv[1] -except: - fname = 'C:/Python/PycharmProjects/GooeyGUI/test.pdf' + doc = fitz.open(fname) -title = "PyMuPDF display of '%s' (%i pages)" % (fname, len(doc)) +page_count = len(doc) -def get_page(pno, zoom = 0): - page = doc[pno] - r = page.rect - mp = r.tl + (r.br - r.tl) * 0.5 - mt = r.tl + (r.tr - r.tl) * 0.5 - ml = r.tl + (r.bl - r.tl) * 0.5 - mr = r.tr + (r.br - r.tr) * 0.5 - mb = r.bl + (r.br - r.bl) * 0.5 - mat = fitz.Matrix(2, 2) - if zoom == 1: +# 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=0): + """Return a PNG image for a document page number. If zoom is other than 0, one of the 4 page quadrants are zoomed-in instead and the corresponding clip returned. + + """ + dlist = dlist_tab[pno] # get display list + if not dlist: # create if not yet there + dlist_tab[pno] = doc[pno].getDisplayList() + dlist = dlist_tab[pno] + r = dlist.rect # page rectangle + mp = r.tl + (r.br - r.tl) * 0.5 # rect middle point + mt = r.tl + (r.tr - r.tl) * 0.5 # middle of top edge + ml = r.tl + (r.bl - r.tl) * 0.5 # middle of left edge + mr = r.tr + (r.br - r.tr) * 0.5 # middle of right egde + mb = r.bl + (r.br - r.bl) * 0.5 # middle of bottom edge + mat = fitz.Matrix(2, 2) # zoom matrix + if zoom == 1: # top-left quadrant clip = fitz.Rect(r.tl, mp) - elif zoom == 4: + elif zoom == 4: # bot-right quadrant clip = fitz.Rect(mp, r.br) - elif zoom == 2: + elif zoom == 2: # top-right clip = fitz.Rect(mt, mr) - elif zoom == 3: + elif zoom == 3: # bot-left clip = fitz.Rect(ml, mb) - if zoom == 0: - pix = page.getPixmap(alpha = False) + if zoom == 0: # total page + pix = dlist.getPixmap(alpha=False) else: - pix = page.getPixmap(alpha = False, matrix = mat, clip = clip) - return pix.getPNGData() + pix = dlist.getPixmap(alpha=False, matrix=mat, clip=clip) + return pix.getPNGData() # return the PNG image -form = sg.FlexForm(title, return_keyboard_events=True) -data = get_page(0) +form = sg.FlexForm(title, return_keyboard_events=True, use_default_focus=False) + +cur_page = 0 +data = get_page(cur_page) # show page 1 for start image_elem = sg.Image(data=data) -layout = [ [image_elem], - [sg.ReadFormButton('Next'), - sg.ReadFormButton('Prev'), - sg.ReadFormButton('First'), - sg.ReadFormButton('Last'), - sg.ReadFormButton('Zoom-1'), - sg.ReadFormButton('Zoom-2'), - sg.ReadFormButton('Zoom-3'), - sg.ReadFormButton('Zoom-4'), - sg.Quit()] ] +goto = sg.InputText(str(cur_page + 1), size=(5, 1), do_not_clear=True) + +layout = [ + [ + sg.ReadFormButton('Next'), + sg.ReadFormButton('Prev'), + sg.Text('Page:'), + goto, + ], + [ + sg.Text("Zoom:"), + sg.ReadFormButton('Top-L'), + sg.ReadFormButton('Top-R'), + sg.ReadFormButton('Bot-L'), + sg.ReadFormButton('Bot-R'), + ], + [image_elem], +] form.Layout(layout) +my_keys = ("Next", "Next:34", "Prev", "Prior:33", "Top-L", "Top-R", + "Bot-L", "Bot-R", "MouseWheel:Down", "MouseWheel:Up") +zoom_buttons = ("Top-L", "Top-R", "Bot-L", "Bot-R") + +old_page = 0 +old_zoom = 0 # used for zoom on/off +# the zoom buttons work in on/off mode. -i = oldzoom = 0 while True: - button,value = form.Read() + button, value = form.ReadNonBlocking() zoom = 0 - if button in (None, 'Quit'): + force_page = False + if button is None and value is None: break - if button in ("Next", 'Next:34'): - i += 1 - elif button in ("Prev", "Prior:33"): - i -= 1 - elif button == "First": - i = 0 - elif button == "Last": - i = -1 - elif button == "Zoom-1": - zoom = oldzoom = 0 if oldzoom == 1 else 1 - elif button == "Zoom-2": - zoom = oldzoom = 0 if oldzoom == 2 else 2 - elif button == "Zoom-3": - zoom = oldzoom = 0 if oldzoom == 3 else 3 - elif button == "Zoom-4": - zoom = oldzoom = 0 if oldzoom == 4 else 4 - try: - data = get_page(i, zoom) - except: - i = 0 - data = get_page(i, zoom) - image_elem.Update(data=data) + if button is None: + continue + + if button in ("Escape:27"): # this spares me a 'Quit' button! + break + # print("hex(button)", hexlify(button.encode())) + if button[0] == chr(13): # surprise: this is 'Enter'! + try: + cur_page = int(value[0]) - 1 # check if valid + while cur_page < 0: + cur_page += page_count + except: + cur_page = 0 # this guy's trying to fool me + goto.Update(str(cur_page + 1)) + # goto.TKStringVar.set(str(cur_page + 1)) + + elif button in ("Next", "Next:34", "MouseWheel:Down"): + cur_page += 1 + elif button in ("Prev", "Prior:33", "MouseWheel:Up"): + cur_page -= 1 + elif button == "Top-L": + zoom = 1 + elif button == "Top-R": + zoom = 2 + elif button == "Bot-L": + zoom = 3 + elif button == "Bot-R": + zoom = 4 + + # sanitize page number + if cur_page >= page_count: # wrap around + cur_page = 0 + while cur_page < 0: # we show conventional page numbers + cur_page += page_count + + # prevent creating same data again + if cur_page != old_page: + zoom = old_zoom = 0 + force_page = True + + if button in zoom_buttons: + if 0 < zoom == old_zoom: + zoom = 0 + force_page = True + + if zoom != old_zoom: + force_page = True + + if force_page: + data = get_page(cur_page, zoom) + image_elem.Update(data=data) + old_page = cur_page + old_zoom = zoom + + # update page number field + if button in my_keys or not value[0]: + goto.Update(str(cur_page + 1)) + # goto.TKStringVar.set(str(cur_page + 1)) diff --git a/PySimpleGUI.py b/PySimpleGUI.py index 751ef6de..c69c74d1 100644 --- a/PySimpleGUI.py +++ b/PySimpleGUI.py @@ -1027,7 +1027,6 @@ class FlexForm: return BuildResults(self, False, self) def KeyboardCallback(self, event ): - # print(".",) self.LastButtonClicked = None self.FormRemainedOpen = True if event.char != '': @@ -1038,6 +1037,16 @@ class FlexForm: results = BuildResults(self, False, self) self.TKroot.quit() + def MouseWheelCallback(self, event ): + self.LastButtonClicked = None + self.FormRemainedOpen = True + # print(ObjToStringSingleObj(event)) + direction = 'Down' if event.delta < 0 else 'Up' + self.LastKeyboardEvent = 'MouseWheel:' + direction + if not self.NonBlocking: + results = BuildResults(self, False, self) + self.TKroot.quit() + def _Close(self): try: @@ -1796,8 +1805,10 @@ def StartupTK(my_flex_form): my_flex_form.SetIcon(my_flex_form.WindowIcon) if my_flex_form.ReturnKeyboardEvents and not my_flex_form.NonBlocking: root.bind("", my_flex_form.KeyboardCallback) + root.bind("", my_flex_form.MouseWheelCallback) elif my_flex_form.ReturnKeyboardEvents: root.bind("", my_flex_form.KeyboardCallback) + root.bind("", my_flex_form.MouseWheelCallback) if my_flex_form.AutoClose: duration = DEFAULT_AUTOCLOSE_TIME if my_flex_form.AutoCloseDuration is None else my_flex_form.AutoCloseDuration