1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3#
4# Impressive, a fancy presentation tool
5# Copyright (C) 2005-2019 Martin J. Fiedler <martin.fiedler@gmx.net>
6#                         and contributors
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License, version 2, as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21from __future__ import print_function, division, unicode_literals
22
23__title__   = "Impressive"
24__version__ = "0.13.0-beta2"
25__rev__     = 298
26__author__  = "Martin J. Fiedler"
27__email__   = "martin.fiedler@gmx.net"
28__website__ = "http://impressive.sourceforge.net/"
29
30import sys
31if __rev__ and (("WIP" in __version__) or ("rc" in __version__) or ("alpha" in __version__) or ("beta" in __version__)):
32    __version__ += " (SVN r%s)" % __rev__
33def greet():
34    print("Welcome to", __title__, "version", __version__, file=sys.stderr)
35if __name__ == "__main__":
36    greet()
37
38def execfile(f, c):
39    with open(f, 'rb') as h:
40        code = compile(h.read(), f, 'exec')
41        exec(code, c)
42
43TopLeft, BottomLeft, TopRight, BottomRight, TopCenter, BottomCenter = range(6)
44NoCache, MemCache, CompressedCache, FileCache, PersistentCache = range(5)  # for CacheMode
45Off, First, Last = range(3)  # for AutoOverview
46
47# You may change the following lines to modify the default settings
48Verbose = False
49Fullscreen = True
50FakeFullscreen = False
51Scaling = False
52Supersample = None
53BackgroundRendering = True
54PDFRendererPath = None
55UseAutoScreenSize = True
56ScreenWidth = 1024
57ScreenHeight = 768
58WindowPos = None
59TransitionDuration = 1000
60MouseHideDelay = 3000
61BoxFadeDuration = 100
62ZoomDuration = 250
63OverviewDuration = 250
64BlankFadeDuration = 250
65BoxFadeBlur = 1.5
66BoxFadeDarkness = 0.25
67BoxFadeDarknessStep = 0.02
68BoxZoomDarkness = 0.96
69MarkColor = (1.0, 0.0, 0.0, 0.1)
70BoxEdgeSize = 4
71ZoomBoxEdgeSize = 1
72SpotRadius = 64
73MinSpotDetail = 13
74SpotDetail = 12
75CacheMode = FileCache
76HighQualityOverview = True
77OverviewBorder = 3
78OverviewLogoBorder = 24
79AutoOverview = Off
80EnableOverview = True
81InitialPage = None
82Wrap = False
83AutoAdvanceTime = 30000
84AutoAdvanceEnabled = False
85AutoAutoAdvance = False
86RenderToDirectory = None
87Rotation = 0
88DAR = None
89PAR = 1.0
90Overscan = 3
91PollInterval = 0
92PageRangeStart = 0
93PageRangeEnd = 999999
94FontSize = 14
95FontTextureWidth = 512
96FontTextureHeight = 256
97Gamma = 1.0
98BlackLevel = 0
99GammaStep = 1.1
100BlackLevelStep = 8
101EstimatedDuration = None
102PageProgress = False
103ProgressLast = None
104AutoAdvanceProgress = False
105ProgressBarSizeFactor = 0.02
106ProgressBarAlpha = 0.5
107ProgressBarColorNormal = (0.0, 1.0, 0.0)
108ProgressBarColorWarning = (1.0, 1.0, 0.0)
109ProgressBarColorCritical = (1.0, 0.0, 0.0)
110ProgressBarColorPage = (0.0, 0.5, 1.0)
111ProgressBarWarningFactor = 1.25
112ProgressBarCriticalFactor = 1.5
113EnableCursor = True
114CursorImage = None
115CursorHotspot = (0, 0)
116MinutesOnly = False
117OSDMargin = 16
118OSDAlpha = 1.0
119OSDTimePos = TopRight
120OSDTitlePos = BottomLeft
121OSDPagePos = BottomRight
122OSDStatusPos = TopLeft
123DefaultZoomFactor = 2
124MaxZoomFactor = 5
125MouseWheelZoom = False
126ZoomStep = 2.0 ** (1.0 / 4)
127WheelZoomDuration = 30
128FadeInOut = False
129ShowLogo = True
130Shuffle = False
131QuitAtEnd = False
132ShowClock = False
133HalfScreen = False
134InvertPages = False
135MinBoxSize = 20
136UseBlurShader = True
137TimeTracking = False
138EventTestMode = False
139Bare = False
140Win32FullscreenVideoHackTiming = [0, 0]
141
142
143# import basic modules
144import random, getopt, os, re, codecs, tempfile, glob, io, re, hashlib
145import traceback, subprocess, time, itertools, ctypes.util, zlib, urllib
146from math import *
147from ctypes import *
148
149# initialize some platform-specific settings
150if os.name == "nt":
151    root = os.path.split(sys.argv[0])[0] or "."
152    _find_paths = [root, os.path.join(root, "win32"), os.path.join(root, "gs")] + list(filter(None, os.getenv("PATH").split(';')))
153    def FindBinary(binary):
154        if not binary.lower().endswith(".exe"):
155            binary += ".exe"
156        for p in _find_paths:
157            path = os.path.join(p, binary)
158            if os.path.isfile(path):
159                return os.path.abspath(path)
160        return binary  # fall-back if not found
161    pdftkPath = FindBinary("pdftk.exe")
162    mutoolPath = FindBinary("mutool.exe")
163    ffmpegPath = FindBinary("ffmpeg.exe")
164    GhostScriptPlatformOptions = ["-I" + os.path.join(root, "gs")]
165    try:
166        import win32api, win32gui
167        HaveWin32API = True
168        MPlayerPath = FindBinary("mplayer.exe")
169        def RunURL(url):
170            win32api.ShellExecute(0, "open", url, "", "", 0)
171    except ImportError:
172        HaveWin32API = False
173        MPlayerPath = ""
174        def RunURL(url): print("Error: cannot run URL `%s'" % url)
175    if getattr(sys, "frozen", False):
176        sys.path.append(root)
177    FontPath = []
178    FontList = ["verdana.ttf", "arial.ttf"]
179    Nice = []
180    try:
181        dpiOK = (WinDLL("shcore").SetProcessDpiAwareness(2) == 0)  # PROCESS_PER_MONITOR_DPI_AWARE
182    except:
183        dpiOK = False
184    if not dpiOK:
185        try:
186            WinDLL("user32").SetProcessDPIAware()
187        except:
188            pass
189else:
190    def FindBinary(x): return x
191    GhostScriptPlatformOptions = []
192    MPlayerPath = "mplayer"
193    pdftkPath = "pdftk"
194    mutoolPath = "mutool"
195    ffmpegPath = "ffmpeg"
196    FontPath = ["/usr/share/fonts", "/usr/local/share/fonts", "/usr/X11R6/lib/X11/fonts/TTF"]
197    FontList = ["DejaVuSans.ttf", "Vera.ttf", "Verdana.ttf"]
198    Nice = ["nice", "-n", "7"]
199    def RunURL(url):
200        try:
201            Popen(["xdg-open", url])
202        except OSError:
203            print("Error: cannot open URL `%s'" % url, file=sys.stderr)
204
205# import special modules
206try:
207    import pygame
208    from pygame.locals import *
209    from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageChops, ImageOps
210    from PIL import TiffImagePlugin, BmpImagePlugin, JpegImagePlugin, PngImagePlugin, PpmImagePlugin
211except (ValueError, ImportError) as err:
212    print("Oops! Cannot load necessary modules:", err, file=sys.stderr)
213    print("""To use Impressive, you need to install the following Python modules:
214 - PyGame   [python-pygame]   http://www.pygame.org/
215 - PIL      [python-imaging]  http://www.pythonware.com/products/pil/
216   or Pillow                  http://pypi.python.org/pypi/Pillow/
217 - PyWin32  (OPTIONAL, Win32) http://sourceforge.net/projects/pywin32/
218Additionally, please be sure to have mupdf-tools and pdftk installed if you
219intend to use PDF input.""", file=sys.stderr)
220    sys.exit(1)
221
222# Python 2/3 compatibility fixes
223try:  # Python 2 path
224    basestring  # only exists in Python 2
225    def Popen(cmdline, *args, **kwargs):
226        # Python 2's subprocess.Popen needs manual unicode->str conversion
227        enc = sys.getfilesystemencoding()
228        cmdline = [arg.encode(enc, 'replace') for arg in cmdline]
229        return subprocess.Popen(cmdline, *args, **kwargs)
230except:  # Python 3 path
231    basestring = str
232    Popen = subprocess.Popen
233    raw_input = input
234
235try:
236    try:
237        import thread
238    except ImportError:
239        import _thread as thread
240    HaveThreads = True
241    def create_lock(): return thread.allocate_lock()
242    def get_thread_id(): return thread.get_ident()
243except ImportError:
244    HaveThreads = False
245    class pseudolock:
246        def __init__(self): self.state = False
247        def acquire(self, dummy=0): self.state = True
248        def release(self): self.state = False
249        def locked(self): return self.state
250    def create_lock(): return pseudolock()
251    def get_thread_id(): return 0xDEADC0DE
252
253CleanExit = False
254
255
256##### GLOBAL VARIABLES #########################################################
257
258# initialize private variables
259DocumentTitle = None
260FileName = ""
261FileList = []
262InfoScriptPath = None
263AvailableRenderers = []
264PDFRenderer = None
265BaseWorkingDir = '.'
266Marking = False
267Tracing = False
268Panning = False
269FileProps = {}
270PageProps = {}
271PageCache = {}
272CacheFile = None
273CacheFileName = None
274CacheFilePos = 0
275CacheMagic = ""
276MPlayerProcess = None
277VideoPlaying = False
278MarkValid, MarkBaseX, MarkBaseY = False, 0, 0
279PanValid, PanBaseX, PanBaseY = False, 0, 0
280MarkUL = (0, 0)
281MarkLR = (0, 0)
282ZoomX0 = 0.0
283ZoomY0 = 0.0
284ZoomArea = 1.0
285ZoomMode = False
286BoxZoom = False  # note: when active, contains the box coordinates
287IsZoomed = 0
288ViewZoomFactor = 1
289ResZoomFactor = 1
290HighResZoomFailed = False
291TransitionRunning = False
292TransitionDone = False
293TransitionPhase = 0.0
294CurrentCaption = 0
295OverviewNeedUpdate = False
296FileStats = None
297OSDFont = None
298CurrentOSDCaption = ""
299CurrentOSDPage = ""
300CurrentOSDStatus = ""
301CurrentOSDComment = ""
302Lrender = create_lock()
303Lcache = create_lock()
304Loverview = create_lock()
305RTrunning = False
306RTrestart = False
307StartTime = 0
308CurrentTime = 0
309PageEnterTime = 0
310PageLeaveTime = 0
311PageTimeout = 0
312NextPageAfterVideo = False
313TimeDisplay = False
314FirstPage = True
315ProgressBarPos = 0
316CursorVisible = True
317OverviewMode = False
318LastPage = 0
319WantStatus = False
320GLVendor = ""
321GLRenderer = ""
322GLVersion = ""
323RequiredShaders = []
324DefaultScreenTransform = (-1.0, 1.0, 2.0, -2.0)
325ScreenTransform = DefaultScreenTransform
326SpotVertices = None
327SpotIndices = None
328CallQueue = []
329
330# tool constants (used in info scripts)
331FirstTimeOnly = 2
332
333
334##### PLATFORM-SPECIFIC PYGAME INTERFACE CODE ##################################
335
336class Platform_PyGame(object):
337    name = 'pygame'
338    allow_custom_fullscreen_res = True
339    has_hardware_cursor = True
340    use_omxplayer = False
341
342    _buttons = { 1: "lmb", 2: "mmb", 3: "rmb", 4: "wheelup", 5: "wheeldown" }
343    _keys = dict((getattr(pygame.locals, k), k[2:].lower()) for k in [k for k in dir(pygame.locals) if k.startswith('K_')])
344
345    def __init__(self):
346        self.next_events = []
347        self.schedule_map_ev2flag = {}
348        self.schedule_map_ev2name = {}
349        self.schedule_map_name2ev = {}
350        self.schedule_max = USEREVENT
351
352    def Init(self):
353        os.environ["SDL_MOUSE_RELATIVE"] = "0"
354        pygame.display.init()
355
356    def GetTicks(self):
357        return pygame.time.get_ticks()
358
359    def GetScreenSize(self):
360        return pygame.display.list_modes()[0]
361
362    def StartDisplay(self):
363        global ScreenWidth, ScreenHeight, Fullscreen, FakeFullscreen, WindowPos
364        pygame.display.set_caption(__title__)
365        flags = OPENGL | DOUBLEBUF
366        if Fullscreen:
367            if FakeFullscreen:
368                print("Using \"fake-fullscreen\" mode.", file=sys.stderr)
369                flags |= NOFRAME
370                if not WindowPos:
371                    WindowPos = (0,0)
372            else:
373                flags |= FULLSCREEN
374        if WindowPos:
375            os.environ["SDL_VIDEO_WINDOW_POS"] = ','.join(map(str, WindowPos))
376        pygame.display.set_mode((ScreenWidth, ScreenHeight), flags)
377        pygame.key.set_repeat(500, 30)
378
379    def LoadOpenGL(self):
380        sdl = None
381
382        # PyGame installations done with pip may come with its own SDL library,
383        # in which case we must not use the default system-wide SDL;
384        # so we need to find out the local library's path
385        try:
386            pattern = re.compile(r'(lib)?SDL(?!_[a-zA-Z]+).*?\.(dll|so(\..*)?|dylib)$', re.I)
387            libs = []
388            for suffix in (".libs", ".dylibs"):
389                libdir = os.path.join(pygame.__path__[0], suffix)
390                if os.path.isdir(libdir):
391                    libs += [os.path.join(libdir, lib) for lib in sorted(os.listdir(libdir)) if pattern.match(lib)]
392            sdl = libs.pop(0)
393        except (IndexError, AttributeError, EnvironmentError):
394            pass
395
396        # generic case: load the system-wide SDL
397        sdl = sdl or ctypes.util.find_library("SDL") or ctypes.util.find_library("SDL-1.2") or "SDL"
398
399        # load the library
400        try:
401            sdl = CDLL(sdl, RTLD_GLOBAL)
402            get_proc_address = CFUNCTYPE(c_void_p, c_char_p)(('SDL_GL_GetProcAddress', sdl))
403        except OSError:
404            raise ImportError("failed to load the SDL library")
405        except AttributeError:
406            raise ImportError("failed to load SDL_GL_GetProcAddress from the SDL library")
407
408        # load the symbols
409        def loadsym(name, prototype):
410            try:
411                addr = get_proc_address(name.encode())
412            except EnvironmentError:
413                return None
414            if not addr:
415                return None
416            return prototype(addr)
417        return OpenGL(loadsym, desktop=True)
418
419    def SwapBuffers(self):
420        pygame.display.flip()
421
422    def Done(self):
423        pygame.display.quit()
424    def Quit(self):
425        pygame.quit()
426
427    def SetWindowTitle(self, text):
428        try:
429            pygame.display.set_caption(text, __title__)
430        except UnicodeEncodeError:
431            pygame.display.set_caption(text.encode('utf-8'), __title__)
432    def GetWindowID(self):
433        return pygame.display.get_wm_info()['window']
434
435    def GetMousePos(self):
436        return pygame.mouse.get_pos()
437    def SetMousePos(self, coords):
438        pygame.mouse.set_pos(coords)
439    def SetMouseVisible(self, visible):
440        pygame.mouse.set_visible(visible)
441
442    def _translate_mods(self, key, mods):
443        if mods & KMOD_SHIFT:
444            key = "shift+" + key
445        if mods & KMOD_ALT:
446            key = "alt+" + key
447        if mods & KMOD_CTRL:
448            key = "ctrl+" + key
449        return key
450    def _translate_button(self, ev):
451        try:
452            return self._translate_mods(self._buttons[ev.button], pygame.key.get_mods())
453        except KeyError:
454            return 'btn' + str(ev.button)
455    def _translate_key(self, ev):
456        try:
457            return self._translate_mods(self._keys[ev.key], ev.mod)
458        except KeyError:
459            return 'unknown-key-' + str(ev.key)
460
461    def _translate_event(self, ev):
462        if ev.type == QUIT:
463            return ["$quit"]
464        elif ev.type == VIDEOEXPOSE:
465            return ["$expose"]
466        elif ev.type == MOUSEBUTTONDOWN:
467            return ['+' + self._translate_button(ev)]
468        elif ev.type == MOUSEBUTTONUP:
469            ev = self._translate_button(ev)
470            return ['*' + ev, '-' + ev]
471        elif ev.type == MOUSEMOTION:
472            pygame.event.clear(MOUSEMOTION)
473            return ["$move"]
474        elif ev.type == KEYDOWN:
475            if ev.mod & KMOD_ALT:
476                if ev.key == K_F4:
477                    return self.PostQuitEvent()
478                elif ev.key == K_TAB:
479                    return "$alt-tab"
480            ev = self._translate_key(ev)
481            return ['+' + ev, '*' + ev]
482        elif ev.type == KEYUP:
483            return ['-' + self._translate_key(ev)]
484        elif (ev.type >= USEREVENT) and (ev.type < self.schedule_max):
485            if not(self.schedule_map_ev2flag.get(ev.type)):
486                pygame.time.set_timer(ev.type, 0)
487            return [self.schedule_map_ev2name.get(ev.type)]
488        else:
489            return []
490
491    def GetEvent(self, poll=False):
492        if self.next_events:
493            return self.next_events.pop(0)
494        if poll:
495            ev = pygame.event.poll()
496        else:
497            ev = pygame.event.wait()
498        evs = self._translate_event(ev)
499        if evs:
500            self.next_events.extend(evs[1:])
501            return evs[0]
502
503    def CheckAnimationCancelEvent(self):
504        while True:
505            ev = pygame.event.poll()
506            if ev.type == NOEVENT:
507                break
508            self.next_events.extend(self._translate_event(ev))
509            if ev.type in set([KEYDOWN, MOUSEBUTTONUP, QUIT]):
510                return True
511
512    def ScheduleEvent(self, name, msec=0, periodic=False):
513        try:
514            ev_code = self.schedule_map_name2ev[name]
515        except KeyError:
516            ev_code = self.schedule_max
517            self.schedule_map_name2ev[name] = ev_code
518            self.schedule_map_ev2name[ev_code] = name
519            self.schedule_max += 1
520        self.schedule_map_ev2flag[ev_code] = periodic
521        pygame.time.set_timer(ev_code, msec)
522
523    def PostQuitEvent(self):
524        pygame.event.post(pygame.event.Event(QUIT))
525
526    def ToggleFullscreen(self):
527        return pygame.display.toggle_fullscreen()
528
529    def Minimize(self):
530        pygame.display.iconify()
531
532    def SetGammaRamp(self, gamma, black_level):
533        scale = 1.0 / (255 - black_level)
534        power = 1.0 / gamma
535        ramp = [int(65535.0 * ((max(0, x - black_level) * scale) ** power)) for x in range(256)]
536        return pygame.display.set_gamma_ramp(ramp, ramp, ramp)
537
538
539class Platform_Win32(Platform_PyGame):
540    name = 'pygame-win32'
541
542    def GetScreenSize(self):
543        if HaveWin32API:
544            dm = win32api.EnumDisplaySettings(None, -1) #ENUM_CURRENT_SETTINGS
545            return (int(dm.PelsWidth), int(dm.PelsHeight))
546        return Platform_PyGame.GetScreenSize(self)
547
548    def LoadOpenGL(self):
549        try:
550            opengl32 = WinDLL("opengl32")
551            get_proc_address = WINFUNCTYPE(c_void_p, c_char_p)(('wglGetProcAddress', opengl32))
552        except OSError:
553            raise ImportError("failed to load the OpenGL library")
554        except AttributeError:
555            raise ImportError("failed to load wglGetProcAddress from the OpenGL library")
556        def loadsym(name, prototype):
557            # try to load OpenGL 1.1 function from opengl32.dll first
558            try:
559                return prototype((name, opengl32))
560            except AttributeError:
561                pass
562            # if that fails, load the extension function via wglGetProcAddress
563            try:
564                addr = get_proc_address(name.encode())
565            except EnvironmentError:
566                addr = None
567            if not addr:
568                return None
569            return prototype(addr)
570        return OpenGL(loadsym, desktop=True)
571
572
573class Platform_Unix(Platform_PyGame):
574    name = 'pygame-unix'
575
576    def GetScreenSize(self):
577        re_res = re.compile(r'\s*(\d+)x(\d+)\s+\d+\.\d+\*')
578        res = None
579        try:
580            xrandr = Popen(["xrandr"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
581            for line in xrandr.stdout:
582                m = re_res.match(line.decode())
583                if m:
584                    res = tuple(map(int, m.groups()))
585            xrandr.wait()
586        except OSError:
587            pass
588        if res:
589            return res
590        return Platform_PyGame.GetScreenSize(self)
591
592
593class Platform_RasPi4(Platform_Unix):
594    use_omxplayer = True
595
596
597class Platform_EGL(Platform_Unix):
598    name = 'egl'
599    egllib = "EGL"
600    gles2lib = "GLESv2"
601
602    def StartDisplay(self, display=None, window=None, width=None, height=None):
603        global ScreenWidth, ScreenHeight
604        width  = width  or ScreenWidth
605        height = height or ScreenHeight
606
607        # load the GLESv2 library before the EGL library (required on the BCM2835)
608        try:
609            self.gles = ctypes.CDLL(ctypes.util.find_library(self.gles2lib))
610        except OSError:
611            raise ImportError("failed to load the OpenGL ES 2.0 library")
612
613        # import all functions first
614        try:
615            egl = CDLL(ctypes.util.find_library(self.egllib))
616            def loadfunc(func, ret, *args):
617                return CFUNCTYPE(ret, *args)((func, egl))
618            eglGetDisplay = loadfunc("eglGetDisplay", c_void_p, c_void_p)
619            eglInitialize = loadfunc("eglInitialize", c_uint, c_void_p, POINTER(c_int), POINTER(c_int))
620            eglChooseConfig = loadfunc("eglChooseConfig", c_uint, c_void_p, c_void_p, POINTER(c_void_p), c_int, POINTER(c_int))
621            eglCreateWindowSurface = loadfunc("eglCreateWindowSurface", c_void_p, c_void_p, c_void_p, c_void_p, c_void_p)
622            eglCreateContext = loadfunc("eglCreateContext", c_void_p, c_void_p, c_void_p, c_void_p, c_void_p)
623            eglMakeCurrent = loadfunc("eglMakeCurrent", c_uint, c_void_p, c_void_p, c_void_p, c_void_p)
624            self.eglSwapBuffers = loadfunc("eglSwapBuffers", c_int, c_void_p, c_void_p)
625        except OSError:
626            raise ImportError("failed to load the EGL library")
627        except AttributeError:
628            raise ImportError("failed to load required symbols from the EGL library")
629
630        # prepare parameters
631        config_attribs = [
632            0x3024, 8,      # EGL_RED_SIZE >= 8
633            0x3023, 8,      # EGL_GREEN_SIZE >= 8
634            0x3022, 8,      # EGL_BLUE_SIZE >= 8
635            0x3021, 0,      # EGL_ALPHA_SIZE >= 0
636            0x3025, 0,      # EGL_DEPTH_SIZE >= 0
637            0x3040, 0x0004, # EGL_RENDERABLE_TYPE = EGL_OPENGL_ES2_BIT
638            0x3033, 0x0004, # EGL_SURFACE_TYPE = EGL_WINDOW_BIT
639            0x3038          # EGL_NONE
640        ]
641        context_attribs = [
642            0x3098, 2,      # EGL_CONTEXT_CLIENT_VERSION = 2
643            0x3038          # EGL_NONE
644        ]
645        config_attribs = (c_int * len(config_attribs))(*config_attribs)
646        context_attribs = (c_int * len(context_attribs))(*context_attribs)
647
648        # perform actual initialization
649        eglMakeCurrent(None, None, None, None)
650        self.egl_display = eglGetDisplay(display)
651        if not self.egl_display:
652            raise RuntimeError("could not get EGL display")
653        if not eglInitialize(self.egl_display, None, None):
654            raise RuntimeError("could not initialize EGL")
655        config = c_void_p()
656        num_configs = c_int(0)
657        if not eglChooseConfig(self.egl_display, config_attribs, byref(config), 1, byref(num_configs)):
658            raise RuntimeError("failed to get a framebuffer configuration")
659        if not num_configs.value:
660            raise RuntimeError("no suitable framebuffer configuration found")
661        self.egl_surface = eglCreateWindowSurface(self.egl_display, config, window, None)
662        if not self.egl_surface:
663            raise RuntimeError("could not create EGL surface")
664        context = eglCreateContext(self.egl_display, config, None, context_attribs)
665        if not context:
666            raise RuntimeError("could not create OpenGL ES rendering context")
667        if not eglMakeCurrent(self.egl_display, self.egl_surface, self.egl_surface, context):
668            raise RuntimeError("could not activate OpenGL ES rendering context")
669
670    def LoadOpenGL(self):
671        def loadsym(name, prototype):
672            return prototype((name, self.gles))
673        return OpenGL(loadsym, desktop=False)
674
675    def SwapBuffers(self):
676        self.eglSwapBuffers(self.egl_display, self.egl_surface)
677
678
679class Platform_BCM2835(Platform_EGL):
680    name = 'bcm2835'
681    allow_custom_fullscreen_res = False
682    has_hardware_cursor = False
683    use_omxplayer = True
684    egllib = "brcmEGL"
685    gles2lib = "brcmGLESv2"
686    DISPLAY_ID = 0
687
688    def __init__(self, libbcm_host):
689        Platform_EGL.__init__(self)
690        self.libbcm_host_path = libbcm_host
691
692    def Init(self):
693        try:
694            self.bcm_host = CDLL(self.libbcm_host_path)
695            def loadfunc(func, ret, *args):
696                return CFUNCTYPE(ret, *args)((func, self.bcm_host))
697            bcm_host_init = loadfunc("bcm_host_init", None)
698            graphics_get_display_size = loadfunc("graphics_get_display_size", c_int32, c_uint16, POINTER(c_uint32), POINTER(c_uint32))
699        except OSError:
700            raise ImportError("failed to load the bcm_host library")
701        except AttributeError:
702            raise ImportError("failed to load required symbols from the bcm_host library")
703        bcm_host_init()
704        x, y = c_uint32(0), c_uint32(0)
705        if graphics_get_display_size(self.DISPLAY_ID, byref(x), byref(y)) < 0:
706            raise RuntimeError("could not determine display size")
707        self.screen_size = (int(x.value), int(y.value))
708
709    def GetScreenSize(self):
710        return self.screen_size
711
712    def StartDisplay(self):
713        global ScreenWidth, ScreenHeight, Fullscreen, FakeFullscreen, WindowPos
714        class VC_DISPMANX_ALPHA_T(Structure):
715            _fields_ = [("flags", c_int), ("opacity", c_uint32), ("mask", c_void_p)]
716        class EGL_DISPMANX_WINDOW_T(Structure):
717            _fields_ = [("element", c_uint32), ("width", c_int), ("height", c_int)]
718
719        # first, import everything
720        try:
721            def loadfunc(func, ret, *args):
722                return CFUNCTYPE(ret, *args)((func, self.bcm_host))
723            vc_dispmanx_display_open = loadfunc("vc_dispmanx_display_open", c_uint32, c_uint32)
724            vc_dispmanx_update_start = loadfunc("vc_dispmanx_update_start", c_uint32, c_int32)
725            vc_dispmanx_element_add = loadfunc("vc_dispmanx_element_add", c_int32,
726                c_uint32, c_uint32, c_int32,  # update, display, layer
727                c_void_p, c_uint32, c_void_p, c_uint32,  # dest_rect, src, drc_rect, protection
728                POINTER(VC_DISPMANX_ALPHA_T),  # alpha
729                c_void_p, c_uint32)  # clamp, transform
730            vc_dispmanx_update_submit_sync = loadfunc("vc_dispmanx_update_submit_sync", c_int, c_uint32)
731        except AttributeError:
732            raise ImportError("failed to load required symbols from the bcm_host library")
733
734        # sanitize arguments
735        width  = min(ScreenWidth,  self.screen_size[0])
736        height = min(ScreenHeight, self.screen_size[1])
737        if WindowPos:
738            x0, y0 = WindowPos
739        else:
740            x0 = (self.screen_size[0] - width)  // 2
741            y0 = (self.screen_size[1] - height) // 2
742        x0 = max(min(x0, self.screen_size[0] - width),  0)
743        y0 = max(min(y0, self.screen_size[1] - height), 0)
744
745        # prepare arguments
746        dst_rect = (c_int32 * 4)(x0, y0, width, height)
747        src_rect = (c_int32 * 4)(0, 0, width << 16, height << 16)
748        alpha = VC_DISPMANX_ALPHA_T(1, 255, None)  # DISPMANX_FLAGS_ALPHA_FIXED_ALL_PIXELS
749
750        # perform initialization
751        display = vc_dispmanx_display_open(self.DISPLAY_ID)
752        update = vc_dispmanx_update_start(0)
753        layer = vc_dispmanx_element_add(update, display, 0, byref(dst_rect), 0, byref(src_rect), 0, byref(alpha), None, 0)
754        vc_dispmanx_update_submit_sync(update)
755        self.window = EGL_DISPMANX_WINDOW_T(layer, width, height)
756        Platform_EGL.StartDisplay(self, None, byref(self.window), width, height)
757
758        # finally, tell PyGame what just happened
759        pygame.display.set_mode((width, height), 0)
760        pygame.mouse.set_pos((width // 2, height // 2))
761
762
763libbcm_host = ctypes.util.find_library("bcm_host")
764if libbcm_host:
765    try:
766        with open("/sys/firmware/devicetree/base/model") as f:
767            model = f.read()
768    except EnvironmentError:
769        model = ""
770    m = re.search(r'pi\s*(\d+)', model, flags=re.I)
771    if m and (int(m.group(1)) >= 4):
772        Platform = Platform_RasPi4()
773    else:
774        Platform = Platform_BCM2835(libbcm_host)
775elif os.name == "nt":
776    Platform = Platform_Win32()
777else:
778    Platform = Platform_Unix()
779
780
781##### TOOL CODE ################################################################
782
783# read and write the PageProps and FileProps meta-dictionaries
784def GetProp(prop_dict, key, prop, default=None):
785    if not key in prop_dict: return default
786    if isinstance(prop, basestring):
787        return prop_dict[key].get(prop, default)
788    for subprop in prop:
789        try:
790            return prop_dict[key][subprop]
791        except KeyError:
792            pass
793    return default
794def SetProp(prop_dict, key, prop, value):
795    if not key in prop_dict:
796        prop_dict[key] = {prop: value}
797    else:
798        prop_dict[key][prop] = value
799def DelProp(prop_dict, key, prop):
800    try:
801        del prop_dict[key][prop]
802    except KeyError:
803        pass
804
805def GetPageProp(page, prop, default=None):
806    global PageProps
807    return GetProp(PageProps, page, prop, default)
808def SetPageProp(page, prop, value):
809    global PageProps
810    SetProp(PageProps, page, prop, value)
811def DelPageProp(page, prop):
812    global PageProps
813    DelProp(PageProps, page, prop)
814def GetTristatePageProp(page, prop, default=0):
815    res = GetPageProp(page, prop, default)
816    if res != FirstTimeOnly: return res
817    return (GetPageProp(page, '_shown', 0) == 1)
818
819def GetFileProp(page, prop, default=None):
820    global FileProps
821    return GetProp(FileProps, page, prop, default)
822def SetFileProp(page, prop, value):
823    global FileProps
824    SetProp(FileProps, page, prop, value)
825
826# the Impressive logo (256x64 pixels grayscale PNG)
827LOGO = b"""iVBORw0KGgoAAAANSUhEUgAAAQAAAABACAAAAADQNvZiAAAL8ElEQVR4Xu2Ze1hVVfrHv+cc7siAEiF4AW1QEkmD8pJUWlkaaSWWk9pk5ZT5szKvPydvoVhqKuWY9jhkmjZpmZmO9wwzLwhiCImAeEFEkJtyk/se17tZ66yz9zlp+IcPD3z++Z79ujxrne963/XupWjytNCCy5QtuXm/vueAxmBAk8dnWyhpWkhFszTA7VR7qMy
828ajz+PEUS/RXO7omnyDP/9eBKNNuCdg1Pn/PYUmiQR4HRutAEeiwyA0yo0RVwGg1PYaAO6OQKAfys0Qbq6gHO60QacVQCgoAxNkPa4PQPsmOQumQIoU9BI5gYCyHy/CRuAqb8Pq4jZi0byakcA36MpG4Avv0SjcaQ1ZNxxA5S0xnWB26YTfccZ3Bl8wMmquEMG/BV3MgPcwTmJZmnAX8D55U4ZcA+T8hwArd3xJ3H0gnU8nGENVzfbGRCLW8Xe2
8292BpQN/+NwgE0ZV9DgMRPGHp11Gj3SGwD5+8KubtMKM+AwrHLNmdU3S1Mml2F+0K+zPaAHAY/fH6mY+D4/X2ocLKK3nb5z4CS3quPphXXJaxZf6TkPH75KeLpSUXdix+wWQtA0pOMAljk3WChAvN30GMf3Xflarcor0LnobAWKncYAmIbexzOgDD6CMKkTOczzX1okLs84FEhmJB3edekImgaAjw6Dn24Te+rsU1CifaHmY8V9YpnKNmC5znVoh
830w2kixBSYR/C8Yx9nDRkjMoEXdC8JuernC+aYVz4AOjtIxHsAkDfDf91UfED7fqg4MOL2oPYjHk7pBYOevKao3knvoj4h0dP1BHtgneYodOO8eaA+O76lxRnB67z74CAjnuDnO4HTZkCw2RVMBR+ivwYzbFCbfpKrpHf+RCzgj4oPIAFqiMMDUSTXgheTHIFh5N2CKlPbdaykEHe2gwTu2j9aAnDLP7R4wE7a3MyT6Jt4NFcOX9EkQ9imIRcGQ6
831bbexhFwmIrFG4J3WfHVRarG/dwTEoFxQXoDOjowOT2W8iN71yUw7hoL47pZRqA2eUcOGE8NEhs+h+RE9Ai/Li8uOAWGxxZvjQFp9puZcvrupPSr3LXwn5tyyNF5UHlnIIjCUsgMmgCipNhWEyhNFBkgp4D7JCZfp9ELy37awrr90dO+OktH6lIQi1lFVJvAGKgwNrPIpgcNMMyl51h8dkOuR3sDppUUWcsL4GuF8Afh+HE9Pe6BgM6NlTEsys8
832Ad4opv3alHN3CwrXBIBJp0L86whQ6cXO5ODPUWTYGwhD05vqCG+FKqDysNLADKrksEAXOHPpyMt8ujgam9KJGoP4M9SSkFaSDGM8XWt3geTw9LGMjAsBwukKLh8oqhagSdftYJQXC+bMTOXLhRihz6aB2Izf8BGAtDdlpBGHYw572qn5Wyuvv+D034HfaEai0/qxOGBDODZgGFbJzn+imV9njGu4FM5T319XsKZXqN1lycJmicomX8VQ+w0FPq
833KxngVwQwxWV0xBEKbJBCOKOnhTlOoAC59uIA5Ge6VztTh99wRl8hgxwqmXhx8B54Bg3YCQ3gGf9NBa4xvcjkj3V0HnThbrO1XvA3a2iFDACBoqdkc9sFA08yjMYKhufKIRKFhNvmqLDauzN0NwEFmQz6ecHiy/ExcHX0MBkkneK+PPRFCbUqLzB6ATOzu6LmXiaLMMJfd7SdIGy41A5QtFAEG3eZbL2LM1Hmz07U1wd9tCsRsDXWdsFURF+Cg1
834Ug9g9qopHFCbl9QDwgcf+59ppDCifR9LN0oDiQZfQQAAVXuZ2CGhRXcxGTjKAU7mBSQ7dcyY4glO/RtMFfq3l3tRIjXAy86dmPg18hQ7RNdpZjXyJmVIXrIng+8/35PSIOnDoFxeRW3//ZYiHi8YAxFszYKRwFC8bmCyvh+A89WjaFuoJw7a1hgXKMSY9D/nbvAoc4IHrSWYDPN9msoa+PoL6zhel2lntrHXB2bsgaEsy4hoE5BEt9M2T4RUPQ
835GtAhhUDtkjfOIAkOhoS3ABlRRST8OPDEyGzvD+T0MTRO2xcBWLBOcJW1AeMqW4AqqPUdgHGxInaWXkG1J+TKiBOe9W5nqy9/WVQAT1XJtnHKcvRGVA1GQLnXrBKa5JVF1WTD42FzNZ4dcz2eUarGVCeAMiHQHcXAF7UyGKyJAP0s3IDsqjWNT9HRDIVCFx9xZAxWQ121J6HxCXpxHLoyOTzcxD0cIBVikmKnikldVq9xhlm6oZmkRpm7vaylgG
836Hai0NMLE0mObKvF8Ahsc9NmalEtCcgZXZ+v0mtB7lg9tXC+2IYvmfixJgxoskpxQakkGcfGGzK8jdkOHStLnhe3zAeOLEiEP6DIiVSvsyG9j7F3iPp3afLc2aXwQNmdyATMmAs4qUIp62DSCEfYJ2lMy5mtECT5LXd8EGu3tvoVXgvoRRUqdICf22n/r1sRNXQOCuMwBHhqltYLoLgMoP5Vlnr4IWI9q2kl8D9BWgNSCAR2wZEEySK48+o6v1P
837Njk9we3gfjLt31h5vKAFSDslr8EQcS9xDEQ8oWw7TgqvpybzGqnvwvq91sfKea55O2mM6A7yTFpdEk+zBSQFME21579YCa1Sqetvc9BUDPh+CpqUoY1WaIK+J9rDWjvO90ZwPWPbjarUdsFb54BmgrQGTCYZLetBEnnLxO2UWa/WA6G1yLIrOmfS+q40sBDvkNeDjLBguM1TIa9QRf5XM2stgxQztpIWIqU52gjGbYNiHiMSfYpqwYIMwPxh3z
838X7zzpsC4gRI9PIA1+GoT/vks/rku5OBQylSeYLHQCULFQZFU+zWrTgMsVGgNslrirjz4D6s9C4LqMJAaEnZ/OgKKiWzAASQ/G0fKGwoJLD28mfR6MvsmPM/HZGqWvARcAWHFF8t2mAdozsDrrFrugeMyugmBmB6r6aBD+drzFaGpgoBFWcIOgYA5JoCZcOUURYee1raAy4xGtAUT5Ys2sYa42DZDS+1w9BO5eVpuA7S7YbxLJp1d1dglSmPQcC
839ws69GDyQ6QDOPuoUdCKl8S4g3P+kAi/FsCDhiirBizP18zq8z4s8HwIxrvcb7UL6iN6A8L3OlAn+xC2DVhNsqANzDjNOn0X09BZieJFuc4o/runx2unhkAgwr0gCDWBQzcqovRjmFlfzWRyAMyYxqcHwWjRBTvfvAuS69cKuIUesgGey39wppkjKmQDKnIgc+wQjd0fBM7zqZEuaQD83BF0eLEziOGUfL8BMHaH748bPEGE9OZh3AuBsx8kDoP
8404tBBm8jYxcdgTBs6jiSvapMMoX4b97G+jCzo8uTxzApV83atpljcJWPJeLW1rwiRvAE4PTYr93h9l2SwEwDQl+7txAfB4j27utYlsEhcAIy/smNzD4DpqO60xTvO91dn6GihZApmZJUz8DyzoAMA+9P9+jL0PSIedyADbV6HSPE1Ea8D86Wjl5cmz8PpLW/WjZeIjIynvlyzJO+nR097cp+8Do01EBMpagYjKE2HXwYNR7gpiI+1x/N/ASarWG
841/BJMWQuTFjHxDhjRnGSXaiaZmWXGwzIL/mj14AMXRcUkQBx9xcUDaHViTdLvQGI8nsdhPdAHtrPZFMvXuqtQCTMZ3IwZowJhCuInPEkX0wSLzaRkEmsdgCuLYUlX/k3jGrdn4diAaOuC9Ze+LNdUKZ2VdBhCDo4WDWgfuxCBTJH+k+lNBjaPwESZ0ZTseSN7bkTEvmjikivjq2Fyr+3Q6YqEcCyq9Awb1w1ZFKHDwWMurvg+VoI3Lxv3gVlitY
842FvZWrsysTOv6/z1EIkoc+dAAqB3qNPCfqen5wGu9hTz9xgoeVmMBYqOzqlUQl+uY/9NeB4mjo+DxoGwTnxwRvVgCDowFArWqlgxFAvWyTE5OaOghM9mQx38ACT/ZUCVQVFOSn7oyrgwVGBz5aT/CQMF/vwtTU06lJ9ZAwdA65PyQoJzllRzpk2oWEhPQoSkn5OR5mTPf39oiPuwYNfV/Bgf/AGp2eHdCubUXqDU7UqNPhdvAoZjIzCk0XIxqLn
843OLN3IAzzduAFgMKrzZXA8R7cTPOgGZugNvdzdoA0QWbtQEtGdBiQEl+MzagqSdAiwEttPA/JcotzChXXBQAAAAASUVORK5CYII="""
844# the default cursor (19x23 pixel RGBA PNG)
845DEFAULT_CURSOR = b"""iVBORw0KGgoAAAANSUhEUgAAABMAAAAXCAYAAADpwXTaAAADCklEQVR42qWUXWwMURTH787MznbWbm1VtdWP0KBN+pFWlQRVQlJBQkR4lGqioY0IibSprAchHgQhoh76hAQPJB4IRdBobdFstbZ4oJLup9au3c5Md3fmjnPHdE2qZVsn+c3snDv3v/9zzt2lEcRbx90rnAk/d7x2xdF/BAWwFmv6jm1bal4db95Xp
846uVmLcbEJfQ9Y0Fu8YZ1yzsvnTu6G3LG2YopPM+HbfMWohTObC0pWXLjWrv9DOS52YjJAi8EKJpBqbZMxNAMlZeXdeTOzdP36/duzYF1w4yciSI/gmUJxLIQw7CIomiUZrOu37m9puukvW51sn0kL2FBEN0Yy2qClGswUIiijYjjUvJXrijuaLt4uCGZPv7qmTAWIGIKMMeajliTGQQNqkOGYbiCxTmXr7e3XC0tXmT5mxhNLtVrq3KWLS3YQxw
847RjCyHBD6IFPUVclUMHGeqWFVVWJuXm/Gku2cwNK0zr9fvJc5UdwqGqVoRZ56rOjMAFMWon1NTLZU11WXdZ0/Vb56qj2ri0eOXwzAAnBDEGKWl56oCk2FZNqOoMP9e24XG5sl9VMv0+0eM9XW7mhijkSXPpF+M0YRkOY7iMVFfbsKE1cJtrN1UXmrmUjr6XUMi0lmVYKKj5Hjo3dnSshENU9WXS75IxgoOhfmxWEwurSwvaIX96mCYCbFoNBrEW
848MqnMK0JSurx6HcNhxwOR8TnHx33eALjXt+o4A8EBUVReNjnBgaALGBoQkwWRRGOB1ZFDJhSBV90OoIHmuxOWZZ98E4Q4HVEgDDgAUiZyoQYjsbiI2SSMpRKynrv+jR2sKmlF4TewLpD20RExrXNMY24dpcTYvBj94F1RHC7vdH9Dcf6eF5wwtpDwKk5wZMnoY/fzqIxH3EWiQhS46ETAz7/t3eQfwqQe2g6gT/OGYkfobBHisfkVvv5vg8fP/d
849D6hnQq/Xqn0KJc0aiorxofq9zkL11+8FXeOwCOgGfVlpSof+vygTWAGagB/iiNTfp0IsRkWxA0hxFZyI0lbBRX/pM4ycZx2V6yAv08AAAAABJRU5ErkJggg=="""
850
851# get the contents of a PIL image as a string
852def img2str(img):
853    if hasattr(img, "tobytes"):
854        return img.tobytes()
855    else:
856        return img.tostring()
857
858# determine the next power of two
859def npot(x):
860    res = 1
861    while res < x: res <<= 1
862    return res
863
864# convert boolean value to string
865def b2s(b):
866    if b: return "Y"
867    return "N"
868
869# extract a number at the beginning of a string
870def num(s):
871    s = s.strip()
872    r = b""
873    while s[0:1] in b"0123456789":
874        r += s[0:1]
875        s = s[1:]
876    try:
877        return int(r)
878    except ValueError:
879        return -1
880
881# linearly interpolate between two floating-point RGB colors represented as tuples
882def lerpColor(a, b, t):
883    return tuple([min(1.0, max(0.0, x + t * (y - x))) for x, y in zip(a, b)])
884
885# get a representative subset of file statistics
886def my_stat(filename):
887    try:
888        s = os.stat(filename)
889    except OSError:
890        return None
891    return (s.st_size, s.st_mtime, s.st_ctime, s.st_mode)
892
893# determine (pagecount,width,height) of a PDF file
894def analyze_pdf(filename):
895    f = open(filename,"rb")
896    pdf = f.read()
897    f.close()
898    box = tuple(map(float, pdf.split(b"/MediaBox",1)[1].split(b"]",1)[0].split(b"[",1)[1].strip().split()))
899    return (max(map(num, pdf.split(b"/Count")[1:])), box[2]-box[0], box[3]-box[1])
900
901# unescape &#123; literals in PDF files
902re_unescape = re.compile(r'&#[0-9]+;')
903def decode_literal(m):
904    try:
905        code = int(m.group(0)[2:-1])
906        if code:
907            return chr(code)
908        else:
909            return ""
910    except ValueError:
911        return '?'
912def unescape_pdf(s):
913    return re_unescape.sub(decode_literal, s)
914
915# parse pdftk output
916def pdftkParse(filename, page_offset=0):
917    f = open(filename, "rb")
918    InfoKey = None
919    BookmarkTitle = None
920    Title = None
921    Pages = 0
922    for line in f:
923        try:
924            line = line.decode('utf-8')
925        except UnicodeDecodeError:  # pdftk's output may not be UTF-8-clean
926            line = line.decode('windows-1252', 'replace')
927        try:
928            key, value = [item.strip() for item in line.split(':', 1)]
929        except ValueError:
930            continue
931        key = key.lower()
932        if key == "numberofpages":
933            Pages = int(value)
934        elif key == "infokey":
935            InfoKey = value.lower()
936        elif (key == "infovalue") and (InfoKey == "title"):
937            Title = unescape_pdf(value)
938            InfoKey = None
939        elif key == "bookmarktitle":
940            BookmarkTitle = unescape_pdf(value)
941        elif key == "bookmarkpagenumber" and BookmarkTitle:
942            try:
943                page = int(value)
944                if not GetPageProp(page + page_offset, '_title'):
945                    SetPageProp(page + page_offset, '_title', BookmarkTitle)
946            except ValueError:
947                pass
948            BookmarkTitle = None
949    f.close()
950    if AutoOverview:
951        SetPageProp(page_offset + 1, '_overview', True)
952        for page in range(page_offset + 2, page_offset + Pages):
953            SetPageProp(page, '_overview', \
954                        not(not(GetPageProp(page + AutoOverview - 1, '_title'))))
955        SetPageProp(page_offset + Pages, '_overview', True)
956    return (Title, Pages)
957
958# parse mutool output
959def mutoolParse(f, page_offset=0):
960    title = None
961    pages = 0
962    for line in f:
963        line = line.decode()
964        m = re.match("pages:\s*(\d+)", line, re.I)
965        if m and not(pages):
966            pages = int(m.group(1))
967        m = re.search("/title\s*\(", line, re.I)
968        if m and not(title):
969            title = line[m.end():].replace(')', '\0').replace('\\(', '(').replace('\\\0', ')').split('\0', 1)[0].strip()
970    return (title, pages)
971
972# translate pixel coordinates to normalized screen coordinates
973def MouseToScreen(mousepos):
974    return (ZoomX0 + mousepos[0] * ZoomArea / ScreenWidth,
975            ZoomY0 + mousepos[1] * ZoomArea / ScreenHeight)
976
977# normalize rectangle coordinates so that the upper-left point comes first
978def NormalizeRect(X0, Y0, X1, Y1):
979    return (min(X0, X1), min(Y0, Y1), max(X0, X1), max(Y0, Y1))
980
981# check if a point is inside a box (or a list of boxes)
982def InsideBox(x, y, box):
983    return (x >= box[0]) and (y >= box[1]) and (x < box[2]) and (y < box[3])
984def FindBox(x, y, boxes):
985    for i in range(len(boxes)):
986        if InsideBox(x, y, boxes[i]):
987            return i
988    raise ValueError
989
990# zoom an image size to a destination size, preserving the aspect ratio
991def ZoomToFit(size, dest=None, force_int=False):
992    if not dest:
993        dest = (ScreenWidth + Overscan, ScreenHeight + Overscan)
994    newx = dest[0]
995    newy = size[1] * newx / size[0]
996    if newy > dest[1]:
997        newy = dest[1]
998        newx = size[0] * newy / size[1]
999    if force_int:
1000        return (int(newx), int(newy))
1001    return (newx, newy)
1002
1003# get the overlay grid screen coordinates for a specific page
1004def OverviewPos(page):
1005    return ((page %  OverviewGridSize) * OverviewCellX + OverviewOfsX,
1006            (page // OverviewGridSize) * OverviewCellY + OverviewOfsY)
1007
1008def StopMPlayer():
1009    global MPlayerProcess, VideoPlaying, NextPageAfterVideo
1010    if not MPlayerProcess: return
1011
1012    # first, ask politely
1013    try:
1014        if Platform.use_omxplayer and VideoPlaying:
1015            MPlayerProcess.stdin.write('q')
1016        else:
1017            MPlayerProcess.stdin.write('quit\n')
1018        MPlayerProcess.stdin.flush()
1019        for i in range(10):
1020            if MPlayerProcess.poll() is None:
1021                time.sleep(0.1)
1022            else:
1023                break
1024    except:
1025        pass
1026
1027    # if that didn't work, be rude
1028    if MPlayerProcess.poll() is None:
1029        print("Audio/video player didn't exit properly, killing PID", MPlayerProcess.pid, file=sys.stderr)
1030        try:
1031            if os.name == 'nt':
1032                win32api.TerminateProcess(win32api.OpenProcess(1, False, MPlayerProcess.pid), 0)
1033            else:
1034                os.kill(MPlayerProcess.pid, 2)
1035            MPlayerProcess = None
1036        except:
1037            pass
1038    else:
1039        MPlayerProcess = None
1040
1041    VideoPlaying = False
1042    if os.name == 'nt':
1043        win32gui.ShowWindow(Platform.GetWindowID(), 9)  # SW_RESTORE
1044    if NextPageAfterVideo:
1045        NextPageAfterVideo = False
1046        TransitionTo(GetNextPage(Pcurrent, 1))
1047
1048def ClockTime(minutes):
1049    if minutes:
1050        return time.strftime("%H:%M")
1051    else:
1052        return time.strftime("%H:%M:%S")
1053
1054def FormatTime(t, minutes=False):
1055    t = int(t)
1056    if minutes and (t < 3600):
1057        return "%d min" % (t // 60)
1058    elif minutes:
1059        return "%d:%02d" % (t // 3600, (t // 60) % 60)
1060    elif t < 3600:
1061        return "%d:%02d" % (t // 60, t % 60)
1062    else:
1063        ms = t % 3600
1064        return "%d:%02d:%02d" % (t // 3600, ms // 60, ms % 60)
1065
1066def SafeCall(func, args=[], kwargs={}):
1067    if not func: return None
1068    try:
1069        return func(*args, **kwargs)
1070    except:
1071        print("----- Unhandled Exception ----", file=sys.stderr)
1072        traceback.print_exc(file=sys.stderr)
1073        print("----- End of traceback -----", file=sys.stderr)
1074
1075def Quit(code=0):
1076    global CleanExit
1077    if not code:
1078        CleanExit = True
1079    StopMPlayer()
1080    Platform.Done()
1081    print("Total presentation time: %s." % \
1082                        FormatTime((Platform.GetTicks() - StartTime) / 1000), file=sys.stderr)
1083    sys.exit(code)
1084
1085
1086##### OPENGL (ES) 2.0 LOADER AND TOOLKIT #######################################
1087
1088if os.name == 'nt':
1089    GLFUNCTYPE = WINFUNCTYPE
1090else:
1091    GLFUNCTYPE = CFUNCTYPE
1092
1093class GLFunction(object):
1094    def __init__(self, required, name, ret, *args):
1095        self.name = name
1096        self.required = required
1097        self.prototype = GLFUNCTYPE(ret, *args)
1098
1099class OpenGL(object):
1100    FALSE = 0
1101    TRUE = 1
1102    NO_ERROR = 0
1103    INVALID_ENUM = 0x0500
1104    INVALID_VALUE = 0x0501
1105    INVALID_OPERATION = 0x0502
1106    OUT_OF_MEMORY = 0x0505
1107    INVALID_FRAMEBUFFER_OPERATION = 0x0506
1108    VENDOR = 0x1F00
1109    RENDERER = 0x1F01
1110    VERSION = 0x1F02
1111    EXTENSIONS = 0x1F03
1112    POINTS = 0x0000
1113    LINES = 0x0001
1114    LINE_LOOP = 0x0002
1115    LINE_STRIP = 0x0003
1116    TRIANGLES = 0x0004
1117    TRIANGLE_STRIP = 0x0005
1118    TRIANGLE_FAN = 0x0006
1119    BYTE = 0x1400
1120    UNSIGNED_BYTE = 0x1401
1121    SHORT = 0x1402
1122    UNSIGNED_SHORT = 0x1403
1123    INT = 0x1404
1124    UNSIGNED_INT = 0x1405
1125    FLOAT = 0x1406
1126    DEPTH_TEST = 0x0B71
1127    BLEND = 0x0BE2
1128    ZERO = 0
1129    ONE = 1
1130    SRC_COLOR = 0x0300
1131    ONE_MINUS_SRC_COLOR = 0x0301
1132    SRC_ALPHA = 0x0302
1133    ONE_MINUS_SRC_ALPHA = 0x0303
1134    DST_ALPHA = 0x0304
1135    ONE_MINUS_DST_ALPHA = 0x0305
1136    DST_COLOR = 0x0306
1137    ONE_MINUS_DST_COLOR = 0x0307
1138    DEPTH_BUFFER_BIT = 0x00000100
1139    COLOR_BUFFER_BIT = 0x00004000
1140    TEXTURE0 = 0x84C0
1141    TEXTURE_2D = 0x0DE1
1142    TEXTURE_RECTANGLE = 0x84F5
1143    TEXTURE_MAG_FILTER = 0x2800
1144    TEXTURE_MIN_FILTER = 0x2801
1145    TEXTURE_WRAP_S = 0x2802
1146    TEXTURE_WRAP_T = 0x2803
1147    NEAREST = 0x2600
1148    LINEAR = 0x2601
1149    NEAREST_MIPMAP_NEAREST = 0x2700
1150    LINEAR_MIPMAP_NEAREST = 0x2701
1151    NEAREST_MIPMAP_LINEAR = 0x2702
1152    LINEAR_MIPMAP_LINEAR = 0x2703
1153    CLAMP_TO_EDGE = 0x812F
1154    REPEAT = 0x2901
1155    ALPHA = 0x1906
1156    RGB = 0x1907
1157    RGBA = 0x1908
1158    LUMINANCE = 0x1909
1159    LUMINANCE_ALPHA = 0x190A
1160    ARRAY_BUFFER = 0x8892
1161    ELEMENT_ARRAY_BUFFER = 0x8893
1162    STREAM_DRAW = 0x88E0
1163    STATIC_DRAW = 0x88E4
1164    DYNAMIC_DRAW = 0x88E8
1165    FRAGMENT_SHADER = 0x8B30
1166    VERTEX_SHADER = 0x8B31
1167    COMPILE_STATUS = 0x8B81
1168    LINK_STATUS = 0x8B82
1169    INFO_LOG_LENGTH = 0x8B84
1170    UNPACK_ALIGNMENT = 0x0CF5
1171    MAX_TEXTURE_SIZE = 0x0D33
1172    _funcs = [
1173        GLFunction(True,  "GetString",                c_char_p, c_uint),
1174        GLFunction(True,  "Enable",                   None, c_uint),
1175        GLFunction(True,  "Disable",                  None, c_uint),
1176        GLFunction(True,  "GetError",                 c_uint),
1177        GLFunction(True,  "Viewport",                 None, c_int, c_int, c_int, c_int),
1178        GLFunction(True,  "Clear",                    None, c_uint),
1179        GLFunction(True,  "ClearColor",               None, c_float, c_float, c_float, c_float),
1180        GLFunction(True,  "BlendFunc",                None, c_uint, c_uint),
1181        GLFunction(True,  "GenTextures",              None, c_uint, POINTER(c_int)),
1182        GLFunction(True,  "BindTexture",              None, c_uint, c_int),
1183        GLFunction(True,  "ActiveTexture",            None, c_uint),
1184        GLFunction(True,  "TexParameteri",            None, c_uint, c_uint, c_int),
1185        GLFunction(True,  "TexImage2D",               None, c_uint, c_uint, c_uint, c_uint, c_uint, c_uint, c_uint, c_uint, c_void_p),
1186        GLFunction(True,  "GenerateMipmap",           None, c_uint),
1187        GLFunction(True,  "GenBuffers",               None, c_uint, POINTER(c_int)),
1188        GLFunction(True,  "BindBuffer",               None, c_uint, c_int),
1189        GLFunction(True,  "BufferData",               None, c_uint, c_void_p, c_void_p, c_uint),
1190        GLFunction(True,  "CreateProgram",            c_uint),
1191        GLFunction(True,  "CreateShader",             c_uint, c_uint),
1192        GLFunction(True,  "ShaderSource",             None, c_uint, c_uint, c_void_p, c_void_p),
1193        GLFunction(True,  "CompileShader",            None, c_uint),
1194        GLFunction(True,  "GetShaderiv",              None, c_uint, c_uint, POINTER(c_uint)),
1195        GLFunction(True,  "GetShaderInfoLog",         None, c_uint, c_uint, c_void_p, c_void_p),
1196        GLFunction(True,  "AttachShader",             None, c_uint, c_uint),
1197        GLFunction(True,  "LinkProgram",              None, c_uint),
1198        GLFunction(True,  "GetProgramiv",             None, c_uint, c_uint, POINTER(c_uint)),
1199        GLFunction(True,  "GetProgramInfoLog",        None, c_uint, c_uint, c_void_p, c_void_p),
1200        GLFunction(True,  "UseProgram",               None, c_uint),
1201        GLFunction(True,  "BindAttribLocation",       None, c_uint, c_uint, c_char_p),
1202        GLFunction(True,  "GetAttribLocation",        c_int, c_uint, c_char_p),
1203        GLFunction(True,  "GetUniformLocation",       c_int, c_uint, c_char_p),
1204        GLFunction(True,  "Uniform1f",                None, c_uint, c_float),
1205        GLFunction(True,  "Uniform2f",                None, c_uint, c_float, c_float),
1206        GLFunction(True,  "Uniform3f",                None, c_uint, c_float, c_float, c_float),
1207        GLFunction(True,  "Uniform4f",                None, c_uint, c_float, c_float, c_float, c_float),
1208        GLFunction(True,  "Uniform1i",                None, c_uint, c_int),
1209        GLFunction(True,  "Uniform2i",                None, c_uint, c_int, c_int),
1210        GLFunction(True,  "Uniform3i",                None, c_uint, c_int, c_int, c_int),
1211        GLFunction(True,  "Uniform4i",                None, c_uint, c_int, c_int, c_int, c_int),
1212        GLFunction(True,  "EnableVertexAttribArray",  None, c_uint),
1213        GLFunction(True,  "DisableVertexAttribArray", None, c_uint),
1214        GLFunction(True,  "VertexAttribPointer",      None, c_uint, c_uint, c_uint, c_uint, c_uint, c_void_p),
1215        GLFunction(True,  "DrawArrays",               None, c_uint, c_uint, c_uint),
1216        GLFunction(True,  "DrawElements",             None, c_uint, c_uint, c_uint, c_void_p),
1217        GLFunction(True,  "PixelStorei",              None, c_uint, c_uint),
1218        GLFunction(True,  "GetIntegerv",              None, c_uint, POINTER(c_int)),
1219    ]
1220    _typemap = {
1221                  BYTE:  c_int8,
1222         UNSIGNED_BYTE: c_uint8,
1223                 SHORT:  c_int16,
1224        UNSIGNED_SHORT: c_uint16,
1225                   INT:  c_int32,
1226          UNSIGNED_INT: c_uint32,
1227                 FLOAT:  c_float
1228    }
1229
1230    def __init__(self, loader, desktop=False):
1231        global GLVendor, GLRenderer, GLVersion
1232        self._is_desktop_gl = desktop
1233        for func in self._funcs:
1234            funcptr = None
1235            for suffix in ("", "ARB", "ObjectARB", "EXT", "OES"):
1236                funcptr = loader("gl" + func.name + suffix, func.prototype)
1237                if funcptr:
1238                    break
1239            if not funcptr:
1240                if func.required:
1241                    raise ImportError("failed to import required OpenGL function 'gl%s'" % func.name)
1242                else:
1243                    def errfunc(*args):
1244                        raise ImportError("call to unimplemented OpenGL function 'gl%s'" % func.name)
1245                    funcptr = errfunc
1246            if hasattr(self, func.name):
1247                setattr(self, '_' + func.name, funcptr)
1248            else:
1249                setattr(self, func.name, funcptr)
1250            if func.name == "GetString":
1251                GLVendor = self.GetString(self.VENDOR).decode() or ""
1252                GLRenderer = self.GetString(self.RENDERER).decode() or ""
1253                GLVersion = self.GetString(self.VERSION).decode() or ""
1254        self._init()
1255
1256    def GenTextures(self, n=1):
1257        bufs = (c_int * n)()
1258        self._GenTextures(n, bufs)
1259        if n == 1: return bufs[0]
1260        return list(bufs)
1261
1262    def ActiveTexture(self, tmu):
1263        if tmu < self.TEXTURE0:
1264            tmu += self.TEXTURE0
1265        self._ActiveTexture(tmu)
1266
1267    def GenBuffers(self, n=1):
1268        bufs = (c_int * n)()
1269        self._GenBuffers(n, bufs)
1270        if n == 1: return bufs[0]
1271        return list(bufs)
1272
1273    def BufferData(self, target, size=0, data=None, usage=STATIC_DRAW, type=None):
1274        if isinstance(data, list):
1275            if type:
1276                type = self._typemap[type]
1277            elif isinstance(data[0], int):
1278                type = c_int32
1279            elif isinstance(data[0], float):
1280                type = c_float
1281            else:
1282                raise TypeError("cannot infer buffer data type")
1283            size = len(data) * sizeof(type)
1284            data = (type * len(data))(*data)
1285        self._BufferData(target, cast(size, c_void_p), cast(data, c_void_p), usage)
1286
1287    def ShaderSource(self, shader, source):
1288        source = c_char_p(source.encode())
1289        self._ShaderSource(shader, 1, pointer(source), None)
1290
1291    def GetShaderi(self, shader, pname):
1292        res = (c_uint * 1)()
1293        self.GetShaderiv(shader, pname, res)
1294        return res[0]
1295
1296    def GetShaderInfoLog(self, shader):
1297        length = self.GetShaderi(shader, self.INFO_LOG_LENGTH)
1298        if not length: return ""
1299        buf = create_string_buffer(length + 1)
1300        self._GetShaderInfoLog(shader, length + 1, None, buf)
1301        return buf.raw.split(b'\0', 1)[0].decode()
1302
1303    def GetProgrami(self, program, pname):
1304        res = (c_uint * 1)()
1305        self.GetProgramiv(program, pname, res)
1306        return res[0]
1307
1308    def GetProgramInfoLog(self, program):
1309        length = self.GetProgrami(program, self.INFO_LOG_LENGTH)
1310        if not length: return ""
1311        buf = create_string_buffer(length + 1)
1312        self._GetProgramInfoLog(program, length + 1, None, buf)
1313        return buf.raw.split(b'\0', 1)[0].decode()
1314
1315    def Uniform(self, location, *values):
1316        if not values:
1317            raise TypeError("no values for glUniform")
1318        if (len(values) == 1) and (isinstance(values[0], list) or isinstance(values[0], tuple)):
1319            values = values[0]
1320        l = len(values)
1321        if l > 4:
1322            raise TypeError("uniform vector has too-high order(%d)" % len(values))
1323        if any(isinstance(v, float) for v in values):
1324            if   l == 1: self.Uniform1f(location, values[0])
1325            elif l == 2: self.Uniform2f(location, values[0], values[1])
1326            elif l == 3: self.Uniform3f(location, values[0], values[1], values[2])
1327            else:        self.Uniform4f(location, values[0], values[1], values[2], values[3])
1328        else:
1329            if   l == 1: self.Uniform1i(location, values[0])
1330            elif l == 2: self.Uniform2i(location, values[0], values[1])
1331            elif l == 3: self.Uniform3i(location, values[0], values[1], values[2])
1332            else:        self.Uniform4i(location, values[0], values[1], values[2], values[3])
1333
1334    ##### Convenience Functions #####
1335
1336    def _init(self):
1337        self.enabled_attribs = set()
1338
1339    def set_enabled_attribs(self, *attrs):
1340        want = set(attrs)
1341        for a in (want - self.enabled_attribs):
1342            self.EnableVertexAttribArray(a)
1343        for a in (self.enabled_attribs - want):
1344            self.DisableVertexAttribArray(a)
1345        self.enabled_attribs = want
1346
1347    def set_texture(self, target=TEXTURE_2D, tex=0, tmu=0):
1348        self.ActiveTexture(self.TEXTURE0 + tmu)
1349        self.BindTexture(target, tex)
1350
1351    def make_texture(self, target=TEXTURE_2D, wrap=CLAMP_TO_EDGE, filter=LINEAR_MIPMAP_NEAREST, img=None):
1352        tex = self.GenTextures()
1353        min_filter = filter
1354        if min_filter < self.NEAREST_MIPMAP_NEAREST:
1355            mag_filter = min_filter
1356        else:
1357            mag_filter = self.NEAREST + (min_filter & 1)
1358        self.BindTexture(target, tex)
1359        self.TexParameteri(target, self.TEXTURE_WRAP_S, wrap)
1360        self.TexParameteri(target, self.TEXTURE_WRAP_T, wrap)
1361        self.TexParameteri(target, self.TEXTURE_MIN_FILTER, min_filter)
1362        self.TexParameteri(target, self.TEXTURE_MAG_FILTER, mag_filter)
1363        if img:
1364            self.load_texture(target, img)
1365        return tex
1366
1367    def load_texture(self, target, tex_or_img, img=None):
1368        if img:
1369            gl.BindTexture(target, tex_or_img)
1370        else:
1371            img = tex_or_img
1372        if   img.mode == 'RGBA': format = self.RGBA
1373        elif img.mode == 'RGB':  format = self.RGB
1374        elif img.mode == 'LA':   format = self.LUMINANCE_ALPHA
1375        elif img.mode == 'L':    format = self.LUMINANCE
1376        else: raise TypeError("image has unsupported color format '%s'" % img.mode)
1377        gl.TexImage2D(target, 0, format, img.size[0], img.size[1], 0, format, self.UNSIGNED_BYTE, img2str(img))
1378
1379class GLShaderCompileError(SyntaxError):
1380    pass
1381class GLInvalidShaderError(GLShaderCompileError):
1382    pass
1383
1384class GLShader(object):
1385    LOG_NEVER = 0
1386    LOG_ON_ERROR = 1
1387    LOG_IF_NOT_EMPTY = 2
1388    LOG_ALWAYS = 3
1389    LOG_DEFAULT = LOG_ON_ERROR
1390
1391    def __init__(self, vs=None, fs=None, attributes=[], uniforms=[], loglevel=None):
1392        if not(vs): vs = self.vs
1393        if not(fs): fs = self.fs
1394        if not(attributes) and hasattr(self, 'attributes'):
1395            attributes = self.attributes
1396        if isinstance(attributes, dict):
1397            attributes = attributes.items()
1398        if not(uniforms) and hasattr(self, 'uniforms'):
1399            uniforms = self.uniforms
1400        if isinstance(uniforms, dict):
1401            uniforms = uniforms.items()
1402        uniforms = [((u, None) if isinstance(u, basestring) else u) for u in uniforms]
1403        if (loglevel is None) and hasattr(self, 'loglevel'):
1404            loglevel = self.loglevel
1405        if loglevel is None:
1406            loglevel = self.LOG_DEFAULT
1407
1408        self.program = gl.CreateProgram()
1409        def handle_shader_log(status, log_getter, action):
1410            force_log = (loglevel >= self.LOG_ALWAYS) or ((loglevel >= self.LOG_ON_ERROR) and not(status))
1411            if force_log or (loglevel >= self.LOG_IF_NOT_EMPTY):
1412                log = log_getter().rstrip()
1413            else:
1414                log = ""
1415            if force_log or ((loglevel >= self.LOG_IF_NOT_EMPTY) and log):
1416                if status:
1417                    print("Info: log for %s %s:" % (self.__class__.__name__, action), file=sys.stderr)
1418                else:
1419                    print("Error: %s %s failed - log information follows:" % (self.__class__.__name__, action), file=sys.stderr)
1420                for line in log.split('\n'):
1421                    print('>', line.rstrip(), file=sys.stderr)
1422            if not status:
1423                if log:
1424                    log = ":\n" + log
1425                raise GLShaderCompileError("failure during %s %s" % (self.__class__.__name__, action) + log)
1426        def handle_shader(type_enum, type_name, src):
1427            if gl._is_desktop_gl:
1428                src = src.replace("highp ", "")
1429                src = src.replace("mediump ", "")
1430                src = src.replace("lowp ", "")
1431            shader = gl.CreateShader(type_enum)
1432            gl.ShaderSource(shader, src)
1433            gl.CompileShader(shader)
1434            handle_shader_log(gl.GetShaderi(shader, gl.COMPILE_STATUS),
1435                              lambda: gl.GetShaderInfoLog(shader),
1436                              type_name + " shader compilation")
1437            gl.AttachShader(self.program, shader)
1438        handle_shader(gl.VERTEX_SHADER, "vertex", vs)
1439        handle_shader(gl.FRAGMENT_SHADER, "fragment", fs)
1440        for attr in attributes:
1441            if not isinstance(attr, basestring):
1442                loc, name = attr
1443                if isinstance(loc, basestring):
1444                    loc, name = name, loc
1445                setattr(self, name, loc)
1446            elif hasattr(self, attr):
1447                name = attr
1448                loc = getattr(self, name)
1449            gl.BindAttribLocation(self.program, loc, name.encode())
1450        gl.LinkProgram(self.program)
1451        handle_shader_log(gl.GetProgrami(self.program, gl.LINK_STATUS),
1452                          lambda: gl.GetProgramInfoLog(self.program),
1453                          "linking")
1454        gl.UseProgram(self.program)
1455        for name in attributes:
1456            if isinstance(name, basestring) and not(hasattr(self, attr)):
1457                setattr(self, name, int(gl.GetAttribLocation(self.program, name)))
1458        for u in uniforms:
1459            loc = int(gl.GetUniformLocation(self.program, u[0].encode()))
1460            setattr(self, u[0], loc)
1461            if u[1] is not None:
1462                gl.Uniform(loc, *u[1:])
1463
1464    def use(self):
1465        gl.UseProgram(self.program)
1466        return self
1467
1468    @classmethod
1469    def get_instance(self):
1470        try:
1471            instance = self._instance
1472            if instance:
1473                return instance
1474            else:
1475                raise GLInvalidShaderError("shader failed to compile in the past")
1476        except AttributeError:
1477            try:
1478                self._instance = self()
1479            except GLShaderCompileError as e:
1480                self._instance = None
1481                raise
1482            return self._instance
1483
1484# NOTE: OpenGL drawing code in Impressive uses the following conventions:
1485# - program binding is undefined
1486# - vertex attribute layout is undefined
1487# - vertex attribute enable/disable is managed by gl.set_enabled_attribs()
1488# - texture bindings are undefined
1489# - ActiveTexure is TEXTURE0
1490# - array and element array buffer bindings are undefined
1491# - BLEND is disabled, BlendFunc is (SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
1492
1493
1494##### STOCK SHADERS ############################################################
1495
1496class SimpleQuad(object):
1497    "vertex buffer singleton for a simple quad (used by various shaders)"
1498    vbuf = None
1499    @classmethod
1500    def draw(self):
1501        gl.set_enabled_attribs(0)
1502        if not self.vbuf:
1503            self.vbuf = gl.GenBuffers()
1504            gl.BindBuffer(gl.ARRAY_BUFFER, self.vbuf)
1505            gl.BufferData(gl.ARRAY_BUFFER, data=[0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0])
1506        else:
1507            gl.BindBuffer(gl.ARRAY_BUFFER, self.vbuf)
1508        gl.VertexAttribPointer(0, 2, gl.FLOAT, False, 0, 0)
1509        gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
1510
1511
1512class TexturedRectShader(GLShader):
1513    vs = """
1514        attribute highp vec2 aPos;
1515        uniform highp vec4 uPosTransform;
1516        uniform highp vec4 uScreenTransform;
1517        uniform highp vec4 uTexTransform;
1518        varying mediump vec2 vTexCoord;
1519        void main() {
1520            highp vec2 pos = uPosTransform.xy + aPos * uPosTransform.zw;
1521            gl_Position = vec4(uScreenTransform.xy + pos * uScreenTransform.zw, 0.0, 1.0);
1522            vTexCoord = uTexTransform.xy + aPos * uTexTransform.zw;
1523        }
1524    """
1525    fs = """
1526        uniform lowp vec4 uColor;
1527        uniform lowp sampler2D uTex;
1528        varying mediump vec2 vTexCoord;
1529        void main() {
1530            gl_FragColor = uColor * texture2D(uTex, vTexCoord);
1531        }
1532    """
1533    attributes = { 0: 'aPos' }
1534    uniforms = ['uPosTransform', 'uScreenTransform', 'uTexTransform', 'uColor']
1535
1536    def draw(self, x0, y0, x1, y1, s0=0.0, t0=0.0, s1=1.0, t1=1.0, tex=None, color=1.0):
1537        self.use()
1538        if tex:
1539            gl.BindTexture(gl.TEXTURE_2D, tex)
1540        if isinstance(color, float):
1541            gl.Uniform4f(self.uColor, color, color, color, 1.0)
1542        else:
1543            gl.Uniform(self.uColor, color)
1544        gl.Uniform(self.uPosTransform, x0, y0, x1 - x0, y1 - y0)
1545        gl.Uniform(self.uScreenTransform, ScreenTransform)
1546        gl.Uniform(self.uTexTransform, s0, t0, s1 - s0, t1 - t0)
1547        SimpleQuad.draw()
1548RequiredShaders.append(TexturedRectShader)
1549
1550
1551class TexturedMeshShader(GLShader):
1552    vs = """
1553        attribute highp vec3 aPosAndAlpha;
1554        uniform highp vec4 uPosTransform;
1555        uniform highp vec4 uScreenTransform;
1556        uniform highp vec4 uTexTransform;
1557        varying mediump vec2 vTexCoord;
1558        varying lowp float vAlpha;
1559        void main() {
1560            highp vec2 pos = uPosTransform.xy + aPosAndAlpha.xy * uPosTransform.zw;
1561            gl_Position = vec4(uScreenTransform.xy + pos * uScreenTransform.zw, 0.0, 1.0);
1562            vTexCoord = uTexTransform.xy + aPosAndAlpha.xy * uTexTransform.zw;
1563            vAlpha = aPosAndAlpha.z;
1564        }
1565    """
1566    fs = """
1567        uniform lowp sampler2D uTex;
1568        varying mediump vec2 vTexCoord;
1569        varying lowp float vAlpha;
1570        void main() {
1571            gl_FragColor = vec4(1.0, 1.0, 1.0, vAlpha) * texture2D(uTex, vTexCoord);
1572        }
1573    """
1574    attributes = { 0: 'aPosAndAlpha' }
1575    uniforms = ['uPosTransform', 'uScreenTransform', 'uTexTransform']
1576
1577    def setup(self, x0, y0, x1, y1, s0=0.0, t0=0.0, s1=1.0, t1=1.0, tex=None):
1578        self.use()
1579        if tex:
1580            gl.BindTexture(gl.TEXTURE_2D, tex)
1581        gl.Uniform(self.uPosTransform, x0, y0, x1 - x0, y1 - y0)
1582        gl.Uniform(self.uScreenTransform, ScreenTransform)
1583        gl.Uniform(self.uTexTransform, s0, t0, s1 - s0, t1 - t0)
1584RequiredShaders.append(TexturedMeshShader)
1585
1586
1587class BlurShader(GLShader):
1588    vs = """
1589        attribute highp vec2 aPos;
1590        uniform highp vec4 uScreenTransform;
1591        varying mediump vec2 vTexCoord;
1592        void main() {
1593            gl_Position = vec4(uScreenTransform.xy + aPos * uScreenTransform.zw, 0.0, 1.0);
1594            vTexCoord = aPos;
1595        }
1596    """
1597    fs = """
1598        uniform lowp float uIntensity;
1599        uniform mediump sampler2D uTex;
1600        uniform mediump vec2 uDeltaTexCoord;
1601        varying mediump vec2 vTexCoord;
1602        void main() {
1603            lowp vec3 color = (uIntensity * 0.125) * (
1604                texture2D(uTex, vTexCoord).rgb * 3.0
1605              + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(+0.89, +0.45)).rgb
1606              + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(+0.71, -0.71)).rgb
1607              + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(-0.45, -0.89)).rgb
1608              + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(-0.99, +0.16)).rgb
1609              + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(-0.16, +0.99)).rgb
1610            );
1611            lowp float gray = dot(vec3(0.299, 0.587, 0.114), color);
1612            gl_FragColor = vec4(mix(color, vec3(gray, gray, gray), uIntensity), 1.0);
1613        }
1614    """
1615    attributes = { 0: 'aPos' }
1616    uniforms = ['uScreenTransform', 'uDeltaTexCoord', 'uIntensity']
1617
1618    def draw(self, dtx, dty, intensity=1.0, tex=None):
1619        self.use()
1620        if tex:
1621            gl.BindTexture(gl.TEXTURE_2D, tex)
1622        gl.Uniform(self.uScreenTransform, ScreenTransform)
1623        gl.Uniform2f(self.uDeltaTexCoord, dtx, dty)
1624        gl.Uniform1f(self.uIntensity, intensity)
1625        SimpleQuad.draw()
1626# (not added to RequiredShaders because this shader is allowed to fail)
1627
1628
1629class ProgressBarShader(GLShader):
1630    vs = """
1631        attribute highp vec2 aPos;
1632        uniform highp vec4 uPosTransform;
1633        varying mediump float vGrad;
1634        void main() {
1635            gl_Position = vec4(uPosTransform.xy + aPos * uPosTransform.zw, 0.0, 1.0);
1636            vGrad = 1.0 - 2.0 * aPos.y;
1637        }
1638    """
1639    fs = """
1640        uniform lowp vec4 uColor0;
1641        uniform lowp vec4 uColor1;
1642        varying mediump float vGrad;
1643        void main() {
1644            gl_FragColor = mix(uColor0, uColor1, 1.0 - abs(vGrad));
1645        }
1646    """
1647    attributes = { 0: 'aPos' }
1648    uniforms = ['uPosTransform', 'uColor0', 'uColor1']
1649
1650    def draw(self, x0, y0, x1, y1, color0, color1):
1651        self.use()
1652        tx0 = ScreenTransform[0] + ScreenTransform[2] * x0
1653        ty0 = ScreenTransform[1] + ScreenTransform[3] * y0
1654        tx1 = ScreenTransform[0] + ScreenTransform[2] * x1
1655        ty1 = ScreenTransform[1] + ScreenTransform[3] * y1
1656        gl.Uniform4f(self.uPosTransform, tx0, ty0, tx1 - tx0, ty1 - ty0)
1657        gl.Uniform(self.uColor0, color0)
1658        gl.Uniform(self.uColor1, color1)
1659        SimpleQuad.draw()
1660RequiredShaders.append(ProgressBarShader)
1661
1662
1663##### RENDERING TOOL CODE ######################################################
1664
1665# meshes for highlight boxes and the spotlight are laid out in the same manner:
1666# - vertex 0 is the center vertex
1667# - for each slice, there are two further vertices:
1668#   - vertex 2*i+1 is the "inner" vertex with full alpha
1669#   - vertex 2*i+2 is the "outer" vertex with zero alpha
1670
1671class HighlightIndexBuffer(object):
1672    def __init__(self, npoints, reuse_buf=None, dynamic=False):
1673        if not reuse_buf:
1674            self.buf = gl.GenBuffers()
1675        elif isinstance(reuse_buf, HighlightIndexBuffer):
1676            self.buf = reuse_buf.buf
1677        else:
1678            self.buf = reuse_buf
1679        data = []
1680        for i in range(npoints):
1681            if i:
1682                b0 = 2 * i - 1
1683            else:
1684                b0 = 2 * npoints - 1
1685            b1 = 2 * i + 1
1686            data.extend([
1687                0, b1, b0,
1688                b1, b1+1, b0,
1689                b1+1, b0+1, b0
1690            ])
1691        self.vertices = 9 * npoints
1692        if dynamic:
1693            usage = gl.DYNAMIC_DRAW
1694        else:
1695            usage = gl.STATIC_DRAW
1696        gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.buf)
1697        gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, data=data, type=gl.UNSIGNED_SHORT, usage=usage)
1698
1699    def draw(self):
1700        gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.buf)
1701        gl.DrawElements(gl.TRIANGLES, self.vertices, gl.UNSIGNED_SHORT, 0)
1702
1703
1704def GenerateSpotMesh():
1705    global SpotVertices, SpotIndices
1706    rx0 = SpotRadius * PixelX
1707    ry0 = SpotRadius * PixelY
1708    rx1 = (SpotRadius + BoxEdgeSize) * PixelX
1709    ry1 = (SpotRadius + BoxEdgeSize) * PixelY
1710    slices = max(MinSpotDetail, int(2.0 * pi * SpotRadius / SpotDetail / ZoomArea))
1711    SpotIndices = HighlightIndexBuffer(slices, reuse_buf=SpotIndices, dynamic=True)
1712
1713    vertices = [0.0, 0.0, 1.0]
1714    for i in range(slices):
1715        a = i * 2.0 * pi / slices
1716        vertices.extend([
1717            rx0 * sin(a), ry0 * cos(a), 1.0,
1718            rx1 * sin(a), ry1 * cos(a), 0.0
1719        ])
1720    if not SpotVertices:
1721        SpotVertices = gl.GenBuffers()
1722    gl.BindBuffer(gl.ARRAY_BUFFER, SpotVertices)
1723    gl.BufferData(gl.ARRAY_BUFFER, data=vertices, usage=gl.DYNAMIC_DRAW)
1724
1725
1726##### TRANSITIONS ##############################################################
1727
1728# base class for all transitions
1729class Transition(object):
1730
1731    # constructor: must instantiate (i.e. compile) all required shaders
1732    # and (optionally) perform some additional initialization
1733    def __init__(self):
1734        pass
1735
1736    # called once at the start of each transition
1737    def start(self):
1738        pass
1739
1740    # render a frame of the transition, using the relative time 't' and the
1741    # global texture identifiers Tcurrent and Tnext
1742    def render(self, t):
1743        pass
1744
1745# smoothstep() makes most transitions better :)
1746def smoothstep(t):
1747    return t * t * (3.0 - 2.0 * t)
1748
1749# an array containing all possible transition classes
1750AllTransitions = []
1751
1752
1753class Crossfade(Transition):
1754    """simple crossfade"""
1755    class CrossfadeShader(GLShader):
1756        vs = """
1757            attribute highp vec2 aPos;
1758            uniform highp vec4 uTexTransform;
1759            varying mediump vec2 vTexCoord;
1760            void main() {
1761                gl_Position = vec4(vec2(-1.0, 1.0) + aPos * vec2(2.0, -2.0), 0.0, 1.0);
1762                vTexCoord = uTexTransform.xy + aPos * uTexTransform.zw;
1763            }
1764        """
1765        fs = """
1766            uniform lowp sampler2D uTcurrent;
1767            uniform lowp sampler2D uTnext;
1768            uniform lowp float uTime;
1769            varying mediump vec2 vTexCoord;
1770            void main() {
1771                gl_FragColor = mix(texture2D(uTcurrent, vTexCoord), texture2D(uTnext, vTexCoord), uTime);
1772            }
1773        """
1774        attributes = { 0: 'aPos' }
1775        uniforms = [('uTnext', 1), 'uTexTransform', 'uTime']
1776    def __init__(self):
1777        shader = self.CrossfadeShader.get_instance().use()
1778        gl.Uniform4f(shader.uTexTransform, 0.0, 0.0, TexMaxS, TexMaxT)
1779    def render(self, t):
1780        shader = self.CrossfadeShader.get_instance().use()
1781        gl.set_texture(gl.TEXTURE_2D, Tnext, 1)
1782        gl.set_texture(gl.TEXTURE_2D, Tcurrent, 0)
1783        gl.Uniform1f(shader.uTime, t)
1784        SimpleQuad.draw()
1785AllTransitions.append(Crossfade)
1786
1787
1788class FadeOutFadeIn(Transition):
1789    "fade out to black and fade in again"
1790    def render(self, t):
1791        if t < 0.5:
1792            tex = Tcurrent
1793            t = 1.0 - 2.0 * t
1794        else:
1795            tex = Tnext
1796            t = 2.0 * t - 1.0
1797        TexturedRectShader.get_instance().draw(
1798            0.0, 0.0, 1.0, 1.0,
1799            s1=TexMaxS, t1=TexMaxT,
1800            tex=tex,
1801            color=(t, t, t, 1.0)
1802        )
1803AllTransitions.append(FadeOutFadeIn)
1804
1805
1806class Slide(Transition):
1807    def render(self, t):
1808        t = smoothstep(t)
1809        x = self.dx * t
1810        y = self.dy * t
1811        TexturedRectShader.get_instance().draw(
1812            x, y, x + 1.0, y + 1.0,
1813            s1=TexMaxS, t1=TexMaxT,
1814            tex=Tcurrent
1815        )
1816        TexturedRectShader.get_instance().draw(
1817            x - self.dx,       y - self.dy,
1818            x - self.dx + 1.0, y - self.dy + 1.0,
1819            s1=TexMaxS, t1=TexMaxT,
1820            tex=Tnext
1821        )
1822class SlideUp(Slide):
1823    "slide upwards"
1824    dx, dy = 0.0, -1.0
1825class SlideDown(Slide):
1826    "slide downwards"
1827    dx, dy = 0.0, 1.0
1828class SlideLeft(Slide):
1829    "slide to the left"
1830    dx, dy = -1.0, 0.0
1831class SlideRight(Slide):
1832    "slide to the right"
1833    dx, dy = 1.0, 0.0
1834AllTransitions.extend([SlideUp, SlideDown, SlideLeft, SlideRight])
1835
1836
1837class Squeeze(Transition):
1838    def render(self, t):
1839        for tex, x0, y0, x1, y1 in self.getparams(smoothstep(t)):
1840            TexturedRectShader.get_instance().draw(
1841                x0, y0, x1, y1,
1842                s1=TexMaxS, t1=TexMaxT,
1843                tex=tex
1844            )
1845class SqueezeUp(Squeeze):
1846    "squeeze upwards"
1847    def getparams(self, t):
1848        return ((Tcurrent, 0.0, 0.0, 1.0, 1.0 - t),
1849                (Tnext,    0.0, 1.0 - t, 1.0, 1.0))
1850class SqueezeDown(Squeeze):
1851    "squeeze downwards"
1852    def getparams(self, t):
1853        return ((Tcurrent, 0.0, t, 1.0, 1.0),
1854                (Tnext,    0.0, 0.0, 1.0, t))
1855class SqueezeLeft(Squeeze):
1856    "squeeze to the left"
1857    def getparams(self, t):
1858        return ((Tcurrent, 0.0, 0.0, 1.0 - t, 1.0),
1859                (Tnext,    1.0 - t, 0.0, 1.0, 1.0))
1860class SqueezeRight(Squeeze):
1861    "squeeze to the right"
1862    def getparams(self, t):
1863        return ((Tcurrent, t, 0.0, 1.0, 1.0),
1864                (Tnext,    0.0, 0.0, t, 1.0))
1865AllTransitions.extend([SqueezeUp, SqueezeDown, SqueezeLeft, SqueezeRight])
1866
1867
1868class Wipe(Transition):
1869    band_size = 0.5    # relative size of the wiping band
1870    rx, ry = 16, 16    # mask texture resolution
1871    class_mask = True  # True if the mask shall be shared between all instances of this subclass
1872    class WipeShader(GLShader):
1873        vs = """
1874            attribute highp vec2 aPos;
1875            uniform highp vec4 uTexTransform;
1876            uniform highp vec4 uMaskTransform;
1877            varying mediump vec2 vTexCoord;
1878            varying mediump vec2 vMaskCoord;
1879            void main() {
1880                gl_Position = vec4(vec2(-1.0, 1.0) + aPos * vec2(2.0, -2.0), 0.0, 1.0);
1881                vTexCoord = uTexTransform.xy + aPos * uTexTransform.zw;
1882                vMaskCoord = uMaskTransform.xy + aPos * uMaskTransform.zw;
1883            }
1884        """
1885        fs = """
1886            uniform lowp sampler2D uTcurrent;
1887            uniform lowp sampler2D uTnext;
1888            uniform mediump sampler2D uMaskTex;
1889            uniform mediump vec2 uAlphaTransform;
1890            varying mediump vec2 vTexCoord;
1891            varying mediump vec2 vMaskCoord;
1892            void main() {
1893                mediump float mask = texture2D(uMaskTex, vMaskCoord).r;
1894                mask = (mask + uAlphaTransform.x) * uAlphaTransform.y;
1895                mask = smoothstep(0.0, 1.0, mask);
1896                gl_FragColor = mix(texture2D(uTnext, vTexCoord), texture2D(uTcurrent, vTexCoord), mask);
1897                // gl_FragColor = texture2D(uMaskTex, vMaskCoord);  // uncomment for mask debugging
1898            }
1899        """
1900        attributes = { 0: 'aPos' }
1901        uniforms = [('uTnext', 1), ('uMaskTex', 2), 'uTexTransform', 'uMaskTransform', 'uAlphaTransform']
1902        def __init__(self):
1903            GLShader.__init__(self)
1904            self.mask_tex = gl.make_texture(gl.TEXTURE_2D, gl.CLAMP_TO_EDGE, gl.LINEAR)
1905    mask = None
1906    def __init__(self):
1907        shader = self.WipeShader.get_instance().use()
1908        gl.Uniform4f(shader.uTexTransform, 0.0, 0.0, TexMaxS, TexMaxT)
1909        if not self.class_mask:
1910            self.mask = self.prepare_mask()
1911        elif not self.mask:
1912            self.__class__.mask = self.prepare_mask()
1913    def start(self):
1914        shader = self.WipeShader.get_instance().use()
1915        gl.Uniform4f(shader.uMaskTransform,
1916            0.5 / self.rx, 0.5 / self.ry,
1917            1.0 - 1.0 / self.rx,
1918            1.0 - 1.0 / self.ry)
1919        gl.BindTexture(gl.TEXTURE_2D, shader.mask_tex)
1920        gl.TexImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, self.rx, self.ry, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, self.mask)
1921    def bind_mask_tex(self, shader):
1922        gl.set_texture(gl.TEXTURE_2D, shader.mask_tex, 2)
1923    def render(self, t):
1924        shader = self.WipeShader.get_instance().use()
1925        self.bind_mask_tex(shader)  # own method b/c WipeBrightness overrides it
1926        gl.set_texture(gl.TEXTURE_2D, Tnext, 1)
1927        gl.set_texture(gl.TEXTURE_2D, Tcurrent, 0)
1928        gl.Uniform2f(shader.uAlphaTransform,
1929            self.band_size - t * (1.0 + self.band_size),
1930            1.0 / self.band_size)
1931        SimpleQuad.draw()
1932    def prepare_mask(self):
1933        scale = 1.0 / (self.rx - 1)
1934        xx = [i * scale for i in range((self.rx + 3) & (~3))]
1935        scale = 1.0 / (self.ry - 1)
1936        yy = [i * scale for i in range(self.ry)]
1937        def iter2d():
1938            for y in yy:
1939                for x in xx:
1940                    yield (x, y)
1941        # detour via bytearray() required for Python 2 compatibility
1942        return bytes(bytearray(max(0, min(255, int(self.f(x, y) * 255.0 + 0.5))) for x, y in iter2d()))
1943    def f(self, x, y):
1944        return 0.5
1945class WipeLeft(Wipe):
1946    "wipe from right to left"
1947    def f(self, x, y):
1948        return 1.0 - x
1949class WipeRight(Wipe):
1950    "wipe from left to right"
1951    def f(self, x, y):
1952        return x
1953class WipeUp(Wipe):
1954    "wipe upwards"
1955    def f(self, x, y):
1956        return 1.0 - y
1957class WipeDown(Wipe):
1958    "wipe downwards"
1959    def f(self, x, y):
1960        return y
1961class WipeUpLeft(Wipe):
1962    "wipe from the lower-right to the upper-left corner"
1963    def f(self, x, y):
1964        return 1.0 - 0.5 * (x + y)
1965class WipeUpRight(Wipe):
1966    "wipe from the lower-left to the upper-right corner"
1967    def f(self, x, y):
1968        return 0.5 * (1.0 - y + x)
1969class WipeDownLeft(Wipe):
1970    "wipe from the upper-right to the lower-left corner"
1971    def f(self, x, y):
1972        return 0.5 * (1.0 - x + y)
1973class WipeDownRight(Wipe):
1974    "wipe from the upper-left to the lower-right corner"
1975    def f(self, x, y):
1976        return 0.5 * (x + y)
1977class WipeCenterOut(Wipe):
1978    "wipe from the center outwards"
1979    rx, ry = 64, 32
1980    def __init__(self):
1981        self.scale = 1.0
1982        self.scale = 1.0 / self.f(0.0, 0.0)
1983        Wipe.__init__(self)
1984    def f(self, x, y):
1985        return hypot((x - 0.5) * DAR, y - 0.5) * self.scale
1986class WipeCenterIn(Wipe):
1987    "wipe from the corners inwards"
1988    rx, ry = 64, 32
1989    def __init__(self):
1990        self.scale = 1.0
1991        self.scale = 1.0 / (1.0 - self.f(0.0, 0.0))
1992        Wipe.__init__(self)
1993    def f(self, x, y):
1994        return 1.0 - hypot((x - 0.5) * DAR, y - 0.5) * self.scale
1995class WipeBlobs(Wipe):
1996    """wipe using nice "blob"-like patterns"""
1997    rx, ry = 64, 32
1998    class_mask = False
1999    def __init__(self):
2000        self.x0 = random.random() * 6.2
2001        self.y0 = random.random() * 6.2
2002        self.sx = (random.random() * 15.0 + 5.0) * DAR
2003        self.sy =  random.random() * 15.0 + 5.0
2004        Wipe.__init__(self)
2005    def f(self, x, y):
2006        return 0.5 + 0.25 * (cos(self.x0 + self.sx * x) + cos(self.y0 + self.sy * y))
2007class WipeClouds(Wipe):
2008    """wipe using cloud-like patterns"""
2009    rx, ry = 128, 128
2010    class_mask = False
2011    decay = 0.25
2012    blur = 5
2013    def prepare_mask(self):
2014        assert self.rx == self.ry
2015        noise = Image.frombytes('L', (self.rx * 4, self.ry * 2), bytes(bytearray(random.randrange(256) for i in range(self.rx * self.ry * 8))))
2016        img = Image.new('L', (1, 1), random.randrange(256))
2017        alpha = 1.0
2018        npos = 0
2019        border = 0
2020        while img.size[0] <= self.rx:
2021            border += 2
2022            next = img.size[0] * 2
2023            alpha *= self.decay
2024            img = Image.blend(
2025                img.resize((next, next), Image.BILINEAR),
2026                noise.crop((npos, 0, npos + next, next)),
2027                alpha)
2028            npos += next
2029        img = ImageOps.equalize(ImageOps.autocontrast(img))
2030        for i in range(self.blur):
2031            img = img.filter(ImageFilter.BLUR)
2032        img = img.crop((border, border, img.size[0] - 2 * border, img.size[1] - 2 * border)).resize((self.rx, self.ry), Image.ANTIALIAS)
2033        return img2str(img)
2034class WipeBrightness1(Wipe):
2035    """wipe based on the current slide's brightness"""
2036    band_size = 1.0
2037    def prepare_mask(self):
2038        return True  # dummy
2039    def start(self):
2040        shader = self.WipeShader.get_instance().use()
2041        gl.Uniform4f(shader.uMaskTransform, 0.0, 0.0, TexMaxS, TexMaxT)
2042    def bind_mask_tex(self, dummy):
2043        gl.set_texture(gl.TEXTURE_2D, Tcurrent, 2)
2044class WipeBrightness2(WipeBrightness1):
2045    """wipe based on the next slide's brightness"""
2046    def bind_mask_tex(self, dummy):
2047        gl.set_texture(gl.TEXTURE_2D, Tnext, 2)
2048AllTransitions.extend([WipeLeft, WipeRight, WipeUp, WipeDown, WipeUpLeft, WipeUpRight, WipeDownLeft, WipeDownRight, WipeCenterOut, WipeCenterIn, WipeBlobs, WipeClouds, WipeBrightness1, WipeBrightness2])
2049
2050
2051class PagePeel(Transition):
2052    "an unrealistic, but nice page peel effect"
2053    class PagePeel_PeeledPageShader(GLShader):
2054        vs = """
2055            attribute highp vec2 aPos;
2056            uniform highp vec4 uPosTransform;
2057            varying mediump vec2 vTexCoord;
2058            void main() {
2059                highp vec2 pos = uPosTransform.xy + aPos * uPosTransform.zw;
2060                gl_Position = vec4(vec2(-1.0, 1.0) + pos * vec2(2.0, -2.0), 0.0, 1.0);
2061                vTexCoord = aPos + vec2(0.0, -0.5);
2062            }
2063        """
2064        fs = """
2065            uniform lowp sampler2D uTex;
2066            uniform highp vec4 uTexTransform;
2067            uniform highp float uHeight;
2068            uniform mediump float uShadowStrength;
2069            varying mediump vec2 vTexCoord;
2070            void main() {
2071                mediump vec2 tc = vTexCoord;
2072                tc.y *= 1.0 - tc.x * uHeight;
2073                tc.x = mix(tc.x, tc.x * tc.x, uHeight);
2074                tc = uTexTransform.xy + (tc + vec2(0.0, 0.5)) * uTexTransform.zw;
2075                mediump float shadow_pos = 1.0 - vTexCoord.x;
2076                mediump float light = 1.0 - (shadow_pos * shadow_pos) * uShadowStrength;
2077                gl_FragColor = vec4(light, light, light, 1.0) * texture2D(uTex, tc);
2078            }
2079        """
2080        attributes = { 0: 'aPos' }
2081        uniforms = ['uPosTransform', 'uTexTransform', 'uHeight', 'uShadowStrength']
2082    class PagePeel_RevealedPageShader(GLShader):
2083        vs = """
2084            attribute highp vec2 aPos;
2085            uniform highp vec4 uPosTransform;
2086            uniform highp vec4 uTexTransform;
2087            varying mediump vec2 vTexCoord;
2088            varying mediump float vShadowPos;
2089            void main() {
2090                highp vec2 pos = uPosTransform.xy + aPos * uPosTransform.zw;
2091                gl_Position = vec4(vec2(-1.0, 1.0) + pos * vec2(2.0, -2.0), 0.0, 1.0);
2092                vShadowPos = 1.0 - aPos.x;
2093                vTexCoord = uTexTransform.xy + aPos * uTexTransform.zw;
2094            }
2095        """
2096        fs = """
2097            uniform lowp sampler2D uTex;
2098            uniform mediump float uShadowStrength;
2099            varying mediump vec2 vTexCoord;
2100            varying mediump float vShadowPos;
2101            void main() {
2102                mediump float light = 1.0 - (vShadowPos * vShadowPos) * uShadowStrength;
2103                gl_FragColor = vec4(light, light, light, 1.0) * texture2D(uTex, vTexCoord);
2104            }
2105        """
2106        attributes = { 0: 'aPos' }
2107        uniforms = ['uPosTransform', 'uTexTransform', 'uShadowStrength']
2108    def __init__(self):
2109        shader = self.PagePeel_PeeledPageShader.get_instance().use()
2110        gl.Uniform4f(shader.uTexTransform, 0.0, 0.0, TexMaxS, TexMaxT)
2111        self.PagePeel_RevealedPageShader.get_instance()
2112    def render(self, t):
2113        angle = t * 0.5 * pi
2114        split = cos(angle)
2115        height = sin(angle)
2116        # draw the old page that is peeled away
2117        gl.BindTexture(gl.TEXTURE_2D, Tcurrent)
2118        shader = self.PagePeel_PeeledPageShader.get_instance().use()
2119        gl.Uniform4f(shader.uPosTransform, 0.0, 0.0, split, 1.0)
2120        gl.Uniform1f(shader.uHeight, height * 0.25)
2121        gl.Uniform1f(shader.uShadowStrength, 0.2 * (1.0 - split));
2122        SimpleQuad.draw()
2123        # draw the new page that is revealed
2124        gl.BindTexture(gl.TEXTURE_2D, Tnext)
2125        shader = self.PagePeel_RevealedPageShader.get_instance().use()
2126        gl.Uniform4f(shader.uPosTransform, split, 0.0, 1.0 - split, 1.0)
2127        gl.Uniform4f(shader.uTexTransform, split * TexMaxS, 0.0, (1.0 - split) * TexMaxS, TexMaxT)
2128        gl.Uniform1f(shader.uShadowStrength, split);
2129        SimpleQuad.draw()
2130AllTransitions.append(PagePeel)
2131
2132
2133# the AvailableTransitions array contains a list of all transition classes that
2134# can be randomly assigned to pages;
2135# this selection normally only includes "unintrusive" transtitions, i.e. mostly
2136# crossfade/wipe variations
2137AvailableTransitions = [ # from coolest to lamest
2138    WipeBlobs,
2139    WipeCenterOut,
2140    WipeDownRight, WipeRight, WipeDown
2141]
2142
2143
2144##### OSD FONT RENDERER ########################################################
2145
2146typesUnicodeType = type(u'unicode')
2147typesStringType = type(b'bytestring')
2148
2149# force a string or sequence of ordinals into a unicode string
2150def ForceUnicode(s, charset='iso8859-15'):
2151    if type(s) == typesUnicodeType:
2152        return s
2153    if type(s) == typesStringType:
2154        return s.decode(charset, 'ignore')
2155    if isinstance(s, (tuple, list, range)):
2156        try:
2157            unichr
2158        except NameError:
2159            unichr = chr
2160        return u''.join(map(unichr, s))
2161    raise TypeError("string argument not convertible to Unicode")
2162
2163# search a system font path for a font file
2164def SearchFont(root, name):
2165    if not os.path.isdir(root):
2166        return None
2167    infix = ""
2168    fontfile = []
2169    while (len(infix) < 10) and not(fontfile):
2170        fontfile = list(filter(os.path.isfile, glob.glob(root + infix + name)))
2171        infix += "*/"
2172    if not fontfile:
2173        return None
2174    else:
2175        return fontfile[0]
2176
2177# load a system font
2178def LoadFont(dirs, name, size):
2179    # first try to load the font directly
2180    try:
2181        return ImageFont.truetype(name, size, encoding='unic')
2182    except:
2183        pass
2184    # no need to search further on Windows
2185    if os.name == 'nt':
2186        return None
2187    # start search for the font
2188    for dir in dirs:
2189        fontfile = SearchFont(dir + "/", name)
2190        if fontfile:
2191            try:
2192                return ImageFont.truetype(fontfile, size, encoding='unic')
2193            except:
2194                pass
2195    return None
2196
2197# alignment constants
2198Left = 0
2199Right = 1
2200Center = 2
2201Down = 0
2202Up = 1
2203Auto = -1
2204
2205# font renderer class
2206class GLFont:
2207    def __init__(self, width, height, name, size, search_path=[], default_charset='iso8859-15', extend=1, blur=1):
2208        self.width = width
2209        self.height = height
2210        self._i_extend = range(extend)
2211        self._i_blur = range(blur)
2212        self.feather = extend + blur + 1
2213        self.current_x = 0
2214        self.current_y = 0
2215        self.max_height = 0
2216        self.boxes = {}
2217        self.widths = {}
2218        self.line_height = 0
2219        self.default_charset = default_charset
2220        if isinstance(name, basestring):
2221            self.font = LoadFont(search_path, name, size)
2222        else:
2223            for check_name in name:
2224                self.font = LoadFont(search_path, check_name, size)
2225                if self.font: break
2226        if not self.font:
2227            raise IOError("font file not found")
2228        self.img = Image.new('LA', (width, height))
2229        self.alpha = Image.new('L', (width, height))
2230        self.extend = ImageFilter.MaxFilter()
2231        self.blur = ImageFilter.Kernel((3, 3), [1,2,1,2,4,2,1,2,1])
2232        self.tex = gl.make_texture(gl.TEXTURE_2D, filter=gl.NEAREST)
2233        self.AddString(range(32, 128))
2234        self.vertices = None
2235        self.index_buffer = None
2236        self.index_buffer_capacity = 0
2237
2238    def AddCharacter(self, c):
2239        w, h = self.font.getsize(c)
2240        try:
2241            ox, oy = self.font.getoffset(c)
2242            w += ox
2243            h += oy
2244        except AttributeError:
2245            pass
2246        self.line_height = max(self.line_height, h)
2247        size = (w + 2 * self.feather, h + 2 * self.feather)
2248        glyph = Image.new('L', size)
2249        draw = ImageDraw.Draw(glyph)
2250        draw.text((self.feather, self.feather), c, font=self.font, fill=255)
2251        del draw
2252
2253        box = self.AllocateGlyphBox(*size)
2254        self.img.paste(glyph, (box.orig_x, box.orig_y))
2255
2256        for i in self._i_extend: glyph = glyph.filter(self.extend)
2257        for i in self._i_blur:   glyph = glyph.filter(self.blur)
2258        self.alpha.paste(glyph, (box.orig_x, box.orig_y))
2259
2260        self.boxes[c] = box
2261        self.widths[c] = w
2262        del glyph
2263
2264    def AddString(self, s, charset=None, fail_silently=False):
2265        update_count = 0
2266        try:
2267            for c in ForceUnicode(s, self.GetCharset(charset)):
2268                if c in self.widths:
2269                    continue
2270                self.AddCharacter(c)
2271                update_count += 1
2272        except ValueError:
2273            if fail_silently:
2274                pass
2275            else:
2276                raise
2277        if not update_count: return
2278        self.img.putalpha(self.alpha)
2279        gl.load_texture(gl.TEXTURE_2D, self.tex, self.img)
2280
2281    def AllocateGlyphBox(self, w, h):
2282        if self.current_x + w > self.width:
2283            self.current_x = 0
2284            self.current_y += self.max_height
2285            self.max_height = 0
2286        if self.current_y + h > self.height:
2287            raise ValueError("bitmap too small for all the glyphs")
2288        box = self.GlyphBox()
2289        box.orig_x = self.current_x
2290        box.orig_y = self.current_y
2291        box.size_x = w
2292        box.size_y = h
2293        box.x0 =  self.current_x      / float(self.width)
2294        box.y0 =  self.current_y      / float(self.height)
2295        box.x1 = (self.current_x + w) / float(self.width)
2296        box.y1 = (self.current_y + h) / float(self.height)
2297        box.dsx = w * PixelX
2298        box.dsy = h * PixelY
2299        self.current_x += w
2300        self.max_height = max(self.max_height, h)
2301        return box
2302
2303    def GetCharset(self, charset=None):
2304        if charset: return charset
2305        return self.default_charset
2306
2307    def SplitText(self, s, charset=None):
2308        return ForceUnicode(s, self.GetCharset(charset)).split(u'\n')
2309
2310    def GetLineHeight(self):
2311        return self.line_height
2312
2313    def GetTextWidth(self, s, charset=None):
2314        return max([self.GetTextWidthEx(line) for line in self.SplitText(s, charset)])
2315
2316    def GetTextHeight(self, s, charset=None):
2317        return len(self.SplitText(s, charset)) * self.line_height
2318
2319    def GetTextSize(self, s, charset=None):
2320        lines = self.SplitText(s, charset)
2321        return (max([self.GetTextWidthEx(line) for line in lines]), len(lines) * self.line_height)
2322
2323    def GetTextWidthEx(self, u):
2324        if u: return sum([self.widths.get(c, 0) for c in u])
2325        else: return 0
2326
2327    def GetTextHeightEx(self, u=[]):
2328        return self.line_height
2329
2330    def AlignTextEx(self, x, u, align=Left):
2331        if not align: return x
2332        return x - self.GetTextWidthEx(u) // align
2333
2334    class FontShader(GLShader):
2335        vs = """
2336            attribute highp vec4 aPosAndTexCoord;
2337            varying mediump vec2 vTexCoord;
2338            void main() {
2339                gl_Position = vec4(vec2(-1.0, 1.0) + aPosAndTexCoord.xy * vec2(2.0, -2.0), 0.0, 1.0);
2340                vTexCoord = aPosAndTexCoord.zw;
2341            }
2342        """
2343        fs = """
2344            uniform lowp sampler2D uTex;
2345            uniform lowp vec4 uColor;
2346            varying mediump vec2 vTexCoord;
2347            void main() {
2348                gl_FragColor = uColor * texture2D(uTex, vTexCoord);
2349            }
2350        """
2351        attributes = { 0: 'aPosAndTexCoord' }
2352        uniforms = ['uColor']
2353
2354    def BeginDraw(self):
2355        self.vertices = []
2356
2357    def EndDraw(self, color=(1.0, 1.0, 1.0), alpha=1.0, beveled=True):
2358        if not self.vertices:
2359            self.vertices = None
2360            return
2361        char_count = len(self.vertices) // 16
2362        if char_count > 16383:
2363            print("Internal Error: too many characters (%d) to display in one go, truncating." % char_count, file=sys.stderr)
2364            char_count = 16383
2365
2366        # create an index buffer large enough for the text
2367        if not(self.index_buffer) or (self.index_buffer_capacity < char_count):
2368            self.index_buffer_capacity = (char_count + 63) & (~63)
2369            data = []
2370            for b in range(0, self.index_buffer_capacity * 4, 4):
2371                data.extend([b+0, b+2, b+1, b+1, b+2, b+3])
2372            if not self.index_buffer:
2373                self.index_buffer = gl.GenBuffers()
2374            gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.index_buffer)
2375            gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, data=data, type=gl.UNSIGNED_SHORT, usage=gl.DYNAMIC_DRAW)
2376        else:
2377            gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.index_buffer)
2378
2379        # set the vertex buffer
2380        vbuf = (c_float * len(self.vertices))(*self.vertices)
2381        gl.BindBuffer(gl.ARRAY_BUFFER, 0)
2382        gl.set_enabled_attribs(0)
2383        gl.VertexAttribPointer(0, 4, gl.FLOAT, False, 0, vbuf)
2384
2385        # draw it
2386        shader = self.FontShader.get_instance().use()
2387        gl.BindTexture(gl.TEXTURE_2D, self.tex)
2388        if beveled:
2389            gl.BlendFunc(gl.ZERO, gl.ONE_MINUS_SRC_ALPHA)
2390            gl.Uniform4f(shader.uColor, 0.0, 0.0, 0.0, alpha)
2391            gl.DrawElements(gl.TRIANGLES, char_count * 6, gl.UNSIGNED_SHORT, 0)
2392        gl.BlendFunc(gl.ONE, gl.ONE)
2393        gl.Uniform4f(shader.uColor, color[0] * alpha, color[1] * alpha, color[2] * alpha, 1.0)
2394        gl.DrawElements(gl.TRIANGLES, char_count * 6, gl.UNSIGNED_SHORT, 0)
2395        gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
2396        self.vertices = None
2397
2398    def Draw(self, origin, text, charset=None, align=Left, color=(1.0, 1.0, 1.0), alpha=1.0, beveled=True, bold=False):
2399        own_draw = (self.vertices is None)
2400        if own_draw:
2401            self.BeginDraw()
2402        lines = self.SplitText(text, charset)
2403        x0, y = origin
2404        x0 -= self.feather
2405        y -= self.feather
2406        for line in lines:
2407            sy = y * PixelY
2408            x = self.AlignTextEx(x0, line, align)
2409            for c in line:
2410                if not c in self.widths: continue
2411                self.boxes[c].add_vertices(self.vertices, x * PixelX, sy)
2412                x += self.widths[c]
2413            y += self.line_height
2414        if bold and not(beveled):
2415            self.Draw((origin[0] + 1, origin[1]), text, charset=charset, align=align, color=color, alpha=alpha, beveled=False, bold=False)
2416        if own_draw:
2417            self.EndDraw(color, alpha, beveled)
2418
2419    class GlyphBox:
2420        def add_vertices(self, vertex_list, sx=0.0, sy=0.0):
2421            vertex_list.extend([
2422                sx,            sy,            self.x0, self.y0,
2423                sx + self.dsx, sy,            self.x1, self.y0,
2424                sx,            sy + self.dsy, self.x0, self.y1,
2425                sx + self.dsx, sy + self.dsy, self.x1, self.y1,
2426            ])
2427
2428# high-level draw function
2429def DrawOSD(x, y, text, halign=Auto, valign=Auto, alpha=1.0):
2430    if not(OSDFont) or not(text) or (alpha <= 0.004): return
2431    if alpha > 1.0: alpha = 1.0
2432    if halign == Auto:
2433        if x < 0:
2434            x += ScreenWidth
2435            halign = Right
2436        else:
2437            halign = Left
2438    if HalfScreen and (halign == Left):
2439        x += ScreenWidth // 2
2440    if valign == Auto:
2441        if y < 0:
2442            y += ScreenHeight
2443            valign = Up
2444        else:
2445            valign = Down
2446        if valign != Down:
2447            y -= OSDFont.GetLineHeight() // valign
2448    OSDFont.Draw((x, y), text, align=halign, alpha=alpha)
2449
2450# very high-level draw function
2451def DrawOSDEx(position, text, alpha_factor=1.0):
2452    xpos = position >> 1
2453    y = (1 - 2 * (position & 1)) * OSDMargin
2454    if xpos < 2:
2455        x = (1 - 2 * xpos) * OSDMargin
2456        halign = Auto
2457    else:
2458        x = ScreenWidth // 2
2459        halign = Center
2460    DrawOSD(x, y, text, halign, alpha = OSDAlpha * alpha_factor)
2461
2462RequiredShaders.append(GLFont.FontShader)
2463
2464
2465##### PDF PARSER ###############################################################
2466
2467typesUnicodeType = type(u'unicode')
2468typesStringType = type(b'bytestring')
2469
2470class PDFError(Exception):
2471    pass
2472
2473class PDFref:
2474    def __init__(self, ref):
2475        self.ref = ref
2476    def __repr__(self):
2477        return "PDFref(%d)" % self.ref
2478
2479re_pdfstring = re.compile(r'\(\)|\(.*?[^\\]\)')
2480pdfstringrepl = [("\\"+x[0], x[1:]) for x in "(( )) n\n r\r t\t".split(" ")]
2481def pdf_maskstring(s):
2482    s = s[1:-1]
2483    for a, b in pdfstringrepl:
2484        s = s.replace(a, b)
2485    return " <" + "".join(["%02X"%ord(c) for c in s]) + "> "
2486def pdf_mask_all_strings(s):
2487    return re_pdfstring.sub(lambda x: pdf_maskstring(x.group(0)), s)
2488def pdf_unmaskstring(s):
2489    s = bytes(bytearray(int(s[i:i+2], 16) for i in range(1, len(s)-1, 2)))
2490    try:
2491        return s.decode('utf-8')
2492    except UnicodeDecodeError:
2493        return s.decode('windows-1252', 'replace')
2494
2495class PDFParser:
2496    def __init__(self, filename):
2497        self.f = open(filename, "rb")
2498        self.errors = 0
2499
2500        # find the first cross-reference table
2501        self.f.seek(0, 2)
2502        filesize = self.f.tell()
2503        self.f.seek(filesize - 128)
2504        trailer = self.f.read().decode()
2505        i = trailer.rfind("startxref")
2506        if i < 0:
2507            raise PDFError("cross-reference table offset missing")
2508        try:
2509            offset = int(trailer[i:].split("\n")[1].strip())
2510        except (IndexError, ValueError):
2511            raise PDFError("malformed cross-reference table offset")
2512
2513        # follow the trailer chain
2514        self.xref = {}
2515        while offset:
2516            newxref = self.xref
2517            self.xref, rootref, offset = self.parse_trailer(offset)
2518            self.xref.update(newxref)
2519
2520        # scan the page and names tree
2521        self.obj2page = {}
2522        self.page2obj = {}
2523        self.annots = {}
2524        self.page_count = 0
2525        self.box = {}
2526        self.names = {}
2527        self.rotate = {}
2528        root = self.getobj(rootref, 'Catalog')
2529        try:
2530            self.scan_page_tree(root['Pages'].ref)
2531        except KeyError:
2532            raise PDFError("root page tree node missing")
2533        try:
2534            self.scan_names_tree(root['Names'].ref)
2535        except KeyError:
2536            pass
2537
2538    def getline(self):
2539        while True:
2540            line = self.f.readline().strip()
2541            if line: return line
2542
2543    def find_length(self, tokens, begin, end):
2544        level = 1
2545        for i in range(1, len(tokens)):
2546            if tokens[i] == begin:  level += 1
2547            if tokens[i] == end:    level -= 1
2548            if not level: break
2549        return i + 1
2550
2551    def parse_tokens(self, tokens, want_list=False):
2552        res = []
2553        while tokens:
2554            t = tokens[0]
2555            v = t
2556            tlen = 1
2557            if (len(tokens) >= 3) and (tokens[2] == 'R'):
2558                v = PDFref(int(t))
2559                tlen = 3
2560            elif t == "<<":
2561                tlen = self.find_length(tokens, "<<", ">>")
2562                v = self.parse_tokens(tokens[1 : tlen - 1], True)
2563                v = dict(zip(v[::2], v[1::2]))
2564            elif t == "[":
2565                tlen = self.find_length(tokens, "[", "]")
2566                v = self.parse_tokens(tokens[1 : tlen - 1], True)
2567            elif not(t) or (t[0] == "null"):
2568                v = None
2569            elif (t[0] == '<') and (t[-1] == '>'):
2570                v = pdf_unmaskstring(t)
2571            elif t[0] == '/':
2572                v = t[1:]
2573            elif t == 'null':
2574                v = None
2575            else:
2576                try:
2577                    v = float(t)
2578                    v = int(t)
2579                except ValueError:
2580                    pass
2581            res.append(v)
2582            del tokens[:tlen]
2583        if want_list:
2584            return res
2585        if not res:
2586            return None
2587        if len(res) == 1:
2588            return res[0]
2589        return res
2590
2591    def parse(self, data):
2592        data = pdf_mask_all_strings(data)
2593        data = data.replace("<<", " << ").replace("[", " [ ").replace("(", " (")
2594        data = data.replace(">>", " >> ").replace("]", " ] ").replace(")", ") ")
2595        data = data.replace("/", " /").replace("><", "> <")
2596        return self.parse_tokens(list(filter(None, data.split())))
2597
2598    def getobj(self, obj, force_type=None):
2599        if isinstance(obj, PDFref):
2600            obj = obj.ref
2601        if type(obj) != int:
2602            raise PDFError("object is not a valid reference")
2603        offset = self.xref.get(obj, 0)
2604        if not offset:
2605            raise PDFError("referenced non-existing PDF object")
2606        self.f.seek(offset)
2607        header = self.getline().decode().split(None, 3)
2608        if (len(header) < 3) or (header[2] != "obj") or (header[0] != str(obj)):
2609            raise PDFError("object does not start where it's supposed to")
2610        if len(header) == 4:
2611            data = [header[3]]
2612        else:
2613            data = []
2614        while True:
2615            line = self.getline().decode()
2616            if line in ("endobj", "stream"): break
2617            data.append(line)
2618        data = self.parse(" ".join(data))
2619        if force_type:
2620            try:
2621                t = data['Type']
2622            except (KeyError, IndexError, ValueError):
2623                t = None
2624            if t != force_type:
2625                raise PDFError("object does not match the intended type")
2626        return data
2627
2628    def resolve(self, obj):
2629        if isinstance(obj, PDFref):
2630            return self.getobj(obj)
2631        else:
2632            return obj
2633
2634    def parse_xref_section(self, start, count):
2635        xref = {}
2636        for obj in range(start, start + count):
2637            line = self.getline()
2638            if line[-1] == 'f':
2639                xref[obj] = 0
2640            else:
2641                xref[obj] = int(line[:10], 10)
2642        return xref
2643
2644    def parse_trailer(self, offset):
2645        self.f.seek(offset)
2646        xref = {}
2647        rootref = 0
2648        offset = 0
2649        if self.getline() != b"xref":
2650            raise PDFError("cross-reference table does not start where it's supposed to")
2651            return (xref, rootref, offset)   # no xref table found, abort
2652        # parse xref sections
2653        while True:
2654            line = self.getline()
2655            if line == b"trailer": break
2656            start, count = map(int, line.split())
2657            xref.update(self.parse_xref_section(start, count))
2658        # parse trailer
2659        trailer = ""
2660        while True:
2661            line = self.getline().decode()
2662            if line in ("startxref", "b%%EOF"): break
2663            trailer += line
2664        trailer = self.parse(trailer)
2665        try:
2666            rootref = trailer['Root'].ref
2667        except KeyError:
2668            raise PDFError("root catalog entry missing")
2669        except AttributeError:
2670            raise PDFError("root catalog entry is not a reference")
2671        return (xref, rootref, trailer.get('Prev', 0))
2672
2673    def scan_page_tree(self, obj, mbox=None, cbox=None, rotate=0):
2674        try:
2675            node = self.getobj(obj)
2676            if node['Type'] == 'Pages':
2677                for kid in node['Kids']:
2678                    self.scan_page_tree(kid.ref, \
2679                                        node.get('MediaBox', mbox), \
2680                                        node.get('CropBox', cbox), \
2681                                        node.get('Rotate', 0))
2682            else:
2683                page = self.page_count + 1
2684                anode = node.get('Annots', [])
2685                if anode.__class__ == PDFref:
2686                    anode = self.getobj(anode.ref)
2687                self.page_count = page
2688                self.obj2page[obj] = page
2689                self.page2obj[page] = obj
2690                self.box[page] = self.resolve(node.get('CropBox', cbox) or node.get('MediaBox', mbox))
2691                self.rotate[page] = node.get('Rotate', rotate)
2692                self.annots[page] = [a.ref for a in anode]
2693        except (KeyError, TypeError, ValueError):
2694            self.errors += 1
2695
2696    def scan_names_tree(self, obj, come_from=None, name=None):
2697        try:
2698            node = self.getobj(obj)
2699            # if we came from the root node, proceed to Dests
2700            if not come_from:
2701                for entry in ('Dests', ):
2702                    if entry in node:
2703                        self.scan_names_tree(node[entry], entry)
2704            elif come_from == 'Dests':
2705                if 'Kids' in node:
2706                    for kid in node['Kids']:
2707                        self.scan_names_tree(kid, come_from)
2708                elif 'Names' in node:
2709                    nlist = node['Names']
2710                    while (len(nlist) >= 2) \
2711                    and (type(nlist[0]) == str) \
2712                    and (nlist[1].__class__ == PDFref):
2713                        self.scan_names_tree(nlist[1], come_from, nlist[0])
2714                        del nlist[:2]
2715                elif name and ('D' in node):
2716                    page = self.dest2page(node['D'])
2717                    if page:
2718                        self.names[name] = page
2719            # else: unsupported node, don't care
2720        except PDFError:
2721            self.errors += 1
2722
2723    def dest2page(self, dest):
2724        if type(dest) in (typesStringType, typesUnicodeType):
2725            return self.names.get(dest, None)
2726        if not isinstance(dest, list):
2727            return dest
2728        elif dest[0].__class__ == PDFref:
2729            return self.obj2page.get(dest[0].ref, None)
2730        else:
2731            return dest[0]
2732
2733    def get_href(self, obj):
2734        try:
2735            node = self.getobj(obj, 'Annot')
2736            if node['Subtype'] != 'Link': return None
2737            dest = None
2738            if 'Dest' in node:
2739                dest = self.dest2page(node['Dest'])
2740            elif 'A' in node:
2741                a = node['A']
2742                if isinstance(a, PDFref):
2743                    a = self.getobj(a)
2744                action = a['S']
2745                if action == 'URI':
2746                    dest = a.get('URI', None)
2747                    for prefix in ("file://", "file:", "run://", "run:"):
2748                        if dest.startswith(prefix):
2749                            dest = urllib.unquote(dest[len(prefix):])
2750                            break
2751                elif action == 'Launch':
2752                    dest = a.get('F', None)
2753                    if isinstance(dest, PDFref):
2754                        dest = self.getobj(dest)
2755                    if isinstance(dest, dict):
2756                        dest = dest.get('F', None) or dest.get('Unix', None)
2757                    if not isinstance(dest, basestring):
2758                        dest = None  # still an unknown type -> ignore it
2759                elif action == 'GoTo':
2760                    dest = self.dest2page(a.get('D', None))
2761            if dest:
2762                return tuple(node['Rect'] + [dest])
2763        except PDFError:
2764            self.errors += 1
2765
2766    def GetHyperlinks(self):
2767        res = {}
2768        for page in self.annots:
2769            try:
2770                a = list(filter(None, map(self.get_href, self.annots[page])))
2771            except (PDFError, TypeError, ValueError):
2772                self.errors += 1
2773                a = None
2774            if a: res[page] = a
2775        return res
2776
2777
2778def rotate_coord(x, y, rot):
2779    if   rot == 1: x, y = 1.0 - y,       x
2780    elif rot == 2: x, y = 1.0 - x, 1.0 - y
2781    elif rot == 3: x, y =       y, 1.0 - x
2782    return (x, y)
2783
2784
2785def AddHyperlink(page_offset, page, target, linkbox, pagebox, rotate):
2786    page += page_offset
2787    if isinstance(target, int):
2788        target += page_offset
2789
2790    # compute relative position of the link on the page
2791    w = 1.0 / (pagebox[2] - pagebox[0])
2792    h = 1.0 / (pagebox[3] - pagebox[1])
2793    x0 = (linkbox[0] - pagebox[0]) * w
2794    y0 = (pagebox[3] - linkbox[3]) * h
2795    x1 = (linkbox[2] - pagebox[0]) * w
2796    y1 = (pagebox[3] - linkbox[1]) * h
2797
2798    # get effective rotation
2799    rotate //= 90
2800    page_rot = GetPageProp(page, 'rotate')
2801    if page_rot is None:
2802        page_rot = Rotation
2803    if page_rot:
2804        rotate += page_rot
2805    while rotate < 0:
2806        rotate += 1000000
2807    rotate &= 3
2808
2809    # rotate the rectangle
2810    x0, y0 = rotate_coord(x0, y0, rotate)
2811    x1, y1 = rotate_coord(x1, y1, rotate)
2812    if x0 > x1: x0, x1 = x1, x0
2813    if y0 > y1: y0, y1 = y1, y0
2814
2815    # save the hyperlink
2816    href = (0, target, x0, y0, x1, y1)
2817    if GetPageProp(page, '_href'):
2818        PageProps[page]['_href'].append(href)
2819    else:
2820        SetPageProp(page, '_href', [href])
2821
2822
2823def FixHyperlinks(page):
2824    if not(GetPageProp(page, '_box')) or not(GetPageProp(page, '_href')):
2825        return  # no hyperlinks or unknown page size
2826    bx0, by0, bx1, by1 = GetPageProp(page, '_box')
2827    bdx = bx1 - bx0
2828    bdy = by1 - by0
2829    href = []
2830    for fixed, target, x0, y0, x1, y1 in GetPageProp(page, '_href'):
2831        if fixed:
2832            href.append((1, target, x0, y0, x1, y1))
2833        else:
2834            href.append((1, target, \
2835                int(bx0 + bdx * x0), int(by0 + bdy * y0), \
2836                int(bx0 + bdx * x1), int(by0 + bdy * y1)))
2837    SetPageProp(page, '_href', href)
2838
2839
2840def ParsePDF(filename):
2841    if Bare or not(TempFileName):
2842        return
2843    uncompressed = TempFileName + ".pdf"
2844    analyze = filename
2845
2846    # uncompress the file with either mutool or pdftk
2847    ok = False
2848    err = False
2849    for args in [  # prefer mutool over pdftk, as it's much faster and doesn't force-decompress images
2850        [mutoolPath, "clean", "-g", "-d", "-i", "-f", filename, uncompressed],
2851        [pdftkPath, filename, "output", uncompressed, "uncompress"],
2852    ]:
2853        if not args[0]:
2854            continue  # program not found
2855        try:
2856            assert 0 == Popen(args).wait()
2857            err = not(os.path.isfile(uncompressed))
2858        except (OSError, AssertionError):
2859            err = True
2860        if not err:
2861            ok = True
2862            analyze = uncompressed
2863            break
2864    if ok:
2865        pass
2866    elif err:
2867        print("Note: error while unpacking the PDF file, hyperlinks disabled.", file=sys.stderr)
2868        return
2869    else:
2870        print("Note: neither mutool nor pdftk found, hyperlinks disabled.", file=sys.stderr)
2871        return
2872
2873    count = 0
2874    try:
2875        try:
2876            pdf = PDFParser(analyze)
2877            for page, annots in pdf.GetHyperlinks().items():
2878                for page_offset in FileProps[filename]['offsets']:
2879                    for a in annots:
2880                        AddHyperlink(page_offset, page, a[4], a[:4], pdf.box[page], pdf.rotate[page])
2881                    FixHyperlinks(page + page_offset)
2882                count += len(annots)
2883            if pdf.errors:
2884                print("Note: failed to parse the PDF file, hyperlinks might not work properly", file=sys.stderr)
2885            del pdf
2886            return count
2887        except IOError:
2888            print("Note: intermediate PDF file not readable, hyperlinks disabled.", file=sys.stderr)
2889        except PDFError as e:
2890            print("Note: error in PDF file, hyperlinks disabled.", file=sys.stderr)
2891            print("      PDF parser error message:", e, file=sys.stderr)
2892    finally:
2893        try:
2894            os.remove(uncompressed)
2895        except OSError:
2896            pass
2897
2898
2899##### PAGE CACHE MANAGEMENT ####################################################
2900
2901# helper class that allows PIL to write and read image files with an offset
2902class IOWrapper:
2903    def __init__(self, f, offset=0):
2904        self.f = f
2905        self.offset = offset
2906        self.f.seek(offset)
2907    def read(self, count=None):
2908        if count is None:
2909            return self.f.read()
2910        else:
2911            return self.f.read(count)
2912    def write(self, data):
2913        self.f.write(data)
2914    def seek(self, pos, whence=0):
2915        assert(whence in (0, 1))
2916        if whence:
2917            self.f.seek(pos, 1)
2918        else:
2919            self.f.seek(pos + self.offset)
2920    def tell(self):
2921        return self.f.tell() - self.offset
2922
2923# generate a "magic number" that is used to identify persistent cache files
2924def UpdateCacheMagic():
2925    global CacheMagic
2926    pool = [PageCount, ScreenWidth, ScreenHeight, b2s(Scaling), b2s(Supersample), b2s(Rotation)]
2927    flist = list(FileProps.keys())
2928    flist.sort(key=lambda f: f.lower())
2929    for f in flist:
2930        pool.append(f)
2931        pool.extend(list(GetFileProp(f, 'stat', [])))
2932    CacheMagic = hashlib.md5(b'\0'.join(repr(x).encode('utf-8') for x in pool)).hexdigest().encode('ascii')
2933
2934# set the persistent cache file position to the current end of the file
2935def UpdatePCachePos():
2936    global CacheFilePos
2937    CacheFile.seek(0, 2)
2938    CacheFilePos = CacheFile.tell()
2939
2940# rewrite the header of the persistent cache
2941def WritePCacheHeader(reset=False):
2942    pages = [b"%08x" % PageCache.get(page, 0) for page in range(1, PageCount+1)]
2943    CacheFile.seek(0)
2944    CacheFile.write(CacheMagic + b"".join(pages))
2945    if reset:
2946        CacheFile.truncate()
2947    UpdatePCachePos()
2948
2949# return an image from the persistent cache or None if none is available
2950def GetPCacheImage(page):
2951    if CacheMode != PersistentCache:
2952        return  # not applicable if persistent cache isn't used
2953    Lcache.acquire()
2954    try:
2955        if page in PageCache:
2956            img = Image.open(IOWrapper(CacheFile, PageCache[page]))
2957            img.load()
2958            return img
2959    finally:
2960        Lcache.release()
2961
2962# returns an image from the non-persistent cache or None if none is available
2963def GetCacheImage(page):
2964    if CacheMode in (NoCache, PersistentCache):
2965        return  # not applicable in uncached or persistent-cache mode
2966    Lcache.acquire()
2967    try:
2968        if page in PageCache:
2969            if CacheMode == FileCache:
2970                CacheFile.seek(PageCache[page])
2971                return CacheFile.read(TexSize)
2972            elif CacheMode == CompressedCache:
2973                return zlib.decompress(PageCache[page])
2974            else:
2975                return PageCache[page]
2976    finally:
2977        Lcache.release()
2978
2979# adds an image to the persistent cache
2980def AddToPCache(page, img):
2981    if CacheMode != PersistentCache:
2982        return  # not applicable if persistent cache isn't used
2983    Lcache.acquire()
2984    try:
2985        if page in PageCache:
2986            return  # page is already cached and we can't update it safely
2987                    # -> stop here (the new image will be identical to the old
2988                    #    one anyway)
2989        img.save(IOWrapper(CacheFile, CacheFilePos), "ppm")
2990        PageCache[page] = CacheFilePos
2991        WritePCacheHeader()
2992    finally:
2993        Lcache.release()
2994
2995# adds an image to the non-persistent cache
2996def AddToCache(page, data):
2997    global CacheFilePos
2998    if CacheMode in (NoCache, PersistentCache):
2999        return  # not applicable in uncached or persistent-cache mode
3000    Lcache.acquire()
3001    try:
3002        if CacheMode == FileCache:
3003            if not(page in PageCache):
3004                PageCache[page] = CacheFilePos
3005                CacheFilePos += len(data)
3006            CacheFile.seek(PageCache[page])
3007            CacheFile.write(data)
3008        elif CacheMode == CompressedCache:
3009            PageCache[page] = zlib.compress(data, 1)
3010        else:
3011            PageCache[page] = data
3012    finally:
3013        Lcache.release()
3014
3015# invalidates the whole cache
3016def InvalidateCache():
3017    global PageCache, CacheFilePos
3018    Lcache.acquire()
3019    try:
3020        PageCache = {}
3021        if CacheMode == PersistentCache:
3022            UpdateCacheMagic()
3023            WritePCacheHeader(True)
3024        else:
3025            CacheFilePos = 0
3026    finally:
3027        Lcache.release()
3028
3029# initialize the persistent cache
3030def InitPCache():
3031    global CacheFile, CacheMode
3032
3033    # try to open the pre-existing cache file
3034    try:
3035        CacheFile = open(CacheFileName, "rb+")
3036    except IOError:
3037        CacheFile = None
3038
3039    # check the cache magic
3040    UpdateCacheMagic()
3041    if CacheFile and (CacheFile.read(32) != CacheMagic):
3042        print("Cache file mismatch, recreating cache.", file=sys.stderr)
3043        CacheFile.close()
3044        CacheFile = None
3045
3046    if CacheFile:
3047        # if the magic was valid, import cache data
3048        print("Using already existing persistent cache file.", file=sys.stderr)
3049        for page in range(1, PageCount+1):
3050            offset = int(CacheFile.read(8), 16)
3051            if offset:
3052                PageCache[page] = offset
3053        UpdatePCachePos()
3054    else:
3055        # if the magic was invalid or the file didn't exist, (re-)create it
3056        try:
3057            CacheFile = open(CacheFileName, "wb+")
3058        except IOError:
3059            print("Error: cannot write the persistent cache file (`%s')" % CacheFileName, file=sys.stderr)
3060            print("Falling back to temporary file cache.", file=sys.stderr)
3061            CacheMode = FileCache
3062        WritePCacheHeader()
3063
3064
3065##### PAGE RENDERING ###########################################################
3066
3067class RenderError(RuntimeError):
3068    pass
3069class RendererUnavailable(RenderError):
3070    pass
3071
3072class PDFRendererBase(object):
3073    name = None
3074    binaries = []
3075    test_run_args = []
3076    supports_anamorphic = False
3077    required_options = []
3078    needs_tempfile = True
3079
3080    @classmethod
3081    def supports(self, binary):
3082        if not binary:
3083            return True
3084        binary = os.path.basename(binary).lower()
3085        if binary.endswith(".exe"):
3086            binary = binary[:-4]
3087        return (binary in self.binaries)
3088
3089    def __init__(self, binary=None):
3090        if self.needs_tempfile and not(TempFileName):
3091            raise RendererUnavailable("temporary file creation required, but not available")
3092
3093        # search for a working binary and run it to get a list of its options
3094        self.command = None
3095        for program_spec in (x.split() for x in ([binary] if binary else self.binaries)):
3096            test_binary = FindBinary(program_spec[0])
3097            try:
3098                p = Popen([test_binary] + program_spec[1:] + self.test_run_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
3099                data = p.stdout.read().decode()
3100                p.wait()
3101            except OSError:
3102                continue
3103            self.command = [test_binary] + program_spec[1:]
3104            break
3105        if not self.command:
3106            raise RendererUnavailable("program not found")
3107
3108        # parse the output into an option list
3109        data = [line.strip().replace('\t', ' ') for line in data.split('\n')]
3110        self.options = set([line.split(' ', 1)[0].split('=', 1)[0].strip('-,') for line in data if line.startswith('-')])
3111        if not(set(self.required_options) <= self.options):
3112            raise RendererUnavailable("%s does not support all required options" % os.path.basename(self.command[0]))
3113
3114    def render(self, filename, page, res, antialias=True):
3115        raise RenderError()
3116
3117    def execute(self, args, wait=True, redirect=False):
3118        args = self.command + args
3119        if get_thread_id() == RTrunning:
3120            args = Nice + args
3121        try:
3122            if redirect:
3123                process = Popen(args, stdout=subprocess.PIPE)
3124            else:
3125                process = Popen(args)
3126            if not wait:
3127                return process
3128            if process.wait() != 0:
3129                raise RenderError("rendering failed")
3130        except OSError as e:
3131            raise RenderError("could not start renderer - %s" % e)
3132
3133    def load(self, imgfile, autoremove=False):
3134        try:
3135            img = Image.open(imgfile)
3136            img.load()
3137        except (KeyboardInterrupt, SystemExit):
3138            raise
3139        except IOError as e:
3140            raise RenderError("could not read image file - %s" % e)
3141        if autoremove:
3142            self.remove(imgfile)
3143        return img
3144
3145    def remove(self, tmpfile):
3146        try:
3147            os.unlink(tmpfile)
3148        except OSError:
3149            pass
3150
3151
3152class MuPDFRenderer(PDFRendererBase):
3153    name = "MuPDF 1.4 or newer"
3154    binaries = ["mudraw", "mutool draw"]
3155    test_run_args = []
3156    required_options = ["F", "c", "o", "r"]
3157    needs_tempfile = (os.name == 'nt')
3158
3159    def render(self, filename, page, res, antialias=True):
3160        # direct stdout pipe from mutool on Unix; not possible on Win32
3161        # because mutool does LF->CRLF mangling on the image data
3162        pipe = (os.name != 'nt')
3163        imgfile = "-" if pipe else (TempFileName + ".ppm")
3164        if ("A" in self.options) and not(antialias):
3165            aa_opts = ["-A", "0"]
3166        else:
3167            aa_opts = []
3168        proc = self.execute(
3169            ["-F", "pnm", "-c", "rgb", "-o", imgfile, "-r", str(res[0])] \
3170            + aa_opts + [filename, str(page)],
3171            wait=not(pipe), redirect=pipe)
3172        if pipe:
3173            try:
3174                out, err = proc.communicate()
3175            except EnvironmentError as e:
3176                raise RenderError("could not run renderer - %s" % e)
3177            if not out:
3178                raise RenderError("renderer returned empty image")
3179            return self.load(io.BytesIO(out))
3180        else:
3181            return self.load(imgfile, autoremove=True)
3182AvailableRenderers.append(MuPDFRenderer)
3183
3184
3185class MuPDFLegacyRenderer(PDFRendererBase):
3186    name = "MuPDF (legacy)"
3187    binaries = ["mudraw", "pdfdraw"]
3188    test_run_args = []
3189    required_options = ["o", "r"]
3190
3191    # helper object for communication with the reader thread
3192    class ThreadComm(object):
3193        def __init__(self, imgfile):
3194            self.imgfile = imgfile
3195            self.buffer = None
3196            self.error = None
3197            self.cancel = False
3198
3199        def getbuffer(self):
3200            if self.buffer:
3201                return self.buffer
3202            # the reader thread might still be busy reading the last
3203            # chunks of the data and converting them into a BytesIO;
3204            # let's give it some time
3205            maxwait = time.time() + (0.1 if self.error else 0.5)
3206            while not(self.buffer) and (time.time() < maxwait):
3207                time.sleep(0.01)
3208            return self.buffer
3209
3210    @staticmethod
3211    def ReaderThread(comm):
3212        try:
3213            f = open(comm.imgfile, 'rb')
3214            comm.buffer = io.BytesIO(f.read())
3215            f.close()
3216        except IOError as e:
3217            comm.error = "could not open FIFO for reading - %s" % e
3218
3219    def render(self, filename, page, res, antialias=True):
3220        imgfile = TempFileName + ".ppm"
3221        fifo = False
3222        if HaveThreads:
3223            self.remove(imgfile)
3224            try:
3225                os.mkfifo(imgfile)
3226                fifo = True
3227                comm = self.ThreadComm(imgfile)
3228                thread.start_new_thread(self.ReaderThread, (comm, ))
3229            except (OSError, IOError, AttributeError):
3230                pass
3231        if ("b" in self.options) and not(antialias):
3232            aa_opts = ["-b", "0"]
3233        else:
3234            aa_opts = []
3235        try:
3236            self.execute([
3237                "-o", imgfile,
3238                "-r", str(res[0]),
3239                ] + aa_opts + [
3240                filename,
3241                str(page)
3242            ])
3243            if fifo:
3244                if comm.error:
3245                    raise RenderError(comm.error)
3246                if not comm.getbuffer():
3247                    raise RenderError("could not read from FIFO")
3248                return self.load(comm.buffer, autoremove=False)
3249            else:
3250                return self.load(imgfile)
3251        finally:
3252            if fifo:
3253                comm.error = True
3254                if not comm.getbuffer():
3255                    # if rendering failed and the client process didn't write
3256                    # to the FIFO at all, the reader thread would block in
3257                    # read() forever; so let's open+close the FIFO to
3258                    # generate an EOF and thus wake the thead up
3259                    try:
3260                        f = open(imgfile, "w")
3261                        f.close()
3262                    except IOError:
3263                        pass
3264            self.remove(imgfile)
3265AvailableRenderers.append(MuPDFLegacyRenderer)
3266
3267
3268class XpdfRenderer(PDFRendererBase):
3269    name = "Xpdf/Poppler"
3270    binaries = ["pdftoppm"]
3271    test_run_args = ["-h"]
3272    required_options = ["q", "f", "l", "r"]
3273
3274    def __init__(self, binary=None):
3275        PDFRendererBase.__init__(self, binary)
3276        self.supports_anamorphic = ('rx' in self.options) and ('ry' in self.options)
3277
3278    def render(self, filename, page, res, antialias=True):
3279        if self.supports_anamorphic:
3280            args = ["-rx", str(res[0]), "-ry", str(res[1])]
3281        else:
3282            args = ["-r", str(res[0])]
3283        if not antialias:
3284            for arg in ("aa", "aaVector"):
3285                if arg in self.options:
3286                    args += ['-'+arg, 'no']
3287        self.execute([
3288            "-q",
3289            "-f", str(page),
3290            "-l", str(page)
3291            ] + args + [
3292            filename,
3293            TempFileName
3294        ])
3295        digits = GetFileProp(filename, 'digits', 6)
3296        try_digits = list(range(6, 0, -1))
3297        try_digits.sort(key=lambda n: abs(n - digits))
3298        try_digits = [(n, TempFileName + ("-%%0%dd.ppm" % n) % page) for n in try_digits]
3299        for digits, imgfile in try_digits:
3300            if not os.path.exists(imgfile):
3301                continue
3302            SetFileProp(filename, 'digits', digits)
3303            return self.load(imgfile, autoremove=True)
3304        raise RenderError("could not find generated image file")
3305AvailableRenderers.append(XpdfRenderer)
3306
3307class GhostScriptRenderer(PDFRendererBase):
3308    name = "GhostScript"
3309    binaries = ["gs", "gswin32c"]
3310    test_run_args = ["--version"]
3311    supports_anamorphic = True
3312
3313    def render(self, filename, page, res, antialias=True):
3314        imgfile = TempFileName + ".tif"
3315        aa_bits = (4 if antialias else 1)
3316        try:
3317            self.execute(["-q"] + GhostScriptPlatformOptions + [
3318                "-dBATCH", "-dNOPAUSE",
3319                "-sDEVICE=tiff24nc",
3320                "-dUseCropBox",
3321                "-sOutputFile=" + imgfile,
3322                "-dFirstPage=%d" % page,
3323                "-dLastPage=%d" % page,
3324                "-r%dx%d" % res,
3325                "-dTextAlphaBits=%d" % aa_bits,
3326                "-dGraphicsAlphaBits=%s" % aa_bits,
3327                filename
3328            ])
3329            return self.load(imgfile)
3330        finally:
3331            self.remove(imgfile)
3332AvailableRenderers.append(GhostScriptRenderer)
3333
3334def InitPDFRenderer():
3335    global PDFRenderer
3336    if PDFRenderer:
3337        return PDFRenderer
3338    fail_reasons = []
3339    for r_class in AvailableRenderers:
3340        if not r_class.supports(PDFRendererPath):
3341            continue
3342        try:
3343            PDFRenderer = r_class(PDFRendererPath)
3344            print("PDF renderer:", PDFRenderer.name, file=sys.stderr)
3345            return PDFRenderer
3346        except RendererUnavailable as e:
3347            if Verbose:
3348                print("Not using %s for PDF rendering:" % r_class.name, e, file=sys.stderr)
3349            else:
3350                fail_reasons.append((r_class.name, str(e)))
3351    print("ERROR: PDF renderer initialization failed.", file=sys.stderr)
3352    for item in fail_reasons:
3353        print("       - %s: %s" % item, file=sys.stderr)
3354    print("       Display of PDF files will not be supported.", file=sys.stderr)
3355
3356
3357def ApplyRotation(img, rot):
3358    rot = (rot or 0) & 3
3359    if not rot: return img
3360    return img.transpose({1:Image.ROTATE_270, 2:Image.ROTATE_180, 3:Image.ROTATE_90}[rot])
3361
3362# generate a dummy image
3363def DummyPage():
3364    img = Image.new('RGB', (ScreenWidth, ScreenHeight))
3365    img.paste(LogoImage, ((ScreenWidth  - LogoImage.size[0]) // 2,
3366                          (ScreenHeight - LogoImage.size[1]) // 2))
3367    return img
3368
3369# load a page from a PDF file
3370def RenderPDF(page, MayAdjustResolution, ZoomMode):
3371    if not PDFRenderer:
3372        return DummyPage()
3373
3374    # load props
3375    SourceFile = GetPageProp(page, '_file')
3376    RealPage = GetPageProp(page, '_page')
3377    OutputSizes = GetPageProp(page, '_out')
3378    if not OutputSizes:
3379        OutputSizes = GetFileProp(SourceFile, 'out', [(ScreenWidth + Overscan, ScreenHeight + Overscan), (ScreenWidth + Overscan, ScreenHeight + Overscan)])
3380        SetPageProp(page, '_out', OutputSizes)
3381    Resolutions = GetPageProp(page, '_res')
3382    if not Resolutions:
3383        Resolutions = GetFileProp(SourceFile, 'res', [(72.0, 72.0), (72.0, 72.0)])
3384        SetPageProp(page, '_res', Resolutions)
3385    rot = GetPageProp(page, 'rotate', Rotation)
3386    out = OutputSizes[rot & 1]
3387    res = Resolutions[rot & 1]
3388    zscale = 1
3389
3390    # handle supersample and zoom mode
3391    use_aa = True
3392    if ZoomMode:
3393        res = (int(ResZoomFactor * res[0]), int(ResZoomFactor * res[1]))
3394        out = (int(ResZoomFactor * out[0]), int(ResZoomFactor * out[1]))
3395        zscale = ResZoomFactor
3396    elif Supersample:
3397        res = (Supersample * res[0], Supersample * res[1])
3398        out = (Supersample * out[0], Supersample * out[1])
3399        use_aa = False
3400
3401    # prepare the renderer options
3402    if PDFRenderer.supports_anamorphic:
3403        parscale = False
3404        useres = (int(res[0] + 0.5), int(res[1] + 0.5))
3405    else:
3406        parscale = (abs(1.0 - PAR) > 0.01)
3407        useres = max(res[0], res[1])
3408        res = (useres, useres)
3409        useres = int(useres + 0.5)
3410        useres = (useres, useres)
3411
3412    # call the renderer
3413    try:
3414        img = PDFRenderer.render(SourceFile, RealPage, useres, use_aa)
3415    except RenderError as e:
3416        print("ERROR: failed to render page %d:" % page, e, file=sys.stderr)
3417        return DummyPage()
3418
3419    # apply rotation
3420    img = ApplyRotation(img, rot)
3421
3422    # compute final output image size based on PAR
3423    if not parscale:
3424        got = img.size
3425    elif PAR > 1.0:
3426        got = (int(img.size[0] / PAR + 0.5), img.size[1])
3427    else:
3428        got = (img.size[0], int(img.size[1] * PAR + 0.5))
3429
3430    # if the image size is strange, re-adjust the rendering resolution
3431    tolerance = max(4, (ScreenWidth + ScreenHeight) / 400)
3432    if MayAdjustResolution and (max(abs(got[0] - out[0]), abs(got[1] - out[1])) >= tolerance):
3433        newout = ZoomToFit((img.size[0], img.size[1] * PAR), force_int=True)
3434        rscale = (float(newout[0]) / img.size[0], float(newout[1]) / img.size[1])
3435        if rot & 1:
3436            newres = (res[0] * rscale[1], res[1] * rscale[0])
3437        else:
3438            newres = (res[0] * rscale[0], res[1] * rscale[1])
3439        # only modify anything if the resolution deviation is large enough
3440        if max(abs(1.0 - newres[0] / res[0]), abs(1.0 - newres[1] / res[1])) > 0.05:
3441            # create a copy of the old values: they are lists and thus stored
3442            # in the PageProps as references; we don't want to influence other
3443            # pages though
3444            OutputSizes = OutputSizes[:]
3445            Resolutions = Resolutions[:]
3446            # modify the appropriate rotation slot
3447            OutputSizes[rot & 1] = newout
3448            Resolutions[rot & 1] = newres
3449            # store the new values for this page ...
3450            SetPageProp(page, '_out', OutputSizes)
3451            SetPageProp(page, '_res', Resolutions)
3452            # ... and as a default for the file as well (future pages are likely
3453            # to have the same resolution)
3454            SetFileProp(SourceFile, 'out', OutputSizes)
3455            SetFileProp(SourceFile, 'res', Resolutions)
3456            return RenderPDF(page, False, ZoomMode)
3457
3458    # downsample a supersampled image
3459    if Supersample and not(ZoomMode):
3460        img = img.resize((int(float(out[0]) / Supersample + 0.5),
3461                          int(float(out[1]) / Supersample + 0.5)), Image.ANTIALIAS)
3462        parscale = False  # don't scale again
3463
3464    # perform PAR scaling (required for pdftoppm which doesn't support different
3465    # dpi for horizontal and vertical)
3466    if parscale:
3467        if PAR > 1.0:
3468            img = img.resize((int(img.size[0] / PAR + 0.5), img.size[1]), Image.ANTIALIAS)
3469        else:
3470            img = img.resize((img.size[0], int(img.size[1] * PAR + 0.5)), Image.ANTIALIAS)
3471
3472    # crop the overscan (if present)
3473    if Overscan:
3474        target = (ScreenWidth * zscale, ScreenHeight * zscale)
3475        scale = None
3476        if (img.size[1] > target[1]) and (img.size[0] < target[0]):
3477            scale = float(target[1]) / img.size[1]
3478        elif (img.size[0] > target[0]) and (img.size[1] < target[1]):
3479            scale = float(target[0]) / img.size[0]
3480        if scale:
3481            w = int(img.size[0] * scale + 0.5)
3482            h = int(img.size[1] * scale + 0.5)
3483            if (w <= img.size[0]) and (h <= img.size[1]):
3484                x0 = (img.size[0] - w) // 2
3485                y0 = (img.size[1] - h) // 2
3486                img = img.crop((x0, y0, x0 + w, y0 + h))
3487
3488    return img
3489
3490
3491# load a page from an image file
3492def LoadImage(page, zoom=False, img=None):
3493    # open the image file with PIL (if not already done so)
3494    if not img:
3495        try:
3496            img = Image.open(GetPageProp(page, '_file'))
3497            img.load()
3498        except (KeyboardInterrupt, SystemExit):
3499            raise
3500        except:
3501            print("Image file `%s' is broken." % GetPageProp(page, '_file'), file=sys.stderr)
3502            return DummyPage()
3503
3504    # apply rotation
3505    img = ApplyRotation(img, GetPageProp(page, 'rotate', Rotation))
3506
3507    # determine destination size
3508    newsize = ZoomToFit((img.size[0], int(img.size[1] * PAR + 0.5)),
3509                        (ScreenWidth, ScreenHeight), force_int=True)
3510    # don't scale if the source size is too close to the destination size
3511    if abs(newsize[0] - img.size[0]) < 2: newsize = img.size
3512    # don't scale if the source is smaller than the destination
3513    if not(Scaling) and (newsize > img.size): newsize = img.size
3514    # zoom up (if wanted)
3515    if zoom: newsize = (int(ResZoomFactor * newsize[0]), int(ResZoomFactor * newsize[1]))
3516    # skip processing if there was no change
3517    if newsize == img.size: return img
3518
3519    # select a nice filter and resize the image
3520    if newsize > img.size:
3521        filter = Image.BICUBIC
3522    else:
3523        filter = Image.ANTIALIAS
3524    return img.resize(newsize, filter)
3525
3526
3527# load a preview image from a video file
3528def LoadVideoPreview(page, zoom):
3529    global ffmpegWorks, mplayerWorks
3530    img = None
3531    reason = "no working preview generator application available"
3532
3533    if not(img) and ffmpegWorks:
3534        try:
3535            ffmpegWorks = False
3536            reason = "failed to call FFmpeg"
3537            out, dummy = Popen([ffmpegPath,
3538                            "-loglevel", "fatal",
3539                            "-i", GetPageProp(page, '_file'),
3540                            "-vframes", "1", "-pix_fmt", "rgb24",
3541                            "-f", "image2pipe", "-vcodec", "ppm", "-"],
3542                            stdout=subprocess.PIPE).communicate()
3543            ffmpegWorks = True
3544            reason = "FFmpeg output is not valid"
3545            out = io.BytesIO(out)
3546            img = Image.open(out)
3547            img.load()
3548        except (KeyboardInterrupt, SystemExit):
3549            raise
3550        except EnvironmentError:
3551            img = None
3552
3553    if not(img) and mplayerWorks and not(Bare):
3554        cwd = os.getcwd()
3555        try:
3556            try:
3557                mplayerWorks = False
3558                reason = "failed to change into temporary directory"
3559                if TempFileName:
3560                    os.chdir(os.path.dirname(TempFileName))
3561                reason = "failed to call MPlayer"
3562                dummy = Popen([MPlayerPath,
3563                            "-really-quiet", "-nosound",
3564                            "-frames", "1", "-vo", "png",
3565                            GetPageProp(page, '_file')],
3566                            stdin=subprocess.PIPE).communicate()
3567                mplayerWorks = True
3568                reason = "MPlayer output is not valid"
3569                img = Image.open("00000001.png")
3570                img.load()
3571            except (KeyboardInterrupt, SystemExit):
3572                raise
3573            except EnvironmentError:
3574                img = None
3575        finally:
3576            os.chdir(cwd)
3577
3578    if img:
3579        return LoadImage(page, zoom, img)
3580    else:
3581        print("Can not generate preview image for video file `%s' (%s)." % (GetPageProp(page, '_file'), reason), file=sys.stderr)
3582        return DummyPage()
3583ffmpegWorks = True
3584mplayerWorks = True
3585
3586# render a page to an OpenGL texture
3587def PageImage(page, ZoomMode=False, RenderMode=False):
3588    global OverviewNeedUpdate, HighQualityOverview
3589    EnableCacheRead = not(ZoomMode or RenderMode)
3590    EnableCacheWrite = EnableCacheRead and \
3591                       (page >= PageRangeStart) and (page <= PageRangeEnd)
3592
3593    # check for the image in the cache
3594    if EnableCacheRead:
3595        data = GetCacheImage(page)
3596        if data: return data
3597
3598    # if it's not in the temporary cache, render it
3599    Lrender.acquire()
3600    try:
3601        # check the cache again, because another thread might have just
3602        # rendered the page while we were waiting for the render lock
3603        if EnableCacheRead:
3604            data = GetCacheImage(page)
3605            if data: return data
3606
3607        # retrieve the image from the persistent cache or fully re-render it
3608        if EnableCacheRead:
3609            img = GetPCacheImage(page)
3610        else:
3611            img = None
3612        if not img:
3613            if GetPageProp(page, '_page'):
3614                img = RenderPDF(page, not(ZoomMode), ZoomMode)
3615            elif GetPageProp(page, '_video'):
3616                img = LoadVideoPreview(page, ZoomMode)
3617            else:
3618                img = LoadImage(page, ZoomMode)
3619            if GetPageProp(page, 'invert', InvertPages):
3620                img = ImageChops.invert(img)
3621            if EnableCacheWrite:
3622                AddToPCache(page, img)
3623
3624        # create black background image to paste real image onto
3625        if ZoomMode:
3626            TextureImage = Image.new('RGB', (int(ResZoomFactor * TexWidth), int(ResZoomFactor * TexHeight)))
3627            TextureImage.paste(img, (int((ResZoomFactor * ScreenWidth  - img.size[0]) / 2),
3628                                     int((ResZoomFactor * ScreenHeight - img.size[1]) / 2)))
3629        else:
3630            TextureImage = Image.new('RGB', (TexWidth, TexHeight))
3631            x0 = (ScreenWidth  - img.size[0]) // 2
3632            y0 = (ScreenHeight - img.size[1]) // 2
3633            TextureImage.paste(img, (x0, y0))
3634            SetPageProp(page, '_box', (x0, y0, x0 + img.size[0], y0 + img.size[1]))
3635            FixHyperlinks(page)
3636
3637        # paste thumbnail into overview image
3638        if EnableOverview \
3639        and GetPageProp(page, ('overview', '_overview'), True) \
3640        and (page >= PageRangeStart) and (page <= PageRangeEnd) \
3641        and not(GetPageProp(page, '_overview_rendered')) \
3642        and not(RenderMode):
3643            pos = OverviewPos(OverviewPageMapInv[page])
3644            Loverview.acquire()
3645            try:
3646                # first, fill the underlying area with black (i.e. remove the dummy logo)
3647                blackness = Image.new('RGB', (OverviewCellX - OverviewBorder,
3648                                              OverviewCellY - OverviewBorder))
3649                OverviewImage.paste(blackness, (pos[0] + OverviewBorder // 2,
3650                                                pos[1] + OverviewBorder))
3651                del blackness
3652                # then, scale down the original image and paste it
3653                if HalfScreen:
3654                    img = img.crop((0, 0, img.size[0] // 2, img.size[1]))
3655                sx = OverviewCellX - 2 * OverviewBorder
3656                sy = OverviewCellY - 2 * OverviewBorder
3657                if HighQualityOverview:
3658                    t0 = time.time()
3659                    img.thumbnail((sx, sy), Image.ANTIALIAS)
3660                    if (time.time() - t0) > 0.5:
3661                        print("Note: Your system seems to be quite slow; falling back to a faster,", file=sys.stderr)
3662                        print("      but slightly lower-quality overview page rendering mode", file=sys.stderr)
3663                        HighQualityOverview = False
3664                else:
3665                    img.thumbnail((sx * 2, sy * 2), Image.NEAREST)
3666                    img.thumbnail((sx, sy), Image.BILINEAR)
3667                OverviewImage.paste(img,
3668                   (pos[0] + (OverviewCellX - img.size[0]) // 2,
3669                    pos[1] + (OverviewCellY - img.size[1]) // 2))
3670            finally:
3671                Loverview.release()
3672            SetPageProp(page, '_overview_rendered', True)
3673            OverviewNeedUpdate = True
3674        del img
3675
3676        # return texture data
3677        if RenderMode:
3678            return TextureImage
3679        data = img2str(TextureImage)
3680        del TextureImage
3681    finally:
3682        Lrender.release()
3683
3684    # finally add it back into the cache and return it
3685    if EnableCacheWrite:
3686        AddToCache(page, data)
3687    return data
3688
3689# render a page to an OpenGL texture
3690def RenderPage(page, target):
3691    gl.BindTexture(gl.TEXTURE_2D, target)
3692    while gl.GetError():
3693        pass  # clear all OpenGL errors
3694    gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGB, TexWidth, TexHeight, 0, gl.RGB, gl.UNSIGNED_BYTE, PageImage(page))
3695    if gl.GetError():
3696        print("I'm sorry, but your graphics card is not capable of rendering presentations", file=sys.stderr)
3697        print("in this resolution. Either the texture memory is exhausted, or there is no", file=sys.stderr)
3698        print("support for large textures (%dx%d). Please try to run Impressive in a" % (TexWidth, TexHeight), file=sys.stderr)
3699        print("smaller resolution using the -g command-line option.", file=sys.stderr)
3700        sys.exit(1)
3701
3702# background rendering thread
3703def RenderThread(p1, p2):
3704    global RTrunning, RTrestart
3705    RTrunning = get_thread_id() or True
3706    RTrestart = True
3707    while RTrestart:
3708        RTrestart = False
3709        for pdf in FileProps:
3710            if not pdf.lower().endswith(".pdf"): continue
3711            if RTrestart: break
3712            SafeCall(ParsePDF, [pdf])
3713        if RTrestart: continue
3714        for page in range(1, PageCount + 1):
3715            if RTrestart: break
3716            if (page != p1) and (page != p2) \
3717            and (page >= PageRangeStart) and (page <= PageRangeEnd):
3718                SafeCall(PageImage, [page])
3719    RTrunning = False
3720    if CacheMode >= FileCache:
3721        print("Background rendering finished, used %.1f MiB of disk space." %\
3722              (CacheFilePos / 1048576.0), file=sys.stderr)
3723    elif CacheMode >= MemCache:
3724        print("Background rendering finished, using %.1f MiB of memory." %\
3725              (sum(map(len, PageCache.values())) / 1048576.0), file=sys.stderr)
3726
3727
3728##### RENDER MODE ##############################################################
3729
3730def DoRender():
3731    global TexWidth, TexHeight
3732    TexWidth = ScreenWidth
3733    TexHeight = ScreenHeight
3734    if os.path.exists(RenderToDirectory):
3735        print("Destination directory `%s' already exists," % RenderToDirectory, file=sys.stderr)
3736        print("refusing to overwrite anything.", file=sys.stderr)
3737        return 1
3738    try:
3739        os.mkdir(RenderToDirectory)
3740    except OSError as e:
3741        print("Cannot create destination directory `%s':" % RenderToDirectory, file=sys.stderr)
3742        print(e.strerror, file=sys.stderr)
3743        return 1
3744    print("Rendering presentation into `%s'" % RenderToDirectory, file=sys.stderr)
3745    for page in range(1, PageCount + 1):
3746        PageImage(page, RenderMode=True).save("%s/page%04d.png" % (RenderToDirectory, page))
3747        sys.stdout.write("[%d] " % page)
3748        sys.stdout.flush()
3749    print(file=sys.stderr)
3750    print("Done.", file=sys.stderr)
3751    return 0
3752
3753
3754##### INFO SCRIPT I/O ##########################################################
3755
3756# info script reader
3757def LoadInfoScript():
3758    global PageProps
3759    try:
3760        os.chdir(os.path.dirname(InfoScriptPath) or BaseWorkingDir)
3761    except OSError:
3762        pass
3763    OldPageProps = PageProps
3764    try:
3765        execfile(InfoScriptPath, globals())
3766    except IOError:
3767        pass
3768    except:
3769        print("----- Exception in info script ----", file=sys.stderr)
3770        traceback.print_exc(file=sys.stderr)
3771        print("----- End of traceback -----", file=sys.stderr)
3772    NewPageProps = PageProps
3773    PageProps = OldPageProps
3774    del OldPageProps
3775    for page in NewPageProps:
3776        for prop in NewPageProps[page]:
3777            SetPageProp(page, prop, NewPageProps[page][prop])
3778    del NewPageProps
3779
3780# we can't save lambda expressions, so we need to warn the user
3781# in every possible way
3782ScriptTainted = False
3783LambdaWarning = False
3784def here_was_a_lambda_expression_that_could_not_be_saved():
3785    global LambdaWarning
3786    if not LambdaWarning:
3787        print("WARNING: The info script for the current file contained lambda expressions that", file=sys.stderr)
3788        print("         were removed during the a save operation.", file=sys.stderr)
3789        LambdaWarning = True
3790
3791# "clean" a PageProps entry so that only 'public' properties are left
3792def GetPublicProps(props):
3793    props = props.copy()
3794    # delete private (underscore) props
3795    for prop in list(props.keys()):
3796        if str(prop)[0] == '_':
3797            del props[prop]
3798    # clean props to default values
3799    if props.get('overview', False):
3800        del props['overview']
3801    if not props.get('skip', True):
3802        del props['skip']
3803    if ('boxes' in props) and not(props['boxes']):
3804        del props['boxes']
3805    return props
3806
3807# Generate a string representation of a property value. Mainly this converts
3808# classes or instances to the name of the class.
3809class dummyClass:
3810    pass
3811
3812typesClassType = type(dummyClass)
3813typesInstanceType = type(dummyClass())
3814typesFunctionType = type(GetPublicProps)
3815
3816def PropValueRepr(value):
3817    global ScriptTainted
3818    if type(value) == typesFunctionType:
3819        if value.__name__ != "<lambda>":
3820            return value.__name__
3821        if not ScriptTainted:
3822            print("WARNING: The info script contains lambda expressions, which cannot be saved", file=sys.stderr)
3823            print("         back. The modifed script will be written into a separate file to", file=sys.stderr)
3824            print("         minimize data loss.", file=sys.stderr)
3825            ScriptTainted = True
3826        return "here_was_a_lambda_expression_that_could_not_be_saved"
3827    elif isinstance(value, typesClassType):
3828        return value.__name__
3829    elif isinstance(value, typesInstanceType):
3830        return value.__class__.__name__
3831    elif type(value) == dict:
3832        return "{ " + ", ".join([PropValueRepr(k) + ": " + PropValueRepr(value[k]) for k in value]) + " }"
3833    else:
3834        return repr(value)
3835
3836# generate a nicely formatted string representation of a page's properties
3837def SinglePagePropRepr(page):
3838    props = GetPublicProps(PageProps[page])
3839    if not props: return None
3840    return "\n%3d: {%s\n     }" % (page, \
3841        ",".join(["\n       " + repr(prop) + ": " + PropValueRepr(props[prop]) for prop in props]))
3842
3843# generate a nicely formatted string representation of all page properties
3844def PagePropRepr():
3845    pages = list(PageProps.keys())
3846    pages.sort()
3847    return "PageProps = {%s\n}" % (",".join(filter(None, map(SinglePagePropRepr, pages))))
3848
3849# count the characters of a python dictionary source code, correctly handling
3850# embedded strings and comments, and nested dictionaries
3851def CountDictChars(s, start=0):
3852    context = None
3853    level = 0
3854    for i in range(start, len(s)):
3855        c = s[i]
3856        if context is None:
3857            if c == '{': level += 1
3858            if c == '}': level -= 1
3859            if c == '#': context = '#'
3860            if c == '"': context = '"'
3861            if c == "'": context = "'"
3862        elif context[0] == "\\":
3863            context=context[1]
3864        elif context == '#':
3865            if c in "\r\n": context = None
3866        elif context == '"':
3867            if c == "\\": context = "\\\""
3868            if c == '"': context = None
3869        elif context == "'":
3870            if c == "\\": context = "\\'"
3871            if c == "'": context = None
3872        if level < 0: return i
3873    raise ValueError("the dictionary never ends")
3874
3875# modify and save a file's info script
3876def SaveInfoScript(filename):
3877    # read the old info script
3878    try:
3879        f = open(filename, "r")
3880        script = f.read()
3881        f.close()
3882    except IOError:
3883        script = ""
3884    if not script:
3885        script = "# -*- coding: iso-8859-1 -*-\n"
3886
3887    # replace the PageProps of the old info script with the current ones
3888    try:
3889        m = re.search("^.*(PageProps)\s*=\s*(\{).*$", script,re.MULTILINE)
3890        if m:
3891            script = script[:m.start(1)] + PagePropRepr() + \
3892                     script[CountDictChars(script, m.end(2)) + 1 :]
3893        else:
3894            script += "\n" + PagePropRepr() + "\n"
3895    except (AttributeError, ValueError):
3896        pass
3897
3898    if ScriptTainted:
3899        filename += ".modified"
3900
3901    # write the script back
3902    try:
3903        f = open(filename, "w")
3904        f.write(script)
3905        f.close()
3906    except:
3907        print("Oops! Could not write info script!", file=sys.stderr)
3908
3909
3910##### OPENGL RENDERING #########################################################
3911
3912# draw a single progress bar
3913def DrawProgressBar(r, g, b, a, rel, y=1.0, size=ProgressBarSizeFactor):
3914    if (a <= 0.0) or (rel <= 0.0):
3915        return
3916    if HalfScreen:
3917        left, rel = 0.5, 0.5 + 0.5 * rel
3918    else:
3919        left = 0.0
3920    ProgressBarShader.get_instance().draw(
3921        left, y - size,
3922        rel,  y + size,
3923        color0=(r, g, b, 0.0),
3924        color1=(r, g, b, a)
3925    )
3926
3927# draw OSD overlays
3928def DrawOverlays(trans_time=0.0):
3929    reltime = Platform.GetTicks() - StartTime
3930    gl.Enable(gl.BLEND)
3931
3932    if (EstimatedDuration or PageProgress or (PageTimeout and AutoAdvanceProgress)) \
3933    and (OverviewMode or GetPageProp(Pcurrent, 'progress', True)):
3934        y, size = 1.0, ProgressBarSizeFactor
3935        if EstimatedDuration:
3936            rel = (0.001 * reltime) / EstimatedDuration
3937            if rel < 1.0:
3938                r, g, b = ProgressBarColorNormal
3939            elif rel < ProgressBarWarningFactor:
3940                r, g, b = lerpColor(ProgressBarColorNormal, ProgressBarColorWarning,
3941                          (rel - 1.0) / (ProgressBarWarningFactor - 1.0))
3942            elif rel < ProgressBarCriticalFactor:
3943                r, g, b = lerpColor(ProgressBarColorWarning, ProgressBarColorCritical,
3944                          (rel - ProgressBarWarningFactor) / (ProgressBarCriticalFactor - ProgressBarWarningFactor))
3945            else:
3946                r, g, b = ProgressBarColorCritical
3947            DrawProgressBar(r, g, b, ProgressBarAlpha, rel, y)
3948            y -= ProgressBarSizeFactor
3949            size *= 0.7  # if there's a stacked page-progress bar, make it smaller
3950        if PageProgress:
3951            rel = (Pcurrent + trans_time * (Pnext - Pcurrent)) / (ProgressLast or PageCount)
3952            r, g, b = ProgressBarColorPage
3953            DrawProgressBar(r, g, b, ProgressBarAlpha, rel, y, size)
3954            y -= ProgressBarSizeFactor
3955        if PageTimeout and AutoAdvanceProgress:
3956            r, g, b = ProgressBarColorPage
3957            a = ProgressBarAlpha
3958            rel = (reltime - PageEnterTime) / float(PageTimeout)
3959            if TransitionRunning:
3960                a = int(a * (1.0 - TransitionPhase))
3961            elif PageLeaveTime > PageEnterTime:
3962                # we'll be called one frame after the transition finished, but
3963                # before the new page has been fully activated => don't flash
3964                a = 0
3965            if y < 1.0:
3966                y = 0.0  # move to top if there were already bars at the bottom
3967            DrawProgressBar(r, g, b, a, rel, y)
3968
3969    if OSDFont:
3970        OSDFont.BeginDraw()
3971        if WantStatus:
3972            DrawOSDEx(OSDStatusPos, CurrentOSDStatus)
3973        if TimeDisplay:
3974            if ShowClock:
3975                DrawOSDEx(OSDTimePos, ClockTime(MinutesOnly))
3976            else:
3977                t = reltime // 1000
3978                DrawOSDEx(OSDTimePos, FormatTime(t, MinutesOnly))
3979        if CurrentOSDComment and (OverviewMode or not(TransitionRunning)):
3980            DrawOSD(ScreenWidth // 2,
3981                    ScreenHeight - 3*OSDMargin - FontSize,
3982                    CurrentOSDComment, Center, Up)
3983        OSDFont.EndDraw()
3984
3985    if EnableCursor and CursorVisible and CursorImage:
3986        x, y = Platform.GetMousePos()
3987        x -= CursorHotspot[0]
3988        y -= CursorHotspot[1]
3989        X0 = x * PixelX
3990        Y0 = y * PixelY
3991        X1 = X0 + CursorSX
3992        Y1 = Y0 + CursorSY
3993        TexturedRectShader.get_instance().draw(
3994            X0, Y0, X1, Y1,
3995            s1=CursorTX, t1=CursorTY,
3996            tex=CursorTexture
3997        )
3998
3999    gl.Disable(gl.BLEND)
4000
4001
4002# draw the complete image of the current page
4003def DrawCurrentPage(dark=1.0, do_flip=True):
4004    global ScreenTransform
4005    if VideoPlaying: return
4006    boxes = GetPageProp(Pcurrent, 'boxes')
4007    if BoxZoom: boxes = [BoxZoom]
4008    gl.Clear(gl.COLOR_BUFFER_BIT)
4009
4010    # pre-transform for zoom
4011    if ZoomArea != 1.0:
4012        ScreenTransform = (
4013            -2.0 * ZoomX0 / ZoomArea - 1.0,
4014            +2.0 * ZoomY0 / ZoomArea + 1.0,
4015            +2.0 / ZoomArea,
4016            -2.0 / ZoomArea
4017        )
4018
4019    # background layer -- the page's image, darkened if it has boxes
4020    # note: some code paths enable GL_BLEND here; it stays enabled
4021    #       during the rest of this function and will be disabled
4022    #       at the end of DrawOverlays()
4023    is_dark = (boxes or Tracing) and (dark > 0.001)
4024    if not(is_dark) or BoxZoom:
4025        # standard mode
4026        if BoxZoom:
4027            i = 1.0 - BoxZoomDarkness * dark
4028        else:
4029            i = 1.0
4030        TexturedRectShader.get_instance().draw(
4031            0.0, 0.0, 1.0, 1.0,
4032            s1=TexMaxS, t1=TexMaxT,
4033            tex=Tcurrent,
4034            color=(i,i,i,1.0)
4035        )
4036        if BoxZoom and is_dark:
4037            gl.Enable(gl.BLEND)
4038    elif UseBlurShader:
4039        # blurred background (using shader)
4040        blur_scale = BoxFadeBlur * ZoomArea * dark
4041        BlurShader.get_instance().draw(
4042            PixelX * blur_scale,
4043            PixelY * blur_scale,
4044            1.0 - BoxFadeDarkness * dark,
4045            tex=Tcurrent
4046        )
4047        gl.Enable(gl.BLEND)
4048    else:
4049        # blurred background (using oldschool multi-pass blend fallback)
4050        intensity = 1.0 - BoxFadeDarkness * dark
4051        for dx, dy, alpha in (
4052            (0.0,  0.0, 1.0),
4053            (-ZoomArea, 0.0, dark / 2),
4054            (+ZoomArea, 0.0, dark / 3),
4055            (0.0, -ZoomArea, dark / 4),
4056            (0.0, +ZoomArea, dark / 5),
4057        ):
4058            TexturedRectShader.get_instance().draw(
4059                0.0, 0.0, 1.0, 1.0,
4060                TexMaxS *  PixelX * dx,
4061                TexMaxT *  PixelY * dy,
4062                TexMaxS * (PixelX * dx + 1.0),
4063                TexMaxT * (PixelY * dy + 1.0),
4064                tex=Tcurrent,
4065                color=(intensity, intensity, intensity, alpha)
4066            )
4067            gl.Enable(gl.BLEND)  # start blending from the second pass on
4068
4069
4070    if boxes and is_dark:
4071        TexturedMeshShader.get_instance().setup(
4072            0.0, 0.0, 1.0, 1.0,
4073            s1=TexMaxS, t1=TexMaxT
4074            # tex is already set
4075        )
4076        ex = (ZoomBoxEdgeSize if BoxZoom else BoxEdgeSize) * PixelX
4077        ey = (ZoomBoxEdgeSize if BoxZoom else BoxEdgeSize) * PixelY
4078        for X0, Y0, X1, Y1 in boxes:
4079            vertices = (c_float * 27)(
4080                X0, Y0, 1.0,  # note: this produces two degenerate triangles
4081                X0,      Y0,      1.0,
4082                X0 - ex, Y0 - ey, 0.0,
4083                X1,      Y0,      1.0,
4084                X1 + ex, Y0 - ey, 0.0,
4085                X1,      Y1,      1.0,
4086                X1 + ex, Y1 + ey, 0.0,
4087                X0,      Y1,      1.0,
4088                X0 - ex, Y1 + ey, 0.0,
4089            )
4090            gl.BindBuffer(gl.ARRAY_BUFFER, 0)
4091            gl.VertexAttribPointer(0, 3, gl.FLOAT, False, 0, vertices)
4092            BoxIndexBuffer.draw()
4093
4094    if Tracing and is_dark:
4095        x, y = MouseToScreen(Platform.GetMousePos())
4096        TexturedMeshShader.get_instance().setup(
4097            x, y, x + 1.0, y + 1.0,
4098            x * TexMaxS, y * TexMaxT,
4099            (x + 1.0) * TexMaxS, (y + 1.0) * TexMaxT
4100            # tex is already set
4101        )
4102        gl.BindBuffer(gl.ARRAY_BUFFER, SpotVertices)
4103        gl.VertexAttribPointer(0, 3, gl.FLOAT, False, 0, 0)
4104        SpotIndices.draw()
4105
4106    if Marking:
4107        x0 = min(MarkUL[0], MarkLR[0])
4108        y0 = min(MarkUL[1], MarkLR[1])
4109        x1 = max(MarkUL[0], MarkLR[0])
4110        y1 = max(MarkUL[1], MarkLR[1])
4111        # red frame (misusing the progress bar shader as a single-color shader)
4112        color = (MarkColor[0], MarkColor[1], MarkColor[2], 1.0)
4113        ProgressBarShader.get_instance().draw(
4114            x0 - PixelX * ZoomArea, y0 - PixelY * ZoomArea,
4115            x1 + PixelX * ZoomArea, y1 + PixelY * ZoomArea,
4116            color0=color, color1=color
4117        )
4118        # semi-transparent inner area
4119        gl.Enable(gl.BLEND)
4120        TexturedRectShader.get_instance().draw(
4121            x0, y0, x1, y1,
4122            x0 * TexMaxS, y0 * TexMaxT,
4123            x1 * TexMaxS, y1 * TexMaxT,
4124            tex=Tcurrent, color=(1.0, 1.0, 1.0, 1.0 - MarkColor[3])
4125        )
4126
4127    # unapply the zoom transform
4128    ScreenTransform = DefaultScreenTransform
4129
4130    # Done.
4131    DrawOverlays()
4132    if do_flip:
4133        Platform.SwapBuffers()
4134
4135# draw a black screen with the Impressive logo at the center
4136def DrawLogo():
4137    gl.Clear(gl.COLOR_BUFFER_BIT)
4138    if not ShowLogo:
4139        return
4140    if HalfScreen:
4141        x0 = 0.25
4142    else:
4143        x0 = 0.5
4144    TexturedRectShader.get_instance().draw(
4145        x0 - 128.0 / ScreenWidth,  0.5 - 32.0 / ScreenHeight,
4146        x0 + 128.0 / ScreenWidth,  0.5 + 32.0 / ScreenHeight,
4147        tex=LogoTexture
4148    )
4149    if OSDFont:
4150        gl.Enable(gl.BLEND)
4151        OSDFont.Draw((int(ScreenWidth * x0), ScreenHeight // 2 + 48), \
4152                     __version__.split()[0], align=Center, alpha=0.25, beveled=False)
4153        gl.Disable(gl.BLEND)
4154
4155# draw the prerender progress bar
4156def DrawProgress(position):
4157    x0 = 0.1
4158    x2 = 1.0 - x0
4159    x1 = position * x2 + (1.0 - position) * x0
4160    y1 = 0.9
4161    y0 = y1 - 16.0 / ScreenHeight
4162    if HalfScreen:
4163        x0 *= 0.5
4164        x1 *= 0.5
4165        x2 *= 0.5
4166    ProgressBarShader.get_instance().draw(
4167        x0, y0, x2, y1,
4168        color0=(0.25, 0.25, 0.25, 1.0),
4169        color1=(0.50, 0.50, 0.50, 1.0)
4170    )
4171    ProgressBarShader.get_instance().draw(
4172        x0, y0, x1, y1,
4173        color0=(0.25, 0.50, 1.00, 1.0),
4174        color1=(0.03, 0.12, 0.50, 1.0)
4175    )
4176
4177# fade mode
4178def DrawFadeMode(intensity, alpha):
4179    if VideoPlaying: return
4180    DrawCurrentPage(do_flip=False)
4181    gl.Enable(gl.BLEND)
4182    color = (intensity, intensity, intensity, alpha)
4183    ProgressBarShader.get_instance().draw(
4184        0.0, 0.0, 1.0, 1.0,
4185        color0=color, color1=color
4186    )
4187    gl.Disable(gl.BLEND)
4188    Platform.SwapBuffers()
4189
4190def EnterFadeMode(intensity=0.0):
4191    t0 = Platform.GetTicks()
4192    while True:
4193        if Platform.CheckAnimationCancelEvent(): break
4194        t = (Platform.GetTicks() - t0) * 1.0 / BlankFadeDuration
4195        if t >= 1.0: break
4196        DrawFadeMode(intensity, t)
4197    DrawFadeMode(intensity, 1.0)
4198
4199def LeaveFadeMode(intensity=0.0):
4200    t0 = Platform.GetTicks()
4201    while True:
4202        if Platform.CheckAnimationCancelEvent(): break
4203        t = (Platform.GetTicks() - t0) * 1.0 / BlankFadeDuration
4204        if t >= 1.0: break
4205        DrawFadeMode(intensity, 1.0 - t)
4206    DrawCurrentPage()
4207
4208def FadeMode(intensity):
4209    EnterFadeMode(intensity)
4210    def fade_action_handler(action):
4211        if action == "$quit":
4212            PageLeft()
4213            Quit()
4214        elif action == "$expose":
4215            DrawFadeMode(intensity, 1.0)
4216        elif action == "*quit":
4217            Platform.PostQuitEvent()
4218        else:
4219            return False
4220        return True
4221    while True:
4222        ev = Platform.GetEvent()
4223        if ev and not(ProcessEvent(ev, fade_action_handler)) and ev.startswith('*'):
4224            break
4225    LeaveFadeMode(intensity)
4226
4227# gamma control
4228def SetGamma(new_gamma=None, new_black=None, force=False):
4229    global Gamma, BlackLevel
4230    if new_gamma is None: new_gamma = Gamma
4231    if new_gamma <  0.1:  new_gamma = 0.1
4232    if new_gamma > 10.0:  new_gamma = 10.0
4233    if new_black is None: new_black = BlackLevel
4234    if new_black <   0:   new_black = 0
4235    if new_black > 254:   new_black = 254
4236    if not(force) and (abs(Gamma - new_gamma) < 0.01) and (new_black == BlackLevel):
4237        return
4238    Gamma = new_gamma
4239    BlackLevel = new_black
4240    return Platform.SetGammaRamp(new_gamma, new_black)
4241
4242# cursor image
4243def PrepareCustomCursor(cimg):
4244    global CursorTexture, CursorHotspot, CursorSX, CursorSY, CursorTX, CursorTY
4245    if not cimg:
4246        CursorHotspot = (1,0)
4247        cimg = Image.open(io.BytesIO(codecs.decode(DEFAULT_CURSOR, 'base64')))
4248    w, h = cimg.size
4249    tw, th = map(npot, cimg.size)
4250    if (tw > 256) or (th > 256):
4251        print("Custom cursor is ridiculously large, reverting to normal one.", file=sys.stderr)
4252        return False
4253    img = Image.new('RGBA', (tw, th))
4254    img.paste(cimg, (0, 0))
4255    CursorTexture = gl.make_texture(gl.TEXTURE_2D, gl.CLAMP_TO_EDGE, gl.NEAREST)
4256    gl.load_texture(gl.TEXTURE_2D, img)
4257    CursorSX = w * PixelX
4258    CursorSY = h * PixelY
4259    CursorTX = w / float(tw)
4260    CursorTY = h / float(th)
4261    return True
4262
4263
4264##### CONTROL AND NAVIGATION ###################################################
4265
4266# update the applications' title bar
4267def UpdateCaption(page=0, force=False):
4268    global CurrentCaption, CurrentOSDCaption, CurrentOSDPage, CurrentOSDStatus
4269    global CurrentOSDComment
4270    if (page == CurrentCaption) and not(force):
4271        return
4272    CurrentCaption = page
4273    caption = __title__
4274    if DocumentTitle:
4275        caption += " - " + DocumentTitle
4276    if page < 1:
4277        CurrentOSDCaption = ""
4278        CurrentOSDPage = ""
4279        CurrentOSDStatus = ""
4280        CurrentOSDComment = ""
4281        Platform.SetWindowTitle(caption)
4282        return
4283    CurrentOSDPage = "%d/%d" % (page, PageCount)
4284    caption = "%s (%s)" % (caption, CurrentOSDPage)
4285    title = GetPageProp(page, 'title') or GetPageProp(page, '_title')
4286    if title:
4287        caption += ": %s" % title
4288        CurrentOSDCaption = title
4289    else:
4290        CurrentOSDCaption = ""
4291    status = []
4292    if GetPageProp(page, 'skip', False):
4293        status.append("skipped: yes")
4294    if not GetPageProp(page, ('overview', '_overview'), True):
4295        status.append("on overview page: no")
4296    CurrentOSDStatus = ", ".join(status)
4297    CurrentOSDComment = GetPageProp(page, 'comment')
4298    Platform.SetWindowTitle(caption)
4299
4300# get next/previous page
4301def GetNextPage(page, direction):
4302    checked_pages = set()
4303    while True:
4304        checked_pages.add(page)
4305        page = GetPageProp(page,
4306            ('prev' if (direction < 0) else 'next'),
4307            page + direction)
4308        if page in checked_pages:
4309            return 0  # we looped around completely and found nothing
4310        if Wrap:
4311            if page < 1: page = PageCount
4312            if page > PageCount: page = 1
4313        else:
4314            if page < 1 or page > PageCount:
4315                return 0  # start or end of presentation
4316        if not GetPageProp(page, 'skip', False):
4317            return page
4318
4319# pre-load the following page into Pnext/Tnext
4320def PreloadNextPage(page):
4321    global Pnext, Tnext
4322    if (page < 1) or (page > PageCount):
4323        Pnext = 0
4324        return 0
4325    if page == Pnext:
4326        return 1
4327    RenderPage(page, Tnext)
4328    Pnext = page
4329    return 1
4330
4331# perform box fading; the fade animation time is mapped through func()
4332def BoxFade(func):
4333    t0 = Platform.GetTicks()
4334    while BoxFadeDuration > 0:
4335        if Platform.CheckAnimationCancelEvent(): break
4336        t = (Platform.GetTicks() - t0) * 1.0 / BoxFadeDuration
4337        if t >= 1.0: break
4338        DrawCurrentPage(func(t))
4339    DrawCurrentPage(func(1.0))
4340    return 0
4341
4342# reset the timer
4343def ResetTimer():
4344    global StartTime, PageEnterTime
4345    if TimeTracking and not(FirstPage):
4346        print("--- timer was reset here ---")
4347    StartTime = Platform.GetTicks()
4348    PageEnterTime = 0
4349
4350# start video playback
4351def PlayVideo(video):
4352    global MPlayerProcess, VideoPlaying, NextPageAfterVideo
4353    if not video: return
4354    StopMPlayer()
4355    if Platform.use_omxplayer:
4356        opts = ["omxplayer"]
4357    else:
4358        opts = [MPlayerPath, "-quiet", "-slave", \
4359                "-monitorpixelaspect", "1:1", \
4360                "-vo", "gl", \
4361                "-autosync", "100"]
4362        try:
4363            opts += ["-wid", str(Platform.GetWindowID())]
4364        except KeyError:
4365            if Fullscreen:
4366                opts.append("-fs")
4367            else:
4368                print("Sorry, but Impressive only supports video on your operating system if fullscreen", file=sys.stderr)
4369                print("mode is used.", file=sys.stderr)
4370                VideoPlaying = False
4371                MPlayerProcess = None
4372                return
4373    if not isinstance(video, list):
4374        video = [video]
4375    NextPageAfterVideo = False
4376    try:
4377        MPlayerProcess = Popen(opts + video, stdin=subprocess.PIPE)
4378        if Platform.use_omxplayer:
4379            gl.Clear(gl.COLOR_BUFFER_BIT)
4380            Platform.SwapBuffers()
4381        if Fullscreen and (os.name == 'nt'):
4382            # very ugly Win32-specific hack: in -wid embedding mode,
4383            # video display only works if we briefly minimize and restore
4384            # the window ... and that's the good case: in -fs, keyboard
4385            # focus is messed up and we don't get any input!
4386            if Win32FullscreenVideoHackTiming[0] > 0:
4387                time.sleep(Win32FullscreenVideoHackTiming[0])
4388            win32gui.ShowWindow(Platform.GetWindowID(), 6)  # SW_MINIMIZE
4389            if Win32FullscreenVideoHackTiming[1] > 0:
4390                time.sleep(Win32FullscreenVideoHackTiming[1])
4391            win32gui.ShowWindow(Platform.GetWindowID(), 9)  # SW_RESTORE
4392        VideoPlaying = True
4393    except OSError:
4394        MPlayerProcess = None
4395
4396# called each time a page is entered, AFTER the transition, BEFORE entering box-fade mode
4397def PreparePage():
4398    global SpotRadius, SpotRadiusBase
4399    global BoxFadeDarkness, BoxFadeDarknessBase
4400    global BoxZoomDarkness, BoxZoomDarknessBase
4401    override = GetPageProp(Pcurrent, 'radius')
4402    if override:
4403        SpotRadius = override
4404        SpotRadiusBase = override
4405        GenerateSpotMesh()
4406    override = GetPageProp(Pcurrent, 'darkness')
4407    if override is not None:
4408        BoxFadeDarkness = override * 0.01
4409        BoxFadeDarknessBase = override * 0.01
4410    override = GetPageProp(Pcurrent, 'zoomdarkness')
4411    if override is not None:
4412        BoxZoomDarkness = override * 0.01
4413        BoxZoomDarknessBase = override * 0.01
4414
4415# called each time a page is entered, AFTER the transition, AFTER entering box-fade mode
4416def PageEntered(update_time=True):
4417    global PageEnterTime, PageTimeout, MPlayerProcess, IsZoomed, WantStatus
4418    if update_time:
4419        PageEnterTime = Platform.GetTicks() - StartTime
4420    IsZoomed = 0  # no, we don't have a pre-zoomed image right now
4421    WantStatus = False  # don't show status unless it's changed interactively
4422    PageTimeout = AutoAdvanceTime if AutoAdvanceEnabled else 0
4423    shown = GetPageProp(Pcurrent, '_shown', 0)
4424    try:
4425        os.chdir(os.path.dirname(GetPageProp(Pcurrent, '_file')))
4426    except OSError:
4427        pass
4428    if not(shown) or Wrap:
4429        PageTimeout = GetPageProp(Pcurrent, 'timeout', PageTimeout)
4430    if GetPageProp(Pcurrent, '_video'):
4431        PlayVideo(GetPageProp(Pcurrent, '_file'))
4432    if not(shown) or GetPageProp(Pcurrent, 'always', False):
4433        if not GetPageProp(Pcurrent, '_video'):
4434            video = GetPageProp(Pcurrent, 'video')
4435            sound = GetPageProp(Pcurrent, 'sound')
4436            PlayVideo(video)
4437            if sound and not(video):
4438                StopMPlayer()
4439                try:
4440                    MPlayerProcess = Popen(
4441                        [MPlayerPath, "-quiet", "-really-quiet", "-novideo", sound],
4442                        stdin=subprocess.PIPE)
4443                except OSError:
4444                    MPlayerProcess = None
4445        SafeCall(GetPageProp(Pcurrent, 'OnEnterOnce'))
4446    SafeCall(GetPageProp(Pcurrent, 'OnEnter'))
4447    if PageTimeout:
4448        Platform.ScheduleEvent("$page-timeout", PageTimeout)
4449    SetPageProp(Pcurrent, '_shown', shown + 1)
4450
4451# called each time a page is left
4452def PageLeft(overview=False):
4453    global FirstPage, LastPage, WantStatus, PageLeaveTime
4454    PageLeaveTime = Platform.GetTicks() - StartTime
4455    WantStatus = False
4456    if not overview:
4457        if GetTristatePageProp(Pcurrent, 'reset'):
4458            ResetTimer()
4459        FirstPage = False
4460        LastPage = Pcurrent
4461        if GetPageProp(Pcurrent, '_shown', 0) == 1:
4462            SafeCall(GetPageProp(Pcurrent, 'OnLeaveOnce'))
4463        SafeCall(GetPageProp(Pcurrent, 'OnLeave'))
4464    if TimeTracking:
4465        t1 = Platform.GetTicks() - StartTime
4466        dt = (t1 - PageEnterTime + 500) // 1000
4467        if overview:
4468            p = "over"
4469        else:
4470            p = "%4d" % Pcurrent
4471        print("%s%9s%9s%9s" % (p, FormatTime(dt),
4472                                  FormatTime(PageEnterTime // 1000),
4473                                  FormatTime(t1 // 1000)))
4474
4475# create an instance of a transition class
4476def InstantiateTransition(trans_class):
4477    try:
4478        return trans_class()
4479    except GLInvalidShaderError:
4480        return None
4481    except GLShaderCompileError:
4482        print("Note: all %s transitions will be disabled" % trans_class.__name__, file=sys.stderr)
4483        return None
4484
4485# perform a transition to a specified page
4486def TransitionTo(page, allow_transition=True, notify_page_left=True):
4487    global Pcurrent, Pnext, Tcurrent, Tnext
4488    global PageCount, Marking, Tracing, Panning
4489    global TransitionRunning, TransitionPhase
4490    global TransitionDone
4491    TransitionDone = False
4492
4493    # first, stop video and kill the auto-timer
4494    if VideoPlaying:
4495        StopMPlayer()
4496    Platform.ScheduleEvent("$page-timeout", 0)
4497
4498    # invalid page? go away
4499    if not PreloadNextPage(page):
4500        if QuitAtEnd:
4501            LeaveZoomMode(allow_transition)
4502            if FadeInOut:
4503                EnterFadeMode()
4504            PageLeft()
4505            Quit()
4506        return 0
4507
4508    # leave zoom mode now, if enabled
4509    LeaveZoomMode(allow_transition)
4510
4511    # notify that the page has been left
4512    if notify_page_left:
4513        PageLeft()
4514    if TransitionDone:
4515        return 1  # nested call to TransitionTo() detected -> abort here
4516
4517    # box fade-out
4518    if GetPageProp(Pcurrent, 'boxes') or Tracing:
4519        skip = BoxFade(lambda t: 1.0 - t)
4520    else:
4521        skip = 0
4522
4523    # some housekeeping
4524    Marking = False
4525    Tracing = False
4526    UpdateCaption(page)
4527
4528    # check if the transition is valid
4529    tpage = max(Pcurrent, Pnext)
4530    trans = None
4531    if allow_transition:
4532        trans = GetPageProp(tpage, 'transition', GetPageProp(tpage, '_transition'))
4533    else:
4534        trans = None
4535    if trans is not None:
4536        transtime = GetPageProp(tpage, 'transtime', TransitionDuration)
4537        try:
4538            dummy = trans.__class__
4539        except AttributeError:
4540            # ah, gotcha! the transition is not yet instantiated!
4541            trans = InstantiateTransition(trans)
4542            PageProps[tpage][tkey] = trans
4543    if trans is None:
4544        transtime = 0
4545
4546    # backward motion? then swap page buffers now
4547    backward = (Pnext < Pcurrent)
4548    if Wrap and (min(Pcurrent, Pnext) == 1) and (max(Pcurrent, Pnext) == PageCount):
4549        backward = not(backward)  # special case: last<->first in wrap mode
4550    if backward:
4551        Pcurrent, Pnext = (Pnext, Pcurrent)
4552        Tcurrent, Tnext = (Tnext, Tcurrent)
4553
4554    # transition animation
4555    if not(skip) and transtime:
4556        transtime = 1.0 / transtime
4557        TransitionRunning = True
4558        trans.start()
4559        t0 = Platform.GetTicks()
4560        while not(VideoPlaying):
4561            if Platform.CheckAnimationCancelEvent():
4562                skip = 1
4563                break
4564            t = (Platform.GetTicks() - t0) * transtime
4565            if t >= 1.0: break
4566            TransitionPhase = t
4567            if backward: t = 1.0 - t
4568            gl.Clear(gl.COLOR_BUFFER_BIT)
4569            trans.render(t)
4570            DrawOverlays(t)
4571            Platform.SwapBuffers()
4572        TransitionRunning = False
4573
4574    # forward motion => swap page buffers now
4575    if not backward:
4576        Pcurrent, Pnext = (Pnext, Pcurrent)
4577        Tcurrent, Tnext = (Tnext, Tcurrent)
4578
4579    # prepare the page's changeable metadata
4580    PreparePage()
4581
4582    # box fade-in
4583    if not(skip) and GetPageProp(Pcurrent, 'boxes'): BoxFade(lambda t: t)
4584
4585    # finally update the screen and preload the next page
4586    DrawCurrentPage()
4587    PageEntered()
4588    if TransitionDone:
4589        return 1
4590    if not PreloadNextPage(GetNextPage(Pcurrent, 1)):
4591        PreloadNextPage(GetNextPage(Pcurrent, -1))
4592    TransitionDone = True
4593    return 1
4594
4595# zoom mode animation
4596def ZoomAnimation(targetx, targety, func, duration_override=None):
4597    global ZoomX0, ZoomY0, ZoomArea
4598    t0 = Platform.GetTicks()
4599    if duration_override is None:
4600        duration = ZoomDuration
4601    else:
4602        duration = duration_override
4603    while duration > 0:
4604        if Platform.CheckAnimationCancelEvent(): break
4605        t = (Platform.GetTicks() - t0) * 1.0 / duration
4606        if t >= 1.0: break
4607        t = func(t)
4608        dark = (t if BoxZoom else 1.0)
4609        t = (2.0 - t) * t
4610        ZoomX0 = targetx * t
4611        ZoomY0 = targety * t
4612        ZoomArea = 1.0 - (1.0 - 1.0 / ViewZoomFactor) * t
4613        DrawCurrentPage(dark=dark)
4614    t = func(1.0)
4615    ZoomX0 = targetx * t
4616    ZoomY0 = targety * t
4617    ZoomArea = 1.0 - (1.0 - 1.0 / ViewZoomFactor) * t
4618    GenerateSpotMesh()
4619    DrawCurrentPage(dark=(t if BoxZoom else 1.0))
4620
4621# re-render zoomed page image
4622def ReRenderZoom(factor):
4623    global ResZoomFactor, IsZoomed, HighResZoomFailed
4624    ResZoomFactor = min(factor, MaxZoomFactor)
4625    if (IsZoomed >= ResZoomFactor) or (ResZoomFactor < 1.1) or HighResZoomFailed:
4626        return
4627    gl.BindTexture(gl.TEXTURE_2D, Tcurrent)
4628    while gl.GetError():
4629        pass  # clear all OpenGL errors
4630    gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGB, int(ResZoomFactor * TexWidth), int(ResZoomFactor * TexHeight), 0, gl.RGB, gl.UNSIGNED_BYTE, PageImage(Pcurrent, True))
4631    if gl.GetError():
4632        print("I'm sorry, but your graphics card is not capable of rendering presentations", file=sys.stderr)
4633        print("in this resolution. Either the texture memory is exhausted, or there is no", file=sys.stderr)
4634        print("support for large textures (%dx%d). Please try to run Impressive in a" % (TexWidth, TexHeight), file=sys.stderr)
4635        print("smaller resolution using the -g command-line option.", file=sys.stderr)
4636        HighResZoomFailed = True
4637        return
4638    DrawCurrentPage()
4639    IsZoomed = ResZoomFactor
4640
4641# enter zoom mode
4642def EnterZoomMode(factor, targetx, targety):
4643    global ZoomMode, ViewZoomFactor
4644    ViewZoomFactor = factor
4645    ZoomAnimation(targetx, targety, lambda t: t)
4646    ZoomMode = True
4647    ReRenderZoom(factor)
4648
4649# leave zoom mode (if enabled)
4650def LeaveZoomMode(allow_transition=True):
4651    global ZoomMode, BoxZoom, Panning, ViewZoomFactor, ResZoomFactor
4652    global ZoomArea, ZoomX0, ZoomY0
4653    if not ZoomMode: return
4654    ZoomAnimation(ZoomX0, ZoomY0, lambda t: 1.0 - t, (None if allow_transition else 0))
4655    ZoomMode = False
4656    BoxZoom = False
4657    Panning = False
4658    ViewZoomFactor = 1
4659    ResZoomFactor = 1
4660    ZoomArea = 1.0
4661    ZoomX0 = 0.0
4662    ZoomY0 = 0.0
4663
4664# change zoom factor in zoom mode
4665def ChangeZoom(target_factor, mousepos):
4666    global ZoomMode, ViewZoomFactor, ZoomArea, ZoomX0, ZoomY0
4667    px, py = MouseToScreen(mousepos)
4668    log_zf = log(ViewZoomFactor)
4669    dlog = log(target_factor) - log_zf
4670    t0 = Platform.GetTicks()
4671    dt = -1
4672    while dt < WheelZoomDuration:
4673        dt = Platform.GetTicks() - t0
4674        rel = min(1.0, float(dt) / WheelZoomDuration) if WheelZoomDuration else 1.0
4675        factor = exp(log_zf + rel * dlog)
4676        if factor < 1.001: factor = 1.0
4677        ZoomArea = 1.0 / factor
4678        ZoomX0 = max(0.0, min(1.0 - ZoomArea, px - mousepos[0] * ZoomArea / ScreenWidth))
4679        ZoomY0 = max(0.0, min(1.0 - ZoomArea, py - mousepos[1] * ZoomArea / ScreenHeight))
4680        DrawCurrentPage()
4681    ViewZoomFactor = factor
4682    ZoomMode = (factor > 1.0)
4683
4684# check whether a box mark is too small
4685def BoxTooSmall():
4686    return ((abs(MarkUL[0] - MarkLR[0]) * ScreenWidth)  < MinBoxSize) \
4687        or ((abs(MarkUL[1] - MarkLR[1]) * ScreenHeight) < MinBoxSize)
4688
4689# increment/decrement spot radius
4690def IncrementSpotSize(delta):
4691    global SpotRadius
4692    if not Tracing:
4693        return
4694    SpotRadius = max(SpotRadius + delta, 8)
4695    GenerateSpotMesh()
4696    DrawCurrentPage()
4697
4698# post-initialize the page transitions
4699def PrepareTransitions():
4700    Unspecified = 0xAFFED00F
4701    # STEP 1: randomly assign transitions where the user didn't specify them
4702    cnt = sum([1 for page in range(1, PageCount + 1) \
4703               if GetPageProp(page, 'transition', Unspecified) == Unspecified])
4704    newtrans = ((cnt // len(AvailableTransitions) + 1) * AvailableTransitions)[:cnt]
4705    random.shuffle(newtrans)
4706    for page in range(1, PageCount + 1):
4707        if GetPageProp(page, 'transition', Unspecified) == Unspecified:
4708            SetPageProp(page, '_transition', newtrans.pop())
4709    # STEP 2: instantiate transitions
4710    for page in PageProps:
4711        for key in ('transition', '_transition'):
4712            if not key in PageProps[page]:
4713                continue
4714            trans = PageProps[page][key]
4715            if trans is not None:
4716                PageProps[page][key] = InstantiateTransition(trans)
4717
4718# update timer values and screen timer
4719def TimerTick():
4720    global CurrentTime, ProgressBarPos
4721    redraw = False
4722    newtime = (Platform.GetTicks() - StartTime) * 0.001
4723    if EstimatedDuration:
4724        newpos = int(ScreenWidth * newtime / EstimatedDuration)
4725        if newpos != ProgressBarPos:
4726            redraw = True
4727        ProgressBarPos = newpos
4728    newtime = int(newtime)
4729    if TimeDisplay and (CurrentTime != newtime):
4730        redraw = True
4731    if PageTimeout and AutoAdvanceProgress:
4732        redraw = True
4733    CurrentTime = newtime
4734    return redraw
4735
4736# enables time tracking mode (if not already done so)
4737def EnableTimeTracking(force=False):
4738    global TimeTracking
4739    if force or (TimeDisplay and not(TimeTracking) and not(ShowClock) and FirstPage):
4740        print("Time tracking mode enabled.", file=sys.stderr)
4741        TimeTracking = True
4742        print("page duration    enter    leave")
4743        print("---- -------- -------- --------")
4744
4745# set cursor visibility
4746def SetCursor(visible):
4747    global CursorVisible
4748    CursorVisible = visible
4749    if EnableCursor and not(CursorImage) and (MouseHideDelay != 1):
4750        Platform.SetMouseVisible(visible)
4751
4752# handle a shortcut key event: store it (if shifted) or return the
4753# page number to navigate to (if not)
4754def HandleShortcutKey(key, current=0):
4755    if not(key) or (key[0] != '*'):
4756        return None
4757    shift = key.startswith('*shift+')
4758    if shift:
4759        key = key[7:]
4760    else:
4761        key = key[1:]
4762    if (len(key) == 1) or ((key >= "f1") and (key <= "f9")):
4763        # Note: F10..F12 are implicitly included due to lexicographic sorting
4764        page = None
4765        for check_page, props in PageProps.items():
4766            if props.get('shortcut') == key:
4767                page = check_page
4768                break
4769        if shift:
4770            if page:
4771                DelPageProp(page, 'shortcut')
4772            SetPageProp(current, 'shortcut', key)
4773        elif page and (page != current):
4774            return page
4775    return None
4776
4777
4778##### EVENT-TO-ACTION BINDING CODE #############################################
4779
4780SpecialKeyNames = set("""
4781ampersand asterisk at backquote backslash backspace break capslock caret clear
4782comma down escape euro end exclaim greater hash help home insert kp_divide
4783kp_enter kp_equals kp_minus kp_multiply kp_plus lalt last lctrl left leftbracket
4784leftparen less lmeta lshift lsuper menu minus mode numlock pagedown pageup pause
4785period plus power print question quote quotedbl ralt rctrl return right
4786rightbracket rightparen rmeta rshift rsuper scrollock semicolon slash space
4787sysreq tab underscore up
4788""".split())
4789KnownEvents = set(list(SpecialKeyNames) + """
4790a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9
4791kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12
4792lmb mmb rmb wheeldown wheelup
4793""".split() + ["btn%d" % i for i in range(1, 20)])
4794
4795# event handling model:
4796# - Platform.GetEvent() generates platform-neutral event (= string) that
4797#   identifies a key or mouse button, with prefix:
4798#   - '+' = key pressed, '-' = key released, '*' = main event ('*' is generated
4799#      directly before '-' for keys and directly after '+' for mouse buttons)
4800#   - "ctrl+", "alt+", "shift+" modifiers, in that order
4801# - event gets translated into a list of actions via the EventMap dictionary
4802# - actions are processed in order of that list, like priorities:
4803#   - list processing terminates at the first action that is successfully handled
4804#   - exception: "forced actions" will always be executed, even if a higher-prio
4805#     action of that list has already been executed; also, they will not stop
4806#     action list execution, even if they have been handled
4807
4808KnownActions = {}
4809EventMap = {}
4810ForcedActions = set()
4811ActivateReleaseActions = set()
4812
4813class ActionNotHandled(Exception):
4814    pass
4815
4816def ActionValidIf(cond):
4817    if not cond:
4818        raise ActionNotHandled()
4819
4820class ActionRelayBase(object):
4821    def __init__(self):
4822        global KnownActions, ActivateReleaseActions
4823        for item in dir(self):
4824            if (item[0] == '_') and (item[1] != '_') and (item[1] != 'X') and (item[-1] != '_'):
4825                doc = getattr(self, item).__doc__
4826                if item.endswith("_ACTIVATE"):
4827                    item = item[:-9]
4828                    ActivateReleaseActions.add(item)
4829                elif item.endswith("_RELEASE"):
4830                    item = item[:-8]
4831                    ActivateReleaseActions.add(item)
4832                item = item[1:].replace('_', '-')
4833                olddoc = KnownActions.get(item)
4834                if not olddoc:
4835                    KnownActions[item] = doc
4836
4837    def __call__(self, ev):
4838        evname = ev[1:].replace('-', '_')
4839        if ev[0] == '$':
4840            meth = getattr(self, '_X_' + evname, None)
4841        elif ev[0] == '*':
4842            meth = getattr(self, '_' + evname, None)
4843        elif ev[0] == '+':
4844            meth = getattr(self, '_' + evname + '_ACTIVATE', None)
4845        elif ev[0] == '-':
4846            meth = getattr(self, '_' + evname + '_RELEASE', None)
4847        if not meth:
4848            return False
4849        try:
4850            meth()
4851            return True
4852        except ActionNotHandled:
4853            return False
4854
4855def ProcessEvent(ev, handler_func):
4856    """
4857    calls the appropriate action handlers for an event
4858    as returned by Platform.GetEvent()
4859    """
4860    if not ev:
4861        return False
4862    if ev[0] == '$':
4863        handler_func(ev)
4864    try:
4865        events = EventMap[ev[1:]]
4866    except KeyError:
4867        return False
4868    prefix = ev[0]
4869    handled = False
4870    no_forced = not(any(((prefix + ev) in ForcedActions) for ev in events))
4871    if no_forced and (prefix in "+-"):
4872        if not(any((ev in ActivateReleaseActions) for ev in events)):
4873            return False
4874    for ev in events:
4875        ev = prefix + ev
4876        if ev in ForcedActions:
4877            handler_func(ev)
4878        elif not handled:
4879            handled = handler_func(ev)
4880        if handled and no_forced:
4881            break
4882    return handled
4883
4884def ValidateEvent(ev, error_prefix=None):
4885    for prefix in ("ctrl+", "alt+", "shift+"):
4886        if ev.startswith(prefix):
4887            ev = ev[len(prefix):]
4888    if (ev in KnownEvents) or ev.startswith('unknown-'):
4889        return True
4890    if error_prefix:
4891        error_prefix += ": "
4892    else:
4893        error_prefix = ""
4894    print("ERROR: %signoring unknown event '%s'" % (error_prefix, ev), file=sys.stderr)
4895    return False
4896
4897def ValidateAction(ev, error_prefix=None):
4898    if not(KnownActions) or (ev in KnownActions):
4899        return True
4900    if error_prefix:
4901        error_prefix += ": "
4902    else:
4903        error_prefix = ""
4904    print("ERROR: %signoring unknown action '%s'" % (error_prefix, ev), file=sys.stderr)
4905    return False
4906
4907def BindEvent(events, actions=None, clear=False, remove=False, error_prefix=None):
4908    """
4909    bind one or more events to one or more actions
4910    - events and actions can be lists or single comma-separated strings
4911    - if clear is False, actions will be *added* to the raw events,
4912      if clear is True, the specified actions will *replace* the current set,
4913      if remove is True, the specified actions will be *removed* from the set
4914    - actions can be omitted; instead, events can be a string consisting
4915      of raw event and internal event names, separated by one of:
4916        '=' -> add or replace, based on the clear flag
4917        '+=' -> always add
4918        ':=' -> always clear
4919        '-=' -> always remove
4920    - some special events are recognized:
4921        'clearall' clears *all* actions of *all* raw events;
4922        'defaults' loads all defaults
4923        'include', followed by whitespace and a filename, will include a file
4924        (that's what the basedirs option is for)
4925    """
4926    global EventMap
4927    if isinstance(events, basestring):
4928        if not actions:
4929            if (';' in events) or ('\n' in events):
4930                for cmd in events.replace('\n', ';').split(';'):
4931                    BindEvent(cmd, clear=clear, remove=remove, error_prefix=error_prefix)
4932                return
4933            if '=' in events:
4934                events, actions = events.split('=', 1)
4935                events = events.rstrip()
4936                if events.endswith('+'):
4937                    clear = False
4938                    events = events[:-1]
4939                elif events.endswith(':'):
4940                    clear = True
4941                    events = events[:-1]
4942                elif events.endswith('-'):
4943                    remove = True
4944                    events = events[:-1]
4945        events = events.split(',')
4946    if actions is None:
4947        actions = []
4948    elif isinstance(actions, basestring):
4949        actions = actions.split(',')
4950    actions = [b.replace('_', '-').strip(' \t$+-').lower() for b in actions]
4951    actions = [a for a in actions if ValidateAction(a, error_prefix)]
4952    for event in events:
4953        event_orig = event.replace('\t', ' ').strip(' \r\n+-$')
4954        if not event_orig:
4955            continue
4956        event = event_orig.replace('-', '_').lower()
4957        if event.startswith('include '):
4958            filename = event_orig[8:].strip()
4959            if (filename.startswith('"') and filename.endswith('"')) \
4960            or (filename.startswith("'") and filename.endswith("'")):
4961                filename = filename[1:-1]
4962            ParseInputBindingFile(filename)
4963            continue
4964        elif event == 'clearall':
4965            EventMap = {}
4966            continue
4967        elif event == 'defaults':
4968            LoadDefaultBindings()
4969            continue
4970        event = event.replace(' ', '')
4971        if not ValidateEvent(event, error_prefix):
4972            continue
4973        if remove:
4974            if event in EventMap:
4975                for a in actions:
4976                    try:
4977                        EventMap[event].remove(a)
4978                    except ValueError:
4979                        pass
4980        elif clear or not(event in EventMap):
4981            EventMap[event] = actions[:]
4982        else:
4983            EventMap[event].extend(actions)
4984
4985def ParseInputBindingFile(filename):
4986    """
4987    parse an input configuration file;
4988    basically calls BindEvent() for each line;
4989    '#' is the comment character
4990    """
4991    try:
4992        f = open(filename, "r")
4993        n = 0
4994        for line in f:
4995            n += 1
4996            line = line.split('#', 1)[0].strip()
4997            if line:
4998                BindEvent(line, error_prefix="%s:%d" % (filename, n))
4999        f.close()
5000    except IOError as e:
5001        print("ERROR: failed to read the input configuration file '%s' -" % filename, e, file=sys.stderr)
5002
5003def EventHelp():
5004    evlist = ["a-z", "0-9", "kp0-kp9", "f1-f12"] + sorted(list(SpecialKeyNames))
5005    print("Event-to-action binding syntax:")
5006    print("  <event> [,<event2...>] = <action> [,<action2...>]")
5007    print("  By default, this will *add* actions to an event.")
5008    print("  To *overwrite* the current binding for an event, use ':=' instead of '='.")
5009    print("  To remove actions from an event, use '-=' instead of '='.")
5010    print("  Join multiple bindings with a semi-colon (';').")
5011    print("Special commands:")
5012    print("  clearall       = clear all bindings")
5013    print("  defaults       = load default bindings")
5014    print("  include <file> = load bindings from a file")
5015    print("Binding files use the same syntax with one binding per line;")
5016    print("comments start with a '#' symbol.")
5017    print()
5018    print("Recognized keyboard event names:")
5019    while evlist:
5020        line = "  "
5021        while evlist and ((len(line) + len(evlist[0])) < 78):
5022            line += evlist.pop(0) + ", "
5023        line = line.rstrip()
5024        if not evlist:
5025            line = line.rstrip(',')
5026        print(line)
5027    print("Recognized mouse event names:")
5028    print("  lmb, mmb, rmb (= left, middle and right mouse buttons),")
5029    print("  wheelup, wheeldown,")
5030    print("  btnX (additional buttons, use --evtest to check their mapping)")
5031    print()
5032    print("Recognized actions:")
5033    maxalen = max(map(len, KnownActions))
5034    for action in sorted(KnownActions):
5035        doc = KnownActions[action]
5036        if doc:
5037            print("  %s - %s" % (action.ljust(maxalen), doc))
5038        else:
5039            print("  %s" % action)
5040    print()
5041    if not EventMap: return
5042    print("Current bindings:")
5043    maxelen = max(map(len, EventMap))
5044    for event in sorted(EventMap):
5045        if EventMap[event]:
5046            print("  %s = %s" % (event.ljust(maxelen), ", ".join(EventMap[event])))
5047
5048def LoadDefaultBindings():
5049    BindEvent("""clearall
5050    escape, return, kp_enter, lmb, rmb = video-stop
5051    space = video-pause
5052    period = video-step
5053    down = video-seek-backward-10
5054    left = video-seek-backward-1
5055    right = video-seek-forward-1
5056    up = video-seek-forward-10
5057
5058    escape = overview-exit, zoom-exit, spotlight-exit, box-clear, quit
5059    q = quit
5060    f = fullscreen
5061    tab = overview-enter, overview-exit
5062    s = save
5063    a = auto-toggle
5064    t = time-toggle
5065    r = time-reset
5066    c = box-clear
5067    y, z = zoom-enter, zoom-exit
5068    o = toggle-overview
5069    i = toggle-skip
5070    u = zoom-update
5071    b, period = fade-to-black
5072    w, comma = fade-to-white
5073    return, kp_enter = overview-confirm, spotlight-enter, spotlight-exit
5074    plus, kp_plus, 0, wheelup = spotlight-grow
5075    minus, kp_minus, 9, wheeldown = spotlight-shrink
5076    ctrl+9, ctrl+0 = spotlight-reset
5077    7 = fade-less
5078    8 = fade-more
5079    ctrl+7, ctrl+8 = fade-reset
5080    leftbracket = gamma-decrease
5081    rightbracket = gamma-increase
5082    shift+leftbracket = gamma-bl-decrease
5083    shift+rightbracket = gamma-bl-increase
5084    backslash = gamma-reset
5085    lmb = box-add, hyperlink, overview-confirm
5086    ctrl+lmb = box-zoom, hyperlink-notrans
5087    rmb = box-zoom-exit, zoom-pan, box-remove, overview-exit
5088    mmb = zoom-pan, zoom-exit, overview-enter, overview-exit
5089    left, wheelup = overview-prev
5090    right, wheeldown = overview-next
5091    up = overview-up
5092    down = overview-down
5093    wheelup = zoom-in
5094    wheeldown = zoom-out
5095
5096    lmb, wheeldown, pagedown, down, right, space = goto-next
5097    ctrl+lmb, ctrl+wheeldown, ctrl+pagedown, ctrl+down, ctrl+right, ctrl+space = goto-next-notrans
5098    rmb, wheelup, pageup, up, left, backspace = goto-prev
5099    ctrl+rmb, ctrl+wheelup, ctrl+pageup, ctrl+up, ctrl+left, ctrl+backspace = goto-prev-notrans
5100    home = goto-start
5101    ctrl+home = goto-start-notrans
5102    end = goto-end
5103    ctrl+end = goto-end-notrans
5104    l = goto-last
5105    ctrl+l = goto-last-notrans
5106    """, error_prefix="LoadDefaultBindings")
5107
5108# basic action implementations (i.e. stuff that is required to work in all modes)
5109class BaseActions(ActionRelayBase):
5110    def _X_quit(self):
5111        Quit()
5112
5113    def _X_alt_tab(self):
5114        ActionValidIf(Fullscreen)
5115        SetFullscreen(False)
5116        Platform.Minimize()
5117
5118    def _quit(self):
5119        "quit Impressive immediately"
5120        Platform.PostQuitEvent()
5121
5122    def _X_move(self):
5123        # mouse move in fullscreen mode -> show mouse cursor and reset mouse timer
5124        if Fullscreen:
5125            Platform.ScheduleEvent("$hide-mouse", MouseHideDelay)
5126            SetCursor(True)
5127
5128    def _X_call(self):
5129        while CallQueue:
5130            func, args, kwargs = CallQueue.pop(0)
5131            func(*args, **kwargs)
5132
5133
5134##### OVERVIEW MODE ############################################################
5135
5136def UpdateOverviewTexture():
5137    global OverviewNeedUpdate
5138    Loverview.acquire()
5139    try:
5140        gl.load_texture(gl.TEXTURE_2D, Tnext, OverviewImage)
5141    finally:
5142        Loverview.release()
5143    OverviewNeedUpdate = False
5144
5145# draw the overview page
5146def DrawOverview():
5147    if VideoPlaying: return
5148    gl.Clear(gl.COLOR_BUFFER_BIT)
5149    TexturedRectShader.get_instance().draw(
5150        0.0, 0.0, 1.0, 1.0,
5151        s1=TexMaxS, t1=TexMaxT,
5152        tex=Tnext, color=0.75
5153    )
5154
5155    pos = OverviewPos(OverviewSelection)
5156    X0 = PixelX *  pos[0]
5157    Y0 = PixelY *  pos[1]
5158    X1 = PixelX * (pos[0] + OverviewCellX)
5159    Y1 = PixelY * (pos[1] + OverviewCellY)
5160    TexturedRectShader.get_instance().draw(
5161        X0, Y0, X1, Y1,
5162        X0 * TexMaxS, Y0 * TexMaxT,
5163        X1 * TexMaxS, Y1 * TexMaxT,
5164        color=1.0
5165    )
5166
5167    gl.Enable(gl.BLEND)
5168    if OSDFont:
5169        OSDFont.BeginDraw()
5170        DrawOSDEx(OSDTitlePos,  CurrentOSDCaption)
5171        DrawOSDEx(OSDPagePos,   CurrentOSDPage)
5172        DrawOSDEx(OSDStatusPos, CurrentOSDStatus)
5173        OSDFont.EndDraw()
5174        DrawOverlays()
5175    Platform.SwapBuffers()
5176
5177# overview zoom effect, time mapped through func
5178def OverviewZoom(func):
5179    global TransitionRunning
5180    if OverviewDuration <= 0:
5181        return
5182    pos = OverviewPos(OverviewSelection)
5183    X0 = PixelX * (pos[0] + OverviewBorder)
5184    Y0 = PixelY * (pos[1] + OverviewBorder)
5185    X1 = PixelX * (pos[0] - OverviewBorder + OverviewCellX)
5186    Y1 = PixelY * (pos[1] - OverviewBorder + OverviewCellY)
5187
5188    shader = TexturedRectShader.get_instance()
5189    TransitionRunning = True
5190    t0 = Platform.GetTicks()
5191    while not(VideoPlaying):
5192        t = (Platform.GetTicks() - t0) * 1.0 / OverviewDuration
5193        if t >= 1.0: break
5194        t = func(t)
5195        t1 = t*t
5196        t = 1.0 - t1
5197
5198        zoom = (t * (X1 - X0) + t1) / (X1 - X0)
5199        OX = zoom * (t * X0 - X0) - (zoom - 1.0) * t * X0
5200        OY = zoom * (t * Y0 - Y0) - (zoom - 1.0) * t * Y0
5201        OX = t * X0 - zoom * X0
5202        OY = t * Y0 - zoom * Y0
5203
5204        gl.Clear(gl.COLOR_BUFFER_BIT)
5205        shader.draw(  # base overview page
5206            OX, OY, OX + zoom, OY + zoom,
5207            s1=TexMaxS, t1=TexMaxT,
5208            tex=Tnext, color=0.75
5209        )
5210        shader.draw(  # highlighted part
5211            OX + X0 * zoom, OY + Y0 * zoom,
5212            OX + X1 * zoom, OY + Y1 * zoom,
5213            X0 * TexMaxS, Y0 * TexMaxT,
5214            X1 * TexMaxS, Y1 * TexMaxT,
5215            color=1.0
5216        )
5217        gl.Enable(gl.BLEND)
5218        shader.draw(  # overlay of the original high-res page
5219            t * X0,      t * Y0,
5220            t * X1 + t1, t * Y1 + t1,
5221            s1=TexMaxS, t1=TexMaxT,
5222            tex=Tcurrent, color=(1.0, 1.0, 1.0, 1.0 - t * t * t)
5223        )
5224
5225        if OSDFont:
5226            OSDFont.BeginDraw()
5227            DrawOSDEx(OSDTitlePos,  CurrentOSDCaption, alpha_factor=t)
5228            DrawOSDEx(OSDPagePos,   CurrentOSDPage,    alpha_factor=t)
5229            DrawOSDEx(OSDStatusPos, CurrentOSDStatus,  alpha_factor=t)
5230            OSDFont.EndDraw()
5231            DrawOverlays()
5232        Platform.SwapBuffers()
5233    TransitionRunning = False
5234
5235# overview keyboard navigation
5236def OverviewKeyboardNav(delta):
5237    global OverviewSelection
5238    dest = OverviewSelection + delta
5239    if (dest >= OverviewPageCount) or (dest < 0):
5240        return
5241    OverviewSelection = dest
5242    x, y = OverviewPos(OverviewSelection)
5243    Platform.SetMousePos((x + (OverviewCellX // 2), y + (OverviewCellY // 2)))
5244
5245# overview mode PageProp toggle
5246def OverviewTogglePageProp(prop, default):
5247    if (OverviewSelection < 0) or (OverviewSelection >= len(OverviewPageMap)):
5248        return
5249    page = OverviewPageMap[OverviewSelection]
5250    SetPageProp(page, prop, not(GetPageProp(page, prop, default)))
5251    UpdateCaption(page, force=True)
5252    DrawOverview()
5253
5254class ExitOverview(Exception):
5255    pass
5256
5257# action implementation for overview mode
5258class OverviewActions(BaseActions):
5259    def _X_move(self):
5260        global OverviewSelection
5261        BaseActions._X_move(self)
5262        # determine highlighted page
5263        x, y = Platform.GetMousePos()
5264        OverviewSelection = \
5265             int((x - OverviewOfsX) / OverviewCellX) + \
5266             int((y - OverviewOfsY) / OverviewCellY) * OverviewGridSize
5267        if (OverviewSelection < 0) or (OverviewSelection >= len(OverviewPageMap)):
5268            UpdateCaption(0)
5269        else:
5270            UpdateCaption(OverviewPageMap[OverviewSelection])
5271        DrawOverview()
5272
5273    def _X_quit(self):
5274        PageLeft(overview=True)
5275        Quit()
5276
5277    def _X_expose(self):
5278        DrawOverview()
5279
5280    def _X_hide_mouse(self):
5281        # mouse timer event -> hide fullscreen cursor
5282        SetCursor(False)
5283        DrawOverview()
5284
5285    def _X_timer_update(self):
5286        force_update = OverviewNeedUpdate
5287        if OverviewNeedUpdate:
5288            UpdateOverviewTexture()
5289        if TimerTick() or force_update:
5290            DrawOverview()
5291
5292    def _overview_exit(self):
5293        "exit overview mode and return to the last page"
5294        global OverviewSelection
5295        OverviewSelection = -1
5296        raise ExitOverview
5297    def _overview_confirm(self):
5298        "exit overview mode and go to the selected page"
5299        raise ExitOverview
5300
5301    def _fullscreen(self):
5302        SetFullscreen(not(Fullscreen))
5303
5304    def _save(self):
5305        SaveInfoScript(InfoScriptPath)
5306
5307    def _fade_to_black(self):
5308        FadeMode(0.0)
5309    def _fade_to_white(self):
5310        FadeMode(1.0)
5311
5312    def _time_toggle(self):
5313        global TimeDisplay
5314        TimeDisplay = not(TimeDisplay)
5315        DrawOverview()
5316    def _time_reset(self):
5317        ResetTimer()
5318        if TimeDisplay:
5319            DrawOverview()
5320
5321    def _toggle_skip(self):
5322        TogglePageProp('skip', False)
5323    def _toggle_overview(self):
5324        TogglePageProp('overview', GetPageProp(Pcurrent, '_overview', True))
5325
5326    def _overview_up(self):
5327        "move the overview selection upwards"
5328        OverviewKeyboardNav(-OverviewGridSize)
5329    def _overview_prev(self):
5330        "select the previous page in overview mode"
5331        OverviewKeyboardNav(-1)
5332    def _overview_next(self):
5333        "select the next page in overview mode"
5334        OverviewKeyboardNav(+1)
5335    def _overview_down(self):
5336        "move the overview selection downwards"
5337        OverviewKeyboardNav(+OverviewGridSize)
5338OverviewActions = OverviewActions()
5339
5340# overview mode entry/loop/exit function
5341def DoOverview():
5342    global Pcurrent, Pnext, Tcurrent, Tnext, Tracing, OverviewSelection
5343    global PageEnterTime, OverviewMode
5344
5345    Platform.ScheduleEvent("$page-timeout", 0)
5346    PageLeft()
5347    UpdateOverviewTexture()
5348
5349    if GetPageProp(Pcurrent, 'boxes') or Tracing:
5350        BoxFade(lambda t: 1.0 - t)
5351    Tracing = False
5352    OverviewSelection = OverviewPageMapInv[Pcurrent]
5353
5354    OverviewMode = True
5355    OverviewZoom(lambda t: 1.0 - t)
5356    DrawOverview()
5357    PageEnterTime = Platform.GetTicks() - StartTime
5358
5359    try:
5360        while True:
5361            ev = Platform.GetEvent()
5362            if not ev:
5363                continue
5364            if not ProcessEvent(ev, OverviewActions):
5365                try:
5366                    page = OverviewPageMap[OverviewSelection]
5367                except IndexError:
5368                    page = 0
5369                page = HandleShortcutKey(ev, page)
5370                if page:
5371                    OverviewSelection = OverviewPageMapInv[page]
5372                    x, y = OverviewPos(OverviewSelection)
5373                    Platform.SetMousePos((x + (OverviewCellX // 2),
5374                                          y + (OverviewCellY // 2)))
5375                    DrawOverview()
5376    except ExitOverview:
5377        PageLeft(overview=True)
5378
5379    if (OverviewSelection < 0) or (OverviewSelection >= OverviewPageCount):
5380        OverviewSelection = OverviewPageMapInv[Pcurrent]
5381        Pnext = Pcurrent
5382    else:
5383        Pnext = OverviewPageMap[OverviewSelection]
5384    if Pnext != Pcurrent:
5385        Pcurrent = Pnext
5386        RenderPage(Pcurrent, Tcurrent)
5387    UpdateCaption(Pcurrent)
5388    OverviewZoom(lambda t: t)
5389    OverviewMode = False
5390    DrawCurrentPage()
5391
5392    if GetPageProp(Pcurrent, 'boxes'):
5393        BoxFade(lambda t: t)
5394    PageEntered()
5395    if not PreloadNextPage(GetNextPage(Pcurrent, 1)):
5396        PreloadNextPage(GetNextPage(Pcurrent, -1))
5397
5398
5399##### EVENT HANDLING ###########################################################
5400
5401# set fullscreen mode
5402def SetFullscreen(fs, do_init=True):
5403    global Fullscreen
5404    if FakeFullscreen:
5405        return  # this doesn't work in fake-fullscreen mode
5406    if do_init:
5407        if fs == Fullscreen: return
5408        if not Platform.ToggleFullscreen(): return
5409    Fullscreen = fs
5410    DrawCurrentPage()
5411    if fs:
5412        Platform.ScheduleEvent("$hide-mouse", MouseHideDelay)
5413    else:
5414        Platform.ScheduleEvent("$hide-mouse", 0)
5415        SetCursor(True)
5416
5417# PageProp toggle
5418def TogglePageProp(prop, default):
5419    global WantStatus
5420    SetPageProp(Pcurrent, prop, not(GetPageProp(Pcurrent, prop, default)))
5421    UpdateCaption(Pcurrent, force=True)
5422    WantStatus = True
5423    DrawCurrentPage()
5424
5425# basic action implementations (i.e. stuff that is required to work, except in overview mode)
5426class BaseDisplayActions(BaseActions):
5427    def _X_quit(self):
5428        if FadeInOut:
5429            EnterFadeMode()
5430        PageLeft()
5431        Quit()
5432
5433    def _X_expose(self):
5434        DrawCurrentPage()
5435
5436    def _X_hide_mouse(self):
5437        # mouse timer event -> hide fullscreen cursor
5438        SetCursor(False)
5439        DrawCurrentPage()
5440
5441    def _X_page_timeout(self):
5442        global NextPageAfterVideo
5443        if VideoPlaying:
5444            NextPageAfterVideo = True
5445        else:
5446            TransitionTo(GetNextPage(Pcurrent, 1))
5447
5448    def _X_poll_file(self):
5449        global RTrunning, RTrestart, Pnext
5450        dirty = False
5451        for f in FileProps:
5452            s = my_stat(f)
5453            if s != GetFileProp(f, 'stat'):
5454                dirty = True
5455                SetFileProp(f, 'stat', s)
5456        if dirty:
5457            # first, check if the new file is valid
5458            if not os.path.isfile(GetPageProp(Pcurrent, '_file')):
5459                return
5460            # invalidate everything we used to know about the input files
5461            InvalidateCache()
5462            for props in PageProps.values():
5463                for prop in ('_overview_rendered', '_box', '_href'):
5464                    if prop in props: del props[prop]
5465            LoadInfoScript()
5466            # force a transition to the current page, reloading it
5467            Pnext = -1
5468            TransitionTo(Pcurrent)
5469            # restart the background renderer thread. this is not completely safe,
5470            # i.e. there's a small chance that we fail to restart the thread, but
5471            # this isn't critical
5472            if CacheMode and BackgroundRendering:
5473                if RTrunning:
5474                    RTrestart = True
5475                else:
5476                    RTrunning = True
5477                    thread.start_new_thread(RenderThread, (Pcurrent, Pnext))
5478
5479    def _X_timer_update(self):
5480        if VideoPlaying and MPlayerProcess:
5481            if MPlayerProcess.poll() is not None:
5482                StopMPlayer()
5483                DrawCurrentPage()
5484        elif TimerTick():
5485            DrawCurrentPage()
5486
5487# action implementations for video playback
5488class VideoActions(BaseDisplayActions):
5489    def _video_stop(self):
5490        "stop video playback"
5491        StopMPlayer()
5492        DrawCurrentPage()
5493
5494    def player_command(self, mplayer_cmd, omxplayer_cmd):
5495        "helper for the various video-* actions"
5496        cmd = omxplayer_cmd if Platform.use_omxplayer else (mplayer_cmd + '\n')
5497        if not cmd: return
5498        try:
5499            MPlayerProcess.stdin.write(cmd)
5500            MPlayerProcess.stdin.flush()
5501        except:
5502            StopMPlayer()
5503            DrawCurrentPage()
5504    def _video_pause(self):
5505        "pause video playback"
5506        self.player_command("pause", 'p')
5507    def _video_step(self):
5508        "advance to the next frame in paused video"
5509        self.player_command("framestep", None)
5510    def _video_seek_backward_10(self):
5511        "seek 10 seconds backward in video"
5512        self.player_command("seek -10 pausing_keep", '\x1b[D')
5513    def _video_seek_backward_1(self):
5514        "seek 1 second backward in video"
5515        self.player_command("seek -1 pausing_keep", None)
5516    def _video_seek_forward_1(self):
5517        "seek 1 second forward in video"
5518        self.player_command("seek 1 pausing_keep", None)
5519    def _video_seek_forward_10(self):
5520        "seek 10 seconds forward in video"
5521        self.player_command("seek 10 pausing_keep", '\x1b[C')
5522VideoActions = VideoActions()
5523
5524# action implementation for normal page display (i.e. everything except overview mode)
5525class PageDisplayActions(BaseDisplayActions):
5526    def _X_move(self):
5527        global Marking, MarkLR, Panning, ZoomX0, ZoomY0
5528        BaseActions._X_move(self)
5529        x, y = Platform.GetMousePos()
5530        # activate marking if mouse is moved away far enough
5531        if MarkValid and not(Marking):
5532            if (abs(x - MarkBaseX) > 4) and (abs(y - MarkBaseY) > 4):
5533                Marking = True
5534        # mouse move while marking -> update marking box
5535        if Marking:
5536            MarkLR = MouseToScreen((x, y))
5537        # mouse move while RMB is pressed -> panning
5538        if PanValid and ZoomMode:
5539            if not(Panning) and (abs(x - PanBaseX) > 1) and (abs(y - PanBaseY) > 1):
5540                Panning = True
5541            # ZoomArea is guaranteed to be float
5542            ZoomX0 = PanAnchorX + (PanBaseX - x) * ZoomArea / ScreenWidth
5543            ZoomY0 = PanAnchorY + (PanBaseY - y) * ZoomArea / ScreenHeight
5544            ZoomX0 = min(max(ZoomX0, 0.0), 1.0 - ZoomArea)
5545            ZoomY0 = min(max(ZoomY0, 0.0), 1.0 - ZoomArea)
5546        # if anything changed, redraw the page
5547        if Marking or Tracing or Panning or (CursorImage and CursorVisible):
5548            DrawCurrentPage()
5549
5550    def _zoom_pan_ACTIVATE(self):
5551        "pan visible region in zoom mode"
5552        global PanValid, Panning, PanBaseX, PanBaseY, PanAnchorX, PanAnchorY
5553        ActionValidIf(ZoomMode and not(BoxZoom))
5554        PanValid = True
5555        Panning = False
5556        PanBaseX, PanBaseY = Platform.GetMousePos()
5557        PanAnchorX = ZoomX0
5558        PanAnchorY = ZoomY0
5559    def _zoom_pan(self):
5560        ActionValidIf(ZoomMode and Panning)
5561    def _zoom_pan_RELEASE(self):
5562        global PanValid, Panning
5563        PanValid = False
5564        Panning = False
5565
5566    def _zoom_enter(self):
5567        "enter zoom mode"
5568        ActionValidIf(not(ZoomMode))
5569        tx, ty = MouseToScreen(Platform.GetMousePos())
5570        EnterZoomMode(DefaultZoomFactor,
5571                      (1.0 - 1.0 / DefaultZoomFactor) * tx,
5572                      (1.0 - 1.0 / DefaultZoomFactor) * ty)
5573    def _zoom_exit(self):
5574        "leave zoom mode"
5575        ActionValidIf(ZoomMode)
5576        LeaveZoomMode()
5577
5578    def _box_add_ACTIVATE(self):
5579        "draw a new highlight box [mouse-only]"
5580        global MarkValid, Marking, MarkBaseX, MarkBaseY, MarkUL, MarkLR
5581        MarkValid = True
5582        Marking = False
5583        MarkBaseX, MarkBaseY = Platform.GetMousePos()
5584        MarkUL = MarkLR = MouseToScreen((MarkBaseX, MarkBaseY))
5585    def _box_add(self):
5586        global Marking
5587        ActionValidIf(Marking)
5588        Marking = False
5589        if BoxTooSmall():
5590            raise ActionNotHandled()
5591        boxes = GetPageProp(Pcurrent, 'boxes', [])
5592        oldboxcount = len(boxes)
5593        boxes.append(NormalizeRect(MarkUL[0], MarkUL[1], MarkLR[0], MarkLR[1]))
5594        SetPageProp(Pcurrent, 'boxes', boxes)
5595        if not(oldboxcount) and not(Tracing):
5596            BoxFade(lambda t: t)
5597        DrawCurrentPage()
5598    def _box_add_RELEASE(self):
5599        global MarkValid
5600        MarkValid = False
5601
5602    def _box_remove(self):
5603        "remove the highlight box under the mouse cursor"
5604        ActionValidIf(not(Panning) and not(Marking))
5605        boxes = GetPageProp(Pcurrent, 'boxes', [])
5606        x, y = MouseToScreen(Platform.GetMousePos())
5607        try:
5608            # if a box is already present around the clicked position, kill it
5609            idx = FindBox(x, y, boxes)
5610            if (len(boxes) == 1) and not(Tracing):
5611                BoxFade(lambda t: 1.0 - t)
5612            del boxes[idx]
5613            SetPageProp(Pcurrent, 'boxes', boxes)
5614            DrawCurrentPage()
5615        except ValueError:
5616            # no box present
5617            raise ActionNotHandled()
5618
5619    def _box_clear(self):
5620        "remove all highlight boxes on the current page"
5621        ActionValidIf(GetPageProp(Pcurrent, 'boxes'))
5622        if not Tracing:
5623            BoxFade(lambda t: 1.0 - t)
5624        DelPageProp(Pcurrent, 'boxes')
5625        DrawCurrentPage()
5626
5627    def _box_zoom_ACTIVATE(self):
5628        "draw a box to zoom into [mouse-only]"
5629        ActionValidIf(not(BoxZoom) and not(Tracing) and not(GetPageProp(Pcurrent, 'boxes')))
5630        return self._box_add_ACTIVATE()
5631    def _box_zoom(self):
5632        global Marking, BoxZoom, ZoomBox
5633        ActionValidIf(Marking and not(BoxZoom) and not(Tracing) and not(GetPageProp(Pcurrent, 'boxes')))
5634        Marking = False
5635        if BoxTooSmall():
5636            raise ActionNotHandled()
5637        zxRatio = 0.5 if HalfScreen else 1.0
5638        z = min(zxRatio / abs(MarkUL[0] - MarkLR[0]), 1.0 / abs(MarkUL[1] - MarkLR[1]))
5639        if z <= 1:
5640            return DrawCurrentPage()
5641        if HalfScreen:
5642            tx = max(MarkLR[0], MarkUL[0])
5643        else:
5644            tx = (MarkUL[0] + MarkLR[0]) * 0.5
5645        ty = (MarkUL[1] + MarkLR[1]) * 0.5
5646        tx = tx + (tx - 0.5) / (z - 1.0)
5647        ty = ty + (ty - 0.5) / (z - 1.0)
5648        tx = (1.0 - 1.0 / z) * tx
5649        ty = (1.0 - 1.0 / z) * ty
5650        BoxZoom = NormalizeRect(MarkUL[0], MarkUL[1], MarkLR[0], MarkLR[1])
5651        EnterZoomMode(z, tx, ty)
5652    def _box_zoom_RELEASE(self):
5653        return self._box_add_RELEASE()
5654
5655    def _box_zoom_exit(self):
5656        "leave box-zoom mode"
5657        ActionValidIf(BoxZoom)
5658        LeaveZoomMode()
5659
5660    def _hyperlink(self, allow_transition=True):
5661        "navigate to the hyperlink under the mouse cursor"
5662        x, y = Platform.GetMousePos()
5663        for valid, target, x0, y0, x1, y1 in GetPageProp(Pcurrent, '_href', []):
5664            if valid and (x >= x0) and (x < x1) and (y >= y0) and (y < y1):
5665                if isinstance(target, int):
5666                    TransitionTo(target, allow_transition=allow_transition)
5667                elif target:
5668                    RunURL(target)
5669                return
5670        raise ActionNotHandled()
5671    def _hyperlink_notrans(self):
5672        "like 'hyperlink', but no transition on page change"
5673        return self._hyperlink(allow_transition=False)
5674
5675    def _goto_prev(self):
5676        "go to the previous page (with transition)"
5677        TransitionTo(GetNextPage(Pcurrent, -1), allow_transition=True)
5678    def _goto_prev_notrans(self):
5679        "go to the previous page (without transition)"
5680        TransitionTo(GetNextPage(Pcurrent, -1), allow_transition=False)
5681    def _goto_next(self):
5682        "go to the next page (with transition)"
5683        TransitionTo(GetNextPage(Pcurrent, +1), allow_transition=True)
5684    def _goto_next_notrans(self):
5685        "go to the next page (without transition)"
5686        TransitionTo(GetNextPage(Pcurrent, +1), allow_transition=False)
5687    def _goto_last(self):
5688        "go to the last visited page (with transition)"
5689        TransitionTo(LastPage, allow_transition=True)
5690    def _goto_last_notrans(self):
5691        "go to the last visited page (without transition)"
5692        TransitionTo(LastPage, allow_transition=False)
5693    def _goto_start(self):
5694        "go to the first page (with transition)"
5695        ActionValidIf(Pcurrent != 1)
5696        TransitionTo(1, allow_transition=True)
5697    def _goto_start_notrans(self):
5698        "go to the first page (without transition)"
5699        ActionValidIf(Pcurrent != 1)
5700        TransitionTo(1, allow_transition=False)
5701    def _goto_end(self):
5702        "go to the final page (with transition)"
5703        ActionValidIf(Pcurrent != PageCount)
5704        TransitionTo(PageCount, allow_transition=True)
5705    def _goto_end_notrans(self):
5706        "go to the final page (without transition)"
5707        ActionValidIf(Pcurrent != PageCount)
5708        TransitionTo(PageCount, allow_transition=False)
5709
5710    def _overview_enter(self):
5711        "zoom out to the overview page"
5712        if not EnableOverview: return
5713        LeaveZoomMode()
5714        DoOverview()
5715
5716    def _spotlight_enter(self):
5717        "enter spotlight mode"
5718        global Tracing
5719        ActionValidIf(not(Tracing))
5720        Tracing = True
5721        if GetPageProp(Pcurrent, 'boxes'):
5722            DrawCurrentPage()
5723        else:
5724            BoxFade(lambda t: t)
5725    def _spotlight_exit(self):
5726        "exit spotlight mode"
5727        global Tracing
5728        ActionValidIf(Tracing)
5729        if not GetPageProp(Pcurrent, 'boxes'):
5730            BoxFade(lambda t: 1.0 - t)
5731        Tracing = False
5732        DrawCurrentPage()
5733
5734    def _spotlight_shrink(self):
5735        "decrease the spotlight radius"
5736        ActionValidIf(Tracing)
5737        IncrementSpotSize(-8)
5738    def _spotlight_grow(self):
5739        "increase the spotlight radius"
5740        ActionValidIf(Tracing)
5741        IncrementSpotSize(+8)
5742    def _spotlight_reset(self):
5743        "reset the spotlight radius to its default value"
5744        global SpotRadius
5745        ActionValidIf(Tracing)
5746        SpotRadius = SpotRadiusBase
5747        GenerateSpotMesh()
5748        DrawCurrentPage()
5749
5750    def _zoom_in(self):
5751        "zoom in a small bit"
5752        ActionValidIf((MouseWheelZoom or ZoomMode) and not(BoxZoom))
5753        ChangeZoom(ViewZoomFactor * ZoomStep, Platform.GetMousePos())
5754    def _zoom_out(self):
5755        "zoom out a small bit"
5756        ActionValidIf((MouseWheelZoom or ZoomMode) and not(BoxZoom))
5757        # ZoomStep is guaranteed to be float
5758        ChangeZoom(ViewZoomFactor / ZoomStep, Platform.GetMousePos())
5759
5760    def _zoom_update(self):
5761        "re-render the page in the current zoom resolution"
5762        ActionValidIf(ZoomMode)
5763        ReRenderZoom(ViewZoomFactor)
5764
5765    def _fullscreen(self):
5766        "toggle fullscreen mode"
5767        SetFullscreen(not(Fullscreen))
5768
5769    def _save(self):
5770        "save the info script"
5771        SaveInfoScript(InfoScriptPath)
5772
5773    def _fade_to_black(self):
5774        "fade to a black screen"
5775        FadeMode(0.0)
5776    def _fade_to_white(self):
5777        "fade to a white screen"
5778        FadeMode(1.0)
5779
5780    def _auto_stop(self):
5781        "stop automatic slideshow"
5782        global AutoAdvanceEnabled, PageTimeout
5783        AutoAdvanceEnabled = False
5784        PageTimeout = 0
5785        Platform.ScheduleEvent('$page-timeout', 0)
5786        if AutoAdvanceProgress:
5787            DrawCurrentPage()
5788    def _auto_start(self):
5789        "start or resume automatic slideshow"
5790        global AutoAdvanceEnabled, PageTimeout
5791        AutoAdvanceEnabled = True
5792        PageTimeout = AutoAdvanceTime
5793        if (GetPageProp(Pcurrent, '_shown') == 1) or Wrap:
5794            PageTimeout = GetPageProp(Pcurrent, 'timeout', PageTimeout)
5795        dt = PageTimeout - (Platform.GetTicks() - PageEnterTime)
5796        if dt > 0:
5797            Platform.ScheduleEvent('$page-timeout', dt)
5798        else:
5799            TransitionTo(GetNextPage(Pcurrent, 1))
5800    def _auto_toggle(self):
5801        "toggle automatic slideshow"
5802        if AutoAdvanceEnabled:
5803            self._auto_stop()
5804        else:
5805            self._auto_start()
5806
5807    def _time_toggle(self):
5808        "toggle time display and/or time tracking mode"
5809        global TimeDisplay
5810        TimeDisplay = not(TimeDisplay)
5811        DrawCurrentPage()
5812        EnableTimeTracking()
5813    def _time_reset(self):
5814        "reset the on-screen timer"
5815        ResetTimer()
5816        if TimeDisplay:
5817            DrawCurrentPage()
5818
5819    def _toggle_skip(self):
5820        "toggle 'skip' flag of current page"
5821        TogglePageProp('skip', False)
5822    def _toggle_overview(self):
5823        "toggle 'visible on overview' flag of current page"
5824        TogglePageProp('overview', GetPageProp(Pcurrent, '_overview', True))
5825
5826    def _fade_less(self):
5827        "decrease the spotlight/box background darkness"
5828        global BoxFadeDarkness, BoxZoomDarkness
5829        if BoxZoom:
5830            BoxZoomDarkness = max(0.0, BoxZoomDarkness - BoxFadeDarknessStep)
5831        else:
5832            BoxFadeDarkness = max(0.0, BoxFadeDarkness - BoxFadeDarknessStep)
5833        DrawCurrentPage()
5834    def _fade_more(self):
5835        "increase the spotlight/box background darkness"
5836        global BoxFadeDarkness, BoxZoomDarkness
5837        if BoxZoom:
5838            BoxZoomDarkness = min(1.0, BoxZoomDarkness + BoxFadeDarknessStep)
5839        else:
5840            BoxFadeDarkness = min(1.0, BoxFadeDarkness + BoxFadeDarknessStep)
5841        DrawCurrentPage()
5842    def _fade_reset(self):
5843        "reset spotlight/box background darkness to default"
5844        global BoxFadeDarkness, BoxZoomDarkness
5845        BoxFadeDarkness = BoxFadeDarknessBase
5846        BoxZoomDarkness = BoxZoomDarknessBase
5847        DrawCurrentPage()
5848
5849    def _gamma_decrease(self):
5850        "decrease gamma"
5851        # GammaStep is guaranteed to be float
5852        SetGamma(new_gamma=Gamma / GammaStep)
5853    def _gamma_increase(self):
5854        "increase gamma"
5855        SetGamma(new_gamma=Gamma * GammaStep)
5856    def _gamma_bl_decrease(self):
5857        "decrease black level"
5858        SetGamma(new_black=BlackLevel - BlackLevelStep)
5859    def _gamma_bl_increase(self):
5860        "increase black level"
5861        SetGamma(new_black=BlackLevel + BlackLevelStep)
5862    def _gamma_reset(self):
5863        "reset gamma and black level to the defaults"
5864        SetGamma(1.0, 0)
5865
5866PageDisplayActions = PageDisplayActions()
5867ForcedActions.update(("-zoom-pan", "+zoom-pan", "-box-add", "+box-add", "-box-zoom", "+box-zoom"))
5868
5869# main event handling function
5870# takes care that $page-timeout events are handled with least priority
5871def EventHandlerLoop():
5872    poll = True
5873    page_timeout = False
5874    while True:
5875        ev = Platform.GetEvent(poll)
5876        poll = bool(ev)
5877        if not ev:
5878            # no more events in the queue -> can now insert a $page-timeout
5879            if page_timeout:
5880                ev = "$page-timeout"
5881                page_timeout = False
5882            else:
5883                continue
5884        elif ev == "$page-timeout":
5885            page_timeout = True
5886            continue
5887
5888        if VideoPlaying:
5889            # video mode -> ignore all non-video actions
5890            ProcessEvent(ev, VideoActions)
5891        elif ProcessEvent(ev, PageDisplayActions):
5892            # normal action has been handled -> done
5893            pass
5894        elif ev and (ev[0] == '*'):
5895            keyfunc = GetPageProp(Pcurrent, 'keys', {}).get(ev[1:], None)
5896            if keyfunc:
5897                SafeCall(keyfunc)
5898            else:
5899                # handle a shortcut key
5900                ctrl = ev.startswith('*ctrl+')
5901                if ctrl:
5902                    ev = '*' + ev[6:]
5903                page = HandleShortcutKey(ev, Pcurrent)
5904                if page:
5905                    TransitionTo(page, allow_transition=not(ctrl))
5906
5907
5908##### FILE LIST GENERATION #####################################################
5909
5910ImageExts = set('.'+x for x in "jpg jpeg png tif tiff bmp ppm pgm".split())
5911VideoExts = set('.'+x for x in "avi mov mp4 mkv ogv mpg mpeg m1v m2v m4v mts m2ts m2t ts webm 3gp flv qt".split())
5912AllExts = set(list(ImageExts) + list(VideoExts) + [".pdf"])
5913
5914def CheckExt(name, exts):
5915    return os.path.splitext(name)[1].lower() in exts
5916def IsImageFile(name): return CheckExt(name, ImageExts)
5917def IsVideoFile(name): return CheckExt(name, VideoExts)
5918def IsPlayable(name):  return CheckExt(name, AllExts)
5919
5920def AddFile(name, title=None, implicit=False):
5921    global FileList, FileName
5922
5923    # handle list files
5924    if name.startswith('@') and os.path.isfile(name[1:]):
5925        name = name[1:]
5926        dirname = os.path.dirname(name)
5927        try:
5928            f = open(name, "r")
5929            next_title = None
5930            for line in f:
5931                line = [part.strip() for part in line.split('#', 1)]
5932                if len(line) == 1:
5933                    subfile = line[0]
5934                    title = None
5935                else:
5936                    subfile, title = line
5937                if subfile:
5938                    AddFile(os.path.normpath(os.path.join(dirname, subfile)), title, implicit=True)
5939            f.close()
5940        except IOError:
5941            print("Error: cannot read list file `%s'" % name, file=sys.stderr)
5942        return
5943
5944    # generate absolute path
5945    path_sep_at_end = name.endswith(os.path.sep)
5946    name = os.path.normpath(os.path.abspath(name)).rstrip(os.path.sep)
5947    if path_sep_at_end:
5948        name += os.path.sep
5949
5950    # set FileName to first (explicitly specified) input file
5951    if not implicit:
5952        if not FileList:
5953            FileName = name
5954        else:
5955            FileName = ""
5956
5957    if os.path.isfile(name):
5958        if IsPlayable(name):
5959            FileList.append(name)
5960            if title: SetFileProp(name, 'title', title)
5961        else:
5962            print("Warning: input file `%s' has unrecognized file type" % name, file=sys.stderr)
5963
5964    elif os.path.isdir(name):
5965        images = [os.path.join(name, f) for f in os.listdir(name) if IsImageFile(f)]
5966        images.sort(key=lambda f: f.lower())
5967        if not images:
5968            print("Warning: no image files in directory `%s'" % name, file=sys.stderr)
5969        for img in images:
5970            AddFile(img, implicit=True)
5971
5972    else:
5973        files = list(filter(IsPlayable, glob.glob(name)))
5974        if files:
5975            for f in files: AddFile(f, implicit=True)
5976        else:
5977            print("Error: input file `%s' not found" % name, file=sys.stderr)
5978
5979
5980##### INITIALIZATION ###########################################################
5981
5982LoadDefaultBindings()
5983
5984def main():
5985    global gl, ScreenWidth, ScreenHeight, TexWidth, TexHeight, TexSize
5986    global TexMaxS, TexMaxT, PixelX, PixelY, LogoImage
5987    global OverviewGridSize, OverviewCellX, OverviewCellY
5988    global OverviewOfsX, OverviewOfsY, OverviewBorder, OverviewImage, OverviewPageCount
5989    global OverviewPageMap, OverviewPageMapInv, FileName, FileList, PageCount
5990    global DocumentTitle, PageProps, LogoTexture, OSDFont
5991    global Pcurrent, Pnext, Tcurrent, Tnext, InitialPage
5992    global CacheFile, CacheFileName, BaseWorkingDir, RenderToDirectory
5993    global PAR, DAR, TempFileName, Bare, MaxZoomFactor
5994    global BackgroundRendering, FileStats, RTrunning, RTrestart, StartTime
5995    global CursorImage, CursorVisible, InfoScriptPath
5996    global HalfScreen, AutoAdvanceTime, AutoAdvanceEnabled, WindowPos
5997    global BoxFadeDarknessBase, BoxZoomDarknessBase, SpotRadiusBase
5998    global BoxIndexBuffer, UseBlurShader
5999
6000    # allocate temporary file
6001    TempFileName = None
6002    try:
6003        TempFileName = tempfile.mktemp(prefix="impressive-", suffix="_tmp")
6004    except EnvironmentError:
6005        if not Bare:
6006            print("Could not allocate temporary file, reverting to --bare mode.", file=sys.stderr)
6007        Bare = True
6008
6009    # some input guesswork
6010    BaseWorkingDir = os.getcwd()
6011    if not(FileName) and (len(FileList) == 1):
6012        FileName = FileList[0]
6013    if FileName and not(FileList):
6014        AddFile(FileName)
6015    if FileName:
6016        DocumentTitle = os.path.splitext(os.path.split(FileName)[1])[0]
6017
6018    # early graphics initialization
6019    Platform.Init()
6020
6021    # detect screen size and compute aspect ratio
6022    if Fullscreen and (UseAutoScreenSize or not(Platform.allow_custom_fullscreen_res)):
6023        size = Platform.GetScreenSize()
6024        if size:
6025            ScreenWidth, ScreenHeight = size
6026            print("Detected screen size: %dx%d pixels" % (ScreenWidth, ScreenHeight), file=sys.stderr)
6027    if DAR is None:
6028        PAR = 1.0
6029        DAR = float(ScreenWidth) / float(ScreenHeight)
6030    else:
6031        PAR = DAR / float(ScreenWidth) * float(ScreenHeight)
6032
6033    # override some irrelevant settings in event test mode
6034    if EventTestMode:
6035        FileList = ["XXX.EventTestDummy.XXX"]
6036        InfoScriptPath = None
6037        RenderToDirectory = False
6038        InitialPage = None
6039        HalfScreen = False
6040
6041    # fill the page list
6042    if Shuffle:
6043        random.shuffle(FileList)
6044    PageCount = 0
6045    for name in FileList:
6046        ispdf = name.lower().endswith(".pdf")
6047        if ispdf:
6048            # PDF input -> initialize renderers and if none available, reject
6049            if not InitPDFRenderer():
6050                print("Ignoring unrenderable input file '%s'." % name, file=sys.stderr)
6051                continue
6052
6053            # try to pre-parse the PDF file
6054            pages = 0
6055            out = [(ScreenWidth + Overscan, ScreenHeight + Overscan),
6056                   (ScreenWidth + Overscan, ScreenHeight + Overscan)]
6057            res = [(72.0, 72.0), (72.0, 72.0)]
6058
6059            # phase 1: internal PDF parser
6060            try:
6061                pages, pdf_width, pdf_height = analyze_pdf(name)
6062                out = [ZoomToFit((pdf_width, pdf_height * PAR)),
6063                       ZoomToFit((pdf_height, pdf_width * PAR))]
6064                res = [(out[0][0] * 72.0 / pdf_width, out[0][1] * 72.0 / pdf_height),
6065                       (out[1][1] * 72.0 / pdf_width, out[1][0] * 72.0 / pdf_height)]
6066            except KeyboardInterrupt:
6067                raise
6068            except:
6069                pass
6070
6071            # phase 2: use pdftk
6072            if pdftkPath and TempFileName:
6073                try:
6074                    assert 0 == Popen([pdftkPath, name, "dump_data_utf8", "output", TempFileName + ".txt"]).wait()
6075                    title, pages = pdftkParse(TempFileName + ".txt", PageCount)
6076                    if title and (len(FileList) == 1):
6077                        DocumentTitle = title
6078                except KeyboardInterrupt:
6079                    raise
6080                except:
6081                    print("pdftkParse() FAILED")
6082                    pass
6083
6084            # phase 3: use mutool (if pdftk wasn't successful)
6085            if not(pages) and mutoolPath:
6086                try:
6087                    proc = Popen([mutoolPath, "info", name], stdout=subprocess.PIPE)
6088                    title, pages = mutoolParse(proc.stdout)
6089                    assert 0 == proc.wait()
6090                    if title and (len(FileList) == 1):
6091                        DocumentTitle = title
6092                except KeyboardInterrupt:
6093                    raise
6094                except:
6095                    pass
6096        else:
6097            # image or video file
6098            pages = 1
6099            if IsVideoFile(name):
6100                SetPageProp(PageCount + 1, '_video', True)
6101            SetPageProp(PageCount + 1, '_title', os.path.split(name)[-1])
6102
6103        # validity check
6104        if not pages:
6105            print("WARNING: The input file `%s' could not be analyzed." % name, file=sys.stderr)
6106            continue
6107
6108        # add pages and files into PageProps and FileProps
6109        pagerange = list(range(PageCount + 1, PageCount + pages + 1))
6110        for page in pagerange:
6111            SetPageProp(page, '_file', name)
6112            if ispdf: SetPageProp(page, '_page', page - PageCount)
6113            title = GetFileProp(name, 'title')
6114            if title: SetPageProp(page, '_title', title)
6115        SetFileProp(name, 'pages', GetFileProp(name, 'pages', []) + pagerange)
6116        SetFileProp(name, 'offsets', GetFileProp(name, 'offsets', []) + [PageCount])
6117        if not GetFileProp(name, 'stat'): SetFileProp(name, 'stat', my_stat(name))
6118        if ispdf:
6119            SetFileProp(name, 'out', out)
6120            SetFileProp(name, 'res', res)
6121        PageCount += pages
6122
6123    # no pages? strange ...
6124    if not PageCount:
6125        print("The presentation doesn't have any pages, quitting.", file=sys.stderr)
6126        sys.exit(1)
6127
6128    # if rendering is wanted, do it NOW
6129    if RenderToDirectory:
6130        sys.exit(DoRender())
6131
6132    # load and execute info script
6133    if not InfoScriptPath:
6134        InfoScriptPath = FileName + ".info"
6135    LoadInfoScript()
6136
6137    # initialize some derived variables
6138    BoxFadeDarknessBase = BoxFadeDarkness
6139    BoxZoomDarknessBase = BoxZoomDarkness
6140    SpotRadiusBase = SpotRadius
6141
6142    # get the initial page number
6143    if not InitialPage:
6144        InitialPage = GetNextPage(0, 1)
6145    Pcurrent = InitialPage
6146    if (Pcurrent <= 0) or (Pcurrent > PageCount):
6147        print("Attempt to start the presentation at an invalid page (%d of %d), quitting." % (InitialPage, PageCount), file=sys.stderr)
6148        sys.exit(1)
6149
6150    # initialize graphics
6151    try:
6152        Platform.StartDisplay()
6153    except Exception as e:
6154        print("FATAL: failed to create rendering surface in the desired resolution (%dx%d)" % (ScreenWidth, ScreenHeight), file=sys.stderr)
6155        print("       detailed error message:", e, file=sys.stderr)
6156        sys.exit(1)
6157    if Fullscreen:
6158        Platform.SetMouseVisible(False)
6159        CursorVisible = False
6160    if (Gamma != 1.0) or (BlackLevel != 0):
6161        SetGamma(force=True)
6162
6163    # initialize OpenGL
6164    try:
6165        gl = Platform.LoadOpenGL()
6166        print("OpenGL renderer:", GLRenderer, file=sys.stderr)
6167
6168        # check if graphics are unaccelerated
6169        renderer = GLRenderer.lower().replace(' ', '').replace('(r)', '')
6170        if not(renderer) \
6171        or (renderer in ("mesaglxindirect", "gdigeneric")) \
6172        or renderer.startswith("software") \
6173        or ("llvmpipe" in renderer):
6174            print("WARNING: Using an OpenGL software renderer. Impressive will work, but it will", file=sys.stderr)
6175            print("         very likely be too slow to be usable.", file=sys.stderr)
6176
6177        # check for old hardware that can't deal with the blur shader
6178        for substr in ("i915", "intel915", "intel945", "intelq3", "intelg3", "inteligd", "gma900", "gma950", "gma3000", "gma3100", "gma3150"):
6179            if substr in renderer:
6180                UseBlurShader = False
6181
6182        # check the OpenGL version (2.0 needed to ensure NPOT texture support)
6183        extensions = set((gl.GetString(gl.EXTENSIONS) or "").split())
6184        if (GLVersion < "2") and (not("GL_ARB_shader_objects" in extensions) or not("GL_ARB_texture_non_power_of_two" in extensions)):
6185            raise ImportError("OpenGL version %r is below 2.0 and the necessary extensions are unavailable" % GLVersion)
6186    except ImportError as e:
6187        if GLVendor: print("OpenGL vendor:", GLVendor, file=sys.stderr)
6188        if GLRenderer: print("OpenGL renderer:", GLRenderer, file=sys.stderr)
6189        if GLVersion: print("OpenGL version:", GLVersion, file=sys.stderr)
6190        print("FATAL:", e, file=sys.stderr)
6191        print("This likely means that your graphics driver or hardware is too old.", file=sys.stderr)
6192        sys.exit(1)
6193
6194    # some further OpenGL configuration
6195    if Verbose:
6196        GLShader.LOG_DEFAULT = GLShader.LOG_IF_NOT_EMPTY
6197    for shader in RequiredShaders:
6198        shader.get_instance()
6199    if UseBlurShader:
6200        try:
6201            BlurShader.get_instance()
6202        except GLShaderCompileError:
6203            UseBlurShader = False
6204    if Verbose:
6205        if UseBlurShader:
6206            print("Using blur-and-desaturate shader for highlight box and spotlight mode.", file=sys.stderr)
6207        else:
6208            print("Using legacy multi-pass blur for highlight box and spotlight mode.", file=sys.stderr)
6209    gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
6210    BoxIndexBuffer = HighlightIndexBuffer(4)
6211
6212    # set up the OpenGL texture size (identical to the screen size because we
6213    # require non-power-of-two texture support by now)
6214    gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
6215    TexWidth  = ScreenWidth
6216    TexHeight = ScreenHeight
6217    TexMaxS = 1.0
6218    TexMaxT = 1.0
6219    TexSize = TexWidth * TexHeight * 3
6220
6221    # determine maximum texture size
6222    maxsize = c_int(0)
6223    gl.GetIntegerv(gl.MAX_TEXTURE_SIZE, ctypes.byref(maxsize))
6224    maxsize = float(maxsize.value)
6225    if (maxsize > ScreenWidth) and (maxsize <= 65536):
6226        MaxZoomFactor = min(MaxZoomFactor, maxsize / ScreenWidth, maxsize / ScreenHeight)
6227    if Verbose:
6228        print("Maximum texture size is %.0f pixels, using maximum zoom level of %.1f." % (maxsize, MaxZoomFactor), file=sys.stderr)
6229
6230    # set up some variables
6231    PixelX = 1.0 / ScreenWidth
6232    PixelY = 1.0 / ScreenHeight
6233    ScreenAspect = float(ScreenWidth) / float(ScreenHeight)
6234
6235    # prepare logo image
6236    LogoImage = Image.open(io.BytesIO(codecs.decode(LOGO, 'base64')))
6237    LogoTexture = gl.make_texture(gl.TEXTURE_2D, filter=gl.NEAREST, img=LogoImage)
6238    DrawLogo()
6239    Platform.SwapBuffers()
6240
6241    # initialize OSD font
6242    try:
6243        OSDFont = GLFont(FontTextureWidth, FontTextureHeight, FontList, FontSize, search_path=FontPath)
6244        DrawLogo()
6245        titles = []
6246        for key in ('title', '_title'):
6247            titles.extend([p[key] for p in PageProps.values() if key in p])
6248        if titles:
6249            OSDFont.AddString("".join(titles))
6250    except ValueError:
6251        print("The OSD font size is too large, the OSD will be rendered incompletely.", file=sys.stderr)
6252    except IOError:
6253        print("Could not open OSD font file, disabling OSD.", file=sys.stderr)
6254    except (NameError, AttributeError, TypeError):
6255        print("Your version of PIL is too old or incomplete, disabling OSD.", file=sys.stderr)
6256
6257    # handle event test mode
6258    if EventTestMode:
6259        DoEventTestMode()
6260
6261    # initialize mouse cursor
6262    if EnableCursor and (CursorImage or not(Platform.has_hardware_cursor)):
6263        img = None
6264        if CursorImage and not(CursorImage.lower() in ("-", "default")):
6265            try:
6266                img = Image.open(CursorImage).convert('RGBA')
6267                img.load()
6268            except:
6269                print("Could not open the mouse cursor image, using standard cursor.", file=sys.stderr)
6270                img = None
6271        CursorImage = PrepareCustomCursor(img)
6272    else:
6273        CursorImage = None
6274
6275    # set up page cache
6276    if CacheMode == PersistentCache:
6277        if not CacheFileName:
6278            CacheFileName = FileName + ".cache"
6279        InitPCache()
6280    if CacheMode == FileCache:
6281        CacheFile = tempfile.TemporaryFile(prefix="impressive-", suffix=".cache")
6282
6283    # overview preparations
6284    if EnableOverview:
6285        # initialize overview metadata
6286        OverviewPageMap=[i for i in range(1, PageCount + 1) \
6287            if GetPageProp(i, ('overview', '_overview'), True) \
6288            and (i >= PageRangeStart) and (i <= PageRangeEnd)]
6289        OverviewPageCount = max(len(OverviewPageMap), 1)
6290        OverviewPageMapInv = {}
6291        for page in range(1, PageCount + 1):
6292            OverviewPageMapInv[page] = len(OverviewPageMap) - 1
6293            for i in range(len(OverviewPageMap)):
6294                if OverviewPageMap[i] >= page:
6295                    OverviewPageMapInv[page] = i
6296                    break
6297
6298        # initialize overview page geometry
6299        OverviewGridSize = 1
6300        while OverviewPageCount > OverviewGridSize * OverviewGridSize:
6301            OverviewGridSize += 1
6302        if HalfScreen:
6303            # in half-screen mode, temporarily override ScreenWidth
6304            saved_screen_width = ScreenWidth
6305            ScreenWidth //= 2
6306        OverviewCellX = ScreenWidth  // OverviewGridSize
6307        OverviewCellY = ScreenHeight // OverviewGridSize
6308        OverviewOfsX = (ScreenWidth  - OverviewCellX * OverviewGridSize) // 2
6309        OverviewOfsY = int((ScreenHeight - OverviewCellY * \
6310                       int((OverviewPageCount + OverviewGridSize - 1) / OverviewGridSize)) / 2)
6311        while OverviewBorder and (min(OverviewCellX - 2 * OverviewBorder, OverviewCellY - 2 * OverviewBorder) < 16):
6312            OverviewBorder -= 1
6313        OverviewImage = Image.new('RGB', (TexWidth, TexHeight))
6314        if HalfScreen:
6315            OverviewOfsX += ScreenWidth
6316            ScreenWidth = saved_screen_width
6317
6318        # fill overlay "dummy" images
6319        dummy = LogoImage.copy()
6320        border = max(OverviewLogoBorder, 2 * OverviewBorder)
6321        maxsize = (OverviewCellX - border, OverviewCellY - border)
6322        if (dummy.size[0] > maxsize[0]) or (dummy.size[1] > maxsize[1]):
6323            size = ZoomToFit(dummy.size, maxsize, force_int=True)
6324            if min(size) > 0:
6325                dummy.thumbnail(size, Image.ANTIALIAS)
6326            else:
6327                dummy = None
6328        if dummy:
6329            margX = (OverviewCellX - dummy.size[0]) // 2
6330            margY = (OverviewCellY - dummy.size[1]) // 2
6331            dummy = dummy.convert(mode='RGB')
6332            for page in range(OverviewPageCount):
6333                pos = OverviewPos(page)
6334                OverviewImage.paste(dummy, (pos[0] + margX, pos[1] + margY))
6335            del dummy
6336
6337    # compute auto-advance timeout, if applicable
6338    if EstimatedDuration and AutoAutoAdvance:
6339        time_left = EstimatedDuration * 1000
6340        pages = 0
6341        p = InitialPage
6342        while p:
6343            override = GetPageProp(p, 'timeout')
6344            if override:
6345                time_left -= override
6346            else:
6347                pages += 1
6348            pnext = GetNextPage(p, 1)
6349            if pnext:
6350                time_left -= GetPageProp(p, 'transtime', TransitionDuration)
6351            p = pnext
6352        if pages and (time_left >= pages):
6353            AutoAdvanceTime = time_left // pages
6354            AutoAdvanceEnabled = True
6355            print("Setting auto-advance timeout to %.1f seconds." % (0.001 * AutoAdvanceTime), file=sys.stderr)
6356        else:
6357            print("Warning: Could not determine auto-advance timeout automatically.", file=sys.stderr)
6358
6359    # set up background rendering
6360    if not HaveThreads:
6361        print("Note: Background rendering isn't available on this platform.", file=sys.stderr)
6362        BackgroundRendering = False
6363
6364    # if caching is enabled, pre-render all pages
6365    if CacheMode and not(BackgroundRendering):
6366        DrawLogo()
6367        DrawProgress(0.0)
6368        Platform.SwapBuffers()
6369        for pdf in FileProps:
6370            if pdf.lower().endswith(".pdf"):
6371                ParsePDF(pdf)
6372        stop = False
6373        progress = 0.0
6374        def prerender_action_handler(action):
6375            if action in ("$quit", "*quit"):
6376                Quit()
6377        for page in list(range(InitialPage, PageCount + 1)) + list(range(1, InitialPage)):
6378            while True:
6379                ev = Platform.GetEvent(poll=True)
6380                if not ev: break
6381                ProcessEvent(ev, prerender_action_handler)
6382                if ev.startswith('*'):
6383                    stop = True
6384            if stop: break
6385            if (page >= PageRangeStart) and (page <= PageRangeEnd):
6386                PageImage(page)
6387            DrawLogo()
6388            progress += 1.0 / PageCount
6389            DrawProgress(progress)
6390            Platform.SwapBuffers()
6391
6392    # create buffer textures
6393    DrawLogo()
6394    Platform.SwapBuffers()
6395    Tcurrent, Tnext = [gl.make_texture(gl.TEXTURE_2D, gl.CLAMP_TO_EDGE, gl.LINEAR) for dummy in (1,2)]
6396
6397    # prebuffer current and next page
6398    Pnext = 0
6399    RenderPage(Pcurrent, Tcurrent)
6400    if not FadeInOut:
6401        DrawCurrentPage()
6402    PageEntered(update_time=False)
6403    PreloadNextPage(GetNextPage(Pcurrent, 1))
6404
6405    # some other preparations
6406    PrepareTransitions()
6407    GenerateSpotMesh()
6408    if PollInterval:
6409        Platform.ScheduleEvent("$poll-file", PollInterval * 1000, periodic=True)
6410
6411    # start the background rendering thread
6412    if CacheMode and BackgroundRendering:
6413        RTrunning = True
6414        thread.start_new_thread(RenderThread, (Pcurrent, Pnext))
6415
6416    # parse PDF file if caching is disabled
6417    if not CacheMode:
6418        for pdf in FileProps:
6419            if pdf.lower().endswith(".pdf"):
6420                SafeCall(ParsePDF, [pdf])
6421
6422    # start output and enter main loop
6423    StartTime = Platform.GetTicks()
6424    if TimeTracking or TimeDisplay:
6425        EnableTimeTracking(True)
6426    Platform.ScheduleEvent("$timer-update", 100, periodic=True)
6427    if not(Fullscreen) and (not(EnableCursor) or CursorImage):
6428        Platform.SetMouseVisible(False)
6429    if FadeInOut:
6430        LeaveFadeMode()
6431    else:
6432        DrawCurrentPage()
6433    UpdateCaption(Pcurrent)
6434    EventHandlerLoop()  # never returns
6435
6436
6437# event test mode implementation
6438def DoEventTestMode():
6439    last_event = "(None)"
6440    need_redraw = True
6441    cx = ScreenWidth // 2
6442    y1 = ScreenHeight // 5
6443    y2 = (ScreenHeight * 4) // 5
6444    if OSDFont:
6445        dy = OSDFont.GetLineHeight()
6446    Platform.ScheduleEvent('$dummy', 1000)  # required to ensure that time measurement works :(
6447    print("Entering Event Test Mode.", file=sys.stderr)
6448    print(" timestamp | delta-time | event")
6449    t0 = Platform.GetTicks()
6450    while True:
6451        if need_redraw:
6452            DrawLogo()
6453            if OSDFont:
6454                gl.Enable(gl.BLEND)
6455                OSDFont.BeginDraw()
6456                OSDFont.Draw((cx, y1 - dy), "Event Test Mode", align=Center, beveled=False, bold=True)
6457                OSDFont.Draw((cx, y1), "press Alt+F4 to quit", align=Center, beveled=False)
6458                OSDFont.Draw((cx, y2 - dy), "Last Event:", align=Center, beveled=False, bold=True)
6459                OSDFont.Draw((cx, y2), last_event, align=Center, beveled=False)
6460                OSDFont.EndDraw()
6461                gl.Disable(gl.BLEND)
6462            Platform.SwapBuffers()
6463            need_redraw = False
6464        ev = Platform.GetEvent()
6465        if ev == '$expose':
6466            need_redraw = True
6467        elif ev == '$quit':
6468            Quit()
6469        elif ev and ev.startswith('*'):
6470            now = Platform.GetTicks()
6471            print("%7d ms | %7d ms | %s" % (int(now), int(now - t0), ev[1:]))
6472            t0 = now
6473            last_event = ev[1:]
6474            need_redraw = True
6475
6476
6477# wrapper around main() that ensures proper uninitialization
6478def run_main():
6479    global CacheFile
6480    try:
6481        try:
6482            main()
6483        except SystemExit:
6484            raise
6485        except KeyboardInterrupt:
6486            pass
6487        except:
6488            print(file=sys.stderr)
6489            print(79 * "=", file=sys.stderr)
6490            print("OOPS! Impressive crashed!", file=sys.stderr)
6491            print("This shouldn't happen. Please report this incident to the author, including the", file=sys.stderr)
6492            print("full output of the program, particularly the following lines. If possible,", file=sys.stderr)
6493            print("please also send the input files you used.", file=sys.stderr)
6494            print(file=sys.stderr)
6495            print("Impressive version:", __version__, file=sys.stderr)
6496            print("Python version:", sys.version, file=sys.stderr)
6497            print("PyGame version:", pygame.__version__, file=sys.stderr)
6498            if hasattr(Image, "__version__"):  # Pillow >= 5.2
6499                print("PIL version: Pillow", Image.__version__, file=sys.stderr)
6500            elif hasattr(Image, "PILLOW_VERSION"):  # Pillow < 7.0
6501                print("PIL version: Pillow", Image.PILLOW_VERSION, file=sys.stderr)
6502            elif hasattr(Image, "VERSION"):  # classic PIL or Pillow 1.x
6503                print("PIL version: classic", Image.VERSION, file=sys.stderr)
6504            else:
6505                print("PIL version: unknown", file=sys.stderr)
6506            if PDFRenderer:
6507                print("PDF renderer:", PDFRenderer.name, file=sys.stderr)
6508            else:
6509                print("PDF renderer: None", file=sys.stderr)
6510            if GLVendor: print("OpenGL vendor:", GLVendor, file=sys.stderr)
6511            if GLRenderer: print("OpenGL renderer:", GLRenderer, file=sys.stderr)
6512            if GLVersion: print("OpenGL version:", GLVersion, file=sys.stderr)
6513            if hasattr(os, 'uname'):
6514                uname = os.uname()
6515                print("Operating system: %s %s (%s)" % (uname[0], uname[2], uname[4]), file=sys.stderr)
6516            else:
6517                print("Python platform:", sys.platform, file=sys.stderr)
6518            if os.path.isfile("/usr/bin/lsb_release"):
6519                lsb_release = Popen(["/usr/bin/lsb_release", "-sd"], stdout=subprocess.PIPE)
6520                print("Linux distribution:", lsb_release.stdout.read().decode().strip(), file=sys.stderr)
6521                lsb_release.wait()
6522            if basestring != str:
6523                cmdline = b' '.join((b'"%s"'%arg if (b' ' in arg) else arg) for arg in sys.argv)
6524            else:
6525                cmdline = ' '.join(('"%s"'%arg if (' ' in arg) else arg) for arg in sys.argv)
6526            print("Command line:", cmdline, file=sys.stderr)
6527            traceback.print_exc(file=sys.stderr)
6528    finally:
6529        StopMPlayer()
6530        # ensure that background rendering is halted
6531        Lrender.acquire()
6532        Lcache.acquire()
6533        # remove all temp files
6534        if 'CacheFile' in globals():
6535            del CacheFile
6536        if TempFileName:
6537            for tmp in glob.glob(TempFileName + "*"):
6538                try:
6539                    os.remove(tmp)
6540                except OSError:
6541                    pass
6542        Platform.Quit()
6543
6544    # release all locks
6545    try:
6546        if Lrender.locked():
6547            Lrender.release()
6548    except:
6549        pass
6550    try:
6551        if Lcache.locked():
6552            Lcache.release()
6553    except:
6554        pass
6555    try:
6556        if Loverview.locked():
6557            Loverview.release()
6558    except:
6559        pass
6560
6561
6562##### COMMAND-LINE PARSER AND HELP #############################################
6563
6564def if_op(cond, res_then, res_else):
6565    if cond: return res_then
6566    else:    return res_else
6567
6568def HelpExit(code=0):
6569    print("""A nice presentation tool.
6570
6571Usage: """+os.path.basename(sys.argv[0])+""" [OPTION...] <INPUT(S)...>
6572
6573Inputs may be PDF files, image files, video files or directories
6574containing image files.
6575
6576Input options:
6577  -h,  --help             show this help text and exit
6578  -r,  --rotate <n>       rotate pages clockwise in 90-degree steps
6579       --scale            scale images to fit screen (not used in PDF mode)
6580       --supersample      use supersampling (only used in PDF mode)
6581  -s                      --supersample for PDF files, --scale for image files
6582  -I,  --script <path>    set the path of the info script
6583  -u,  --poll <seconds>   check periodically if the source files have been
6584                          updated and reload them if they did
6585  -X,  --shuffle          put input files into random order
6586
6587Output options:
6588       --fullscreen       start in fullscreen mode
6589  -ff, --fake-fullscreen  start in "fake fullscreen" mode
6590  -f,  --windowed         start in windowed mode
6591  -g,  --geometry <WxH>   set window size or fullscreen resolution
6592  -A,  --aspect <X:Y>     adjust for a specific display aspect ratio (e.g. 5:4)
6593  -G,  --gamma <G[:BL]>   specify startup gamma and black level
6594  -o,  --output <dir>     don't display the presentation, only render to .png
6595
6596Page options:
6597  -i,  --initialpage <n>  start with page <n>
6598  -p,  --pages <A-B>      only cache pages in the specified range;
6599                          implicitly sets -i <A>
6600  -w,  --wrap             go back to the first page after the last page
6601  -O,  --autooverview <x> automatically derive page visibility on overview page
6602                            -O first = show pages with captions
6603                            -O last  = show pages before pages with captions
6604  -Q,  --autoquit         quit after the last slide (no effect with --wrap)
6605       --nooverview       disable overview page
6606
6607Display options:
6608  -t,  --transition <trans[,trans2...]>
6609                          force a specific transitions or set of transitions
6610  -l,  --listtrans        print a list of available transitions and exit
6611  -F,  --font <file>      use a specific TrueType font file for the OSD
6612  -S,  --fontsize <px>    specify the OSD font size in pixels
6613  -C,  --cursor <F[:X,Y]> use a .png image as the mouse cursor
6614  -N,  --nocursor         don't show a mouse cursor at all
6615  -L,  --layout <spec>    set the OSD layout (please read the documentation)
6616  -z,  --zoom <factor>    set zoom factor (default: 2.0)
6617       --maxzoom <factor> maximum factor to render high-resolution zoom
6618  -x,  --fade             fade in at start and fade out at end
6619       --invert           display slides in inverted colors
6620       --noblur           use legacy blur implementation
6621       --spot-radius <px> set the initial radius of the spotlight, in pixels
6622       --min-box-size <x> set minimum size of a highlight box, in pixels
6623       --box-edge <px>    size of highlight box borders, in pixels
6624       --zbox-edge <px>   size of zoom box borders, in pixels
6625       --darkness <p>     set highlight box mode darkness to <p> percent
6626       --zoomdarkness <p> set box-zoom mode darkness to <p> percent
6627
6628Timing options:
6629  -a,  --auto <seconds>   automatically advance to next page after some seconds
6630  -d,  --duration <time>  set the desired duration of the presentation and show
6631                          a progress bar at the bottom of the screen
6632  -y,  --auto-auto        if a duration is set, set the default time-out so
6633                          that it will be reached exactly
6634  -q,  --page-progress    shows a progress bar based on the position in the
6635                          presentation (based on pages, not time)
6636       --progress-last    set the last page for --page-progress
6637  -k,  --auto-progress    shows a progress bar for each page for auto-advance
6638       --time-display     enable time display (implies --tracking)
6639       --tracking         enable time tracking mode
6640       --clock            show current time instead of time elapsed
6641  -M,  --minutes          display time in minutes, not seconds
6642  -T,  --transtime <ms>   set transition duration in milliseconds
6643  -D,  --mousedelay <ms>  set mouse hide delay for fullscreen mode (in ms)
6644                          (0 = show permanently, 1 = don't show at all)
6645  -B,  --boxfade <ms>     set highlight box fade duration in milliseconds
6646  -Z,  --zoomtime <ms>    set zoom and overview animation time in milliseconds
6647       --overtime <ms>    set only overview animation duration in milliseconds
6648
6649Control options:
6650       --control-help     display help about control configuration and exit
6651  -e,  --bind             set controls (modify event/action bindings)
6652  -E,  --controls <file>  load control configuration from a file
6653       --noclicks         disable page navigation via left/right mouse click
6654  -W,  --nowheel          disable page navigation via mouse wheel, zoom instead
6655       --noquit           disable single-key shortcuts that quit the program
6656       --evtest           run Impressive in event test mode
6657
6658Advanced options:
6659       --bare             don't use any special features (hyperlinks etc.)
6660  -c,  --cache <mode>     set page cache mode:
6661                            -c none       = disable caching completely
6662                            -c memory     = store cache in RAM, uncompressed
6663                            -c compressed = store cache in RAM, compressed
6664                            -c disk       = store cache on disk temporarily
6665                            -c persistent = store cache on disk persistently
6666       --cachefile <path> set the persistent cache file path (implies -cp)
6667  -b,  --noback           don't pre-render images in the background
6668  -P,  --renderer <path>  set path to PDF renderer executable (GhostScript,
6669                          Xpdf/Poppler pdftoppm, or MuPDF mudraw/pdfdraw)
6670  -V,  --overscan <px>    render PDF files <px> pixels larger than the screen
6671  -H,  --half-screen      show OSD on right half of the screen only
6672       --nologo           disable startup logo and version number display
6673  -v,  --verbose          (slightly) more verbose operation
6674
6675For detailed information, visit""", __website__)
6676    sys.exit(code)
6677
6678def ListTransitions():
6679    print("Available transitions:")
6680    standard = dict([(tc.__name__, None) for tc in AvailableTransitions])
6681    trans = [(tc.__name__, tc.__doc__) for tc in AllTransitions]
6682    trans.append(('None', "no transition"))
6683    trans.sort()
6684    maxlen = max([len(item[0]) for item in trans])
6685    for name, desc in trans:
6686        if name in standard:
6687            star = '*'
6688        else:
6689            star = ' '
6690        print(star, name.ljust(maxlen), '-', desc)
6691    print("(transitions with * are enabled by default)")
6692    sys.exit(0)
6693
6694def TryTime(s, regexp, func):
6695    m = re.match(regexp, s, re.I)
6696    if not m: return 0
6697    return func(list(map(int, m.groups())))
6698def ParseTime(s):
6699    return TryTime(s, r'([0-9]+)s?$', lambda m: m[0]) \
6700        or TryTime(s, r'([0-9]+)m$', lambda m: m[0] * 60) \
6701        or TryTime(s, r'([0-9]+)[m:]([0-9]+)[ms]?$', lambda m: m[0] * 60 + m[1]) \
6702        or TryTime(s, r'([0-9]+)[h:]([0-9]+)[hm]?$', lambda m: m[0] * 3600 + m[1] * 60) \
6703        or TryTime(s, r'([0-9]+)[h:]([0-9]+)[m:]([0-9]+)s?$', lambda m: m[0] * 3600 + m[1] * 60 + m[2])
6704
6705def opterr(msg, extra_lines=[]):
6706    print("command line parse error:", msg, file=sys.stderr)
6707    for line in extra_lines:
6708        print(line, file=sys.stderr)
6709    print("use `%s -h' to get help" % sys.argv[0], file=sys.stderr)
6710    print("or visit", __website__, "for full documentation", file=sys.stderr)
6711    sys.exit(2)
6712
6713def SetTransitions(list):
6714    global AvailableTransitions
6715    index = dict([(tc.__name__.lower(), tc) for tc in AllTransitions])
6716    index['none'] = None
6717    AvailableTransitions=[]
6718    for trans in list.split(','):
6719        try:
6720            AvailableTransitions.append(index[trans.lower()])
6721        except KeyError:
6722            opterr("unknown transition `%s'" % trans)
6723
6724def ParseLayoutPosition(value):
6725    xpos = []
6726    ypos = []
6727    for c in value.strip().lower():
6728        if   c == 't': ypos.append(0)
6729        elif c == 'b': ypos.append(1)
6730        elif c == 'l': xpos.append(0)
6731        elif c == 'r': xpos.append(1)
6732        elif c == 'c': xpos.append(2)
6733        else: opterr("invalid position specification `%s'" % value)
6734    if not xpos: opterr("position `%s' lacks X component" % value)
6735    if not ypos: opterr("position `%s' lacks Y component" % value)
6736    if len(xpos)>1: opterr("position `%s' has multiple X components" % value)
6737    if len(ypos)>1: opterr("position `%s' has multiple Y components" % value)
6738    return (xpos[0] << 1) | ypos[0]
6739def SetLayoutSubSpec(key, value):
6740    global OSDTimePos, OSDTitlePos, OSDPagePos, OSDStatusPos
6741    global OSDAlpha, OSDMargin
6742    lkey = key.strip().lower()
6743    if lkey in ('a', 'alpha', 'opacity'):
6744        try:
6745            OSDAlpha = float(value)
6746        except ValueError:
6747            opterr("invalid alpha value `%s'" % value)
6748        if OSDAlpha > 1.0:
6749            OSDAlpha *= 0.01  # accept percentages, too
6750        if (OSDAlpha < 0.0) or (OSDAlpha > 1.0):
6751            opterr("alpha value %s out of range" % value)
6752    elif lkey in ('margin', 'dist', 'distance'):
6753        try:
6754            OSDMargin = float(value)
6755        except ValueError:
6756            opterr("invalid margin value `%s'" % value)
6757        if OSDMargin < 0:
6758            opterr("margin value %s out of range" % value)
6759    elif lkey in ('t', 'time'):
6760        OSDTimePos = ParseLayoutPosition(value)
6761    elif lkey in ('title', 'caption'):
6762        OSDTitlePos = ParseLayoutPosition(value)
6763    elif lkey in ('page', 'number'):
6764        OSDPagePos = ParseLayoutPosition(value)
6765    elif lkey in ('status', 'info'):
6766        OSDStatusPos = ParseLayoutPosition(value)
6767    else:
6768        opterr("unknown layout element `%s'" % key)
6769def SetLayout(spec):
6770    for sub in spec.replace(':', '=').split(','):
6771        try:
6772            key, value = sub.split('=')
6773        except ValueError:
6774            opterr("invalid layout spec `%s'" % sub)
6775        SetLayoutSubSpec(key, value)
6776
6777def ParseCacheMode(arg):
6778    arg = arg.strip().lower()
6779    if "none".startswith(arg): return NoCache
6780    if "off".startswith(arg): return NoCache
6781    if "memory".startswith(arg): return MemCache
6782    if arg == 'z': return CompressedCache
6783    if "compressed".startswith(arg): return CompressedCache
6784    if "disk".startswith(arg): return FileCache
6785    if "file".startswith(arg): return FileCache
6786    if "persistent".startswith(arg): return PersistentCache
6787    opterr("invalid cache mode `%s'" % arg)
6788
6789def ParseAutoOverview(arg):
6790    arg = arg.strip().lower()
6791    if "off".startswith(arg): return Off
6792    if "first".startswith(arg): return First
6793    if "last".startswith(arg): return Last
6794    try:
6795        i = int(arg)
6796        assert (i >= Off) and (i <= Last)
6797    except:
6798        opterr("invalid auto-overview mode `%s'" % arg)
6799
6800def ParseOptions(argv):
6801    global FileName, FileList, Fullscreen, Scaling, Supersample, CacheMode
6802    global TransitionDuration, MouseHideDelay, BoxFadeDuration, ZoomDuration, OverviewDuration
6803    global ScreenWidth, ScreenHeight, InitialPage, Wrap, TimeTracking
6804    global AutoAdvanceTime, AutoAdvanceEnabled, AutoAutoAdvance
6805    global RenderToDirectory, Rotation, DAR, Verbose
6806    global BackgroundRendering, UseAutoScreenSize, PollInterval, CacheFileName
6807    global PageRangeStart, PageRangeEnd, FontList, FontSize, Gamma, BlackLevel
6808    global EstimatedDuration, CursorImage, CursorHotspot, MinutesOnly, Overscan
6809    global PDFRendererPath, InfoScriptPath, EventTestMode, EnableCursor
6810    global AutoOverview, DefaultZoomFactor, FadeInOut, ShowLogo, Shuffle
6811    global QuitAtEnd, ShowClock, HalfScreen, SpotRadius, InvertPages
6812    global MinBoxSize, AutoAdvanceProgress, BoxFadeDarkness
6813    global WindowPos, FakeFullscreen, UseBlurShader, Bare, EnableOverview
6814    global PageProgress, ProgressLast, BoxZoomDarkness, MaxZoomFactor, BoxEdgeSize
6815    global TimeDisplay, MouseWheelZoom, ZoomBoxEdgeSize
6816    DefaultControls = True
6817
6818    # on Python 2, ensure that all command-line strings are encoded properly
6819    if basestring != str:
6820        enc = sys.getfilesystemencoding()
6821        if enc in ('cp437', 'cp852'): enc = 'cp1252'  # work-around for latin Win32
6822        argv = [a.decode(enc, 'replace') for a in argv]
6823
6824    try:  # unused short options: jnJKRUY
6825        opts, args = getopt.getopt(argv,
6826            "vhfg:sc:i:wa:t:lo:r:T:D:B:Z:P:A:mbp:u:F:S:G:d:C:ML:I:O:z:xXqV:QHykWe:E:N",
6827           ["help", "fullscreen", "geometry=", "scale", "supersample",
6828            "nocache", "initialpage=", "wrap", "auto=", "listtrans", "output=",
6829            "rotate=", "transition=", "transtime=", "mousedelay=", "boxfade=",
6830            "zoom=", "gspath=", "renderer=", "aspect=", "memcache",
6831            "noback", "pages=", "poll=", "font=", "fontsize=", "gamma=",
6832            "duration=", "cursor=", "minutes", "layout=", "script=", "cache=",
6833            "cachefile=", "autooverview=", "zoomtime=", "overtime=", "fade", "nologo",
6834            "shuffle", "page-progress", "progress-last=", "overscan=", "autoquit", "noclicks",
6835            "clock", "half-screen", "spot-radius=", "invert", "min-box-size=",
6836            "auto-auto", "auto-progress", "darkness=", "no-clicks", "nowheel",
6837            "no-wheel", "fake-fullscreen", "windowed", "verbose", "noblur",
6838            "tracking", "bind=", "controls=", "control-help", "evtest",
6839            "noquit", "bare", "no-overview", "nooverview", "no-cursor",
6840            "nocursor", "zoomdarkness=", "zoom-darkness=", "box-edge=",
6841            "maxzoom=", "max-zoom=", "time-display", "zbox-edge=",
6842            "vht0=", "vht1="])
6843    except getopt.GetoptError as message:
6844        opterr(message)
6845
6846    for opt, arg in opts:
6847        if opt in ("-h", "--help"):
6848            HelpExit()
6849        if opt in ("-l", "--listtrans"):
6850            ListTransitions()
6851        if opt in ("-v", "--verbose"):
6852            Verbose = not(Verbose)
6853        if opt == "--fullscreen":      Fullscreen, FakeFullscreen = True,  False
6854        if opt == "--fake-fullscreen": Fullscreen, FakeFullscreen = True,  True
6855        if opt == "--windowed":        Fullscreen, FakeFullscreen = False, False
6856        if opt == "-f":
6857            if FakeFullscreen: Fullscreen, FakeFullscreen = True,  False
6858            elif   Fullscreen: Fullscreen, FakeFullscreen = False, False
6859            else:              Fullscreen, FakeFullscreen = True,  True
6860        if opt in ("-s", "--scale"):
6861            Scaling = not(Scaling)
6862        if opt in ("-s", "--supersample"):
6863            Supersample = 2
6864        if opt in ("-w", "--wrap"):
6865            Wrap = not(Wrap)
6866        if opt in ("-x", "--fade"):
6867            FadeInOut = not(FadeInOut)
6868        if opt in ("-O", "--autooverview"):
6869            AutoOverview = ParseAutoOverview(arg)
6870        if opt in ("-c", "--cache"):
6871            CacheMode = ParseCacheMode(arg)
6872        if opt == "--nocache":
6873            print("Note: The `--nocache' option is deprecated, use `--cache none' instead.", file=sys.stderr)
6874            CacheMode = NoCache
6875        if opt in ("-m", "--memcache"):
6876            print("Note: The `--memcache' option is deprecated, use `--cache memory' instead.", file=sys.stderr)
6877            CacheMode = MemCache
6878        if opt == "--cachefile":
6879            CacheFileName = arg
6880            CacheMode = PersistentCache
6881        if opt in ("-M", "--minutes"):
6882            MinutesOnly = not(MinutesOnly)
6883        if opt in ("-b", "--noback"):
6884            BackgroundRendering = not(BackgroundRendering)
6885        if opt in ("-t", "--transition"):
6886            SetTransitions(arg)
6887        if opt in ("-L", "--layout"):
6888            SetLayout(arg)
6889        if opt in ("-o", "--output"):
6890            RenderToDirectory = arg
6891        if opt in ("-I", "--script"):
6892            InfoScriptPath = arg
6893        if opt in ("-F", "--font"):
6894            FontList = [arg]
6895        if opt == "--nologo":
6896            ShowLogo = not(ShowLogo)
6897        if opt in ("--noclicks", "--no-clicks"):
6898            if not DefaultControls:
6899                print("Note: The default control settings have been modified, the `--noclicks' option might not work as expected.", file=sys.stderr)
6900            BindEvent("lmb, rmb, ctrl+lmb, ctrl+rmb -= goto-next, goto-prev, goto-next-notrans, goto-prev-notrans")
6901        if opt in ("-W", "--nowheel", "--no-wheel"):
6902            if not DefaultControls:
6903                print("Note: The default control settings have been modified, the `--nowheel' option might not work as expected.", file=sys.stderr)
6904            BindEvent("wheelup, wheeldown, ctrl+wheelup, ctrl+wheeldown -= goto-next, goto-prev, goto-next-notrans, goto-prev-notrans, overview-next, overview-prev")
6905            MouseWheelZoom = True
6906        if opt in ("--noquit", "--no-quit"):
6907            if not DefaultControls:
6908                print("Note: The default control settings have been modified, the `--noquit' option might not work as expected.", file=sys.stderr)
6909            BindEvent("q,escape -= quit")
6910        if opt in ("-e", "--bind"):
6911            BindEvent(arg, error_prefix="--bind")
6912            DefaultControls = False
6913        if opt in ("-E", "--controls"):
6914            ParseInputBindingFile(arg)
6915            DefaultControls = False
6916        if opt in ("--control-help", "--event-help"):
6917            EventHelp()
6918            sys.exit(0)
6919        if opt == "--evtest":
6920            EventTestMode = not(EventTestMode)
6921        if opt == "--clock":
6922            ShowClock = not(ShowClock)
6923        if opt == "--tracking":
6924            TimeTracking = not(TimeTracking)
6925        if opt == "--time-display":
6926            TimeDisplay = not(TimeDisplay)
6927        if opt in ("-X", "--shuffle"):
6928            Shuffle = not(Shuffle)
6929        if opt in ("-Q", "--autoquit"):
6930            QuitAtEnd = not(QuitAtEnd)
6931        if opt in ("-y", "--auto-auto"):
6932            AutoAutoAdvance = not(AutoAutoAdvance)
6933        if opt in ("-k", "--auto-progress"):
6934            AutoAdvanceProgress = not(AutoAdvanceProgress)
6935        if opt in ("-q", "--page-progress"):
6936            PageProgress = not(PageProgress)
6937        if opt in ("-H", "--half-screen"):
6938            HalfScreen = not(HalfScreen)
6939            if HalfScreen:
6940                OverviewDuration = 0
6941        if opt == "--invert":
6942            InvertPages = not(InvertPages)
6943        if opt in ("-P", "--gspath", "--renderer"):
6944            if any(r.supports(arg) for r in AvailableRenderers):
6945                PDFRendererPath = arg
6946            else:
6947                opterr("unrecognized --renderer",
6948                    ["supported renderer binaries are:"] +
6949                    ["- %s (%s)" % (", ".join(r.binaries), r.name) for r in AvailableRenderers])
6950        if opt in ("-S", "--fontsize"):
6951            try:
6952                FontSize = int(arg)
6953                assert FontSize > 0
6954            except:
6955                opterr("invalid parameter for --fontsize")
6956        if opt in ("-i", "--initialpage"):
6957            try:
6958                InitialPage = int(arg)
6959                assert InitialPage > 0
6960            except:
6961                opterr("invalid parameter for --initialpage")
6962        if opt in ("-d", "--duration"):
6963            try:
6964                EstimatedDuration = ParseTime(arg)
6965                assert EstimatedDuration > 0
6966            except:
6967                opterr("invalid parameter for --duration")
6968        if opt in ("-a", "--auto"):
6969            try:
6970                if arg.lower().strip('.') in ("0", "00", "off", "none", "false"):
6971                    AutoAdvanceEnabled = False
6972                else:
6973                    AutoAdvanceTime = int(float(arg) * 1000)
6974                    assert (AutoAdvanceTime > 0) and (AutoAdvanceTime <= 86400000)
6975                    AutoAdvanceEnabled = True
6976            except:
6977                opterr("invalid parameter for --auto")
6978        if opt in ("-T", "--transtime"):
6979            try:
6980                TransitionDuration = int(arg)
6981                assert (TransitionDuration >= 0) and (TransitionDuration < 32768)
6982            except:
6983                opterr("invalid parameter for --transtime")
6984        if opt in ("-D", "--mousedelay"):
6985            try:
6986                MouseHideDelay = int(arg)
6987                assert (MouseHideDelay >= 0) and (MouseHideDelay < 32768)
6988            except:
6989                opterr("invalid parameter for --mousedelay")
6990        if opt in ("-B", "--boxfade"):
6991            try:
6992                BoxFadeDuration = int(arg)
6993                assert (BoxFadeDuration >= 0) and (BoxFadeDuration < 32768)
6994            except:
6995                opterr("invalid parameter for --boxfade")
6996        if opt in ("-Z", "--zoomtime"):
6997            try:
6998                ZoomDuration = OverviewDuration = int(arg)
6999                assert (ZoomDuration >= 0) and (ZoomDuration < 32768)
7000            except:
7001                opterr("invalid parameter for --zoomtime")
7002        if opt in ("--overtime"):
7003            try:
7004                OverviewDuration = int(arg)
7005                assert (OverviewDuration >= 0) and (OverviewDuration < 32768)
7006            except:
7007                opterr("invalid parameter for --overtime")
7008        if opt == "--spot-radius":
7009            try:
7010                SpotRadius = int(arg)
7011            except:
7012                opterr("invalid parameter for --spot-radius")
7013        if opt == "--min-box-size":
7014            try:
7015                MinBoxSize = int(arg)
7016            except:
7017                opterr("invalid parameter for --min-box-size")
7018        if opt == "--box-edge":
7019            try:
7020                BoxEdgeSize = int(arg)
7021            except:
7022                opterr("invalid parameter for --box-edge")
7023        if opt == "--zbox-edge":
7024            try:
7025                ZoomBoxEdgeSize = int(arg)
7026            except:
7027                opterr("invalid parameter for --zbox-edge")
7028        if opt in ("-r", "--rotate"):
7029            try:
7030                Rotation = int(arg)
7031            except:
7032                opterr("invalid parameter for --rotate")
7033            while Rotation < 0: Rotation += 4
7034            Rotation = Rotation & 3
7035        if opt in ("-u", "--poll"):
7036            try:
7037                PollInterval = int(arg)
7038                assert PollInterval >= 0
7039            except:
7040                opterr("invalid parameter for --poll")
7041        if opt in ("-g", "--geometry"):
7042            try:
7043                parts = arg.replace('+', '|+').replace('-', '|-').split('|')
7044                assert len(parts) in (1, 3)
7045                if len(parts) == 3:
7046                    WindowPos = (int(parts[1]), int(parts[2]))
7047                else:
7048                    assert len(parts) == 1
7049                ScreenWidth, ScreenHeight = map(int, parts[0].split("x"))
7050                assert (ScreenWidth  >= 320) and (ScreenWidth  < 32768)
7051                assert (ScreenHeight >= 200) and (ScreenHeight < 32768)
7052                UseAutoScreenSize = False
7053            except:
7054                opterr("invalid parameter for --geometry")
7055        if opt in ("-p", "--pages"):
7056            try:
7057                PageRangeStart, PageRangeEnd = map(int, arg.split("-"))
7058                assert PageRangeStart > 0
7059                assert PageRangeStart <= PageRangeEnd
7060            except:
7061                opterr("invalid parameter for --pages")
7062            InitialPage = PageRangeStart
7063        if opt == "--progress-last":
7064            try:
7065                ProgressLast = int(arg)
7066                assert ProgressLast > 0
7067            except:
7068                opterr("invalid parameter for --progress-last")
7069        if opt in ("-A", "--aspect"):
7070            try:
7071                if ':' in arg:
7072                    fx, fy = map(float, arg.split(':'))
7073                    DAR = fx / fy
7074                else:
7075                    DAR = float(arg)
7076                assert DAR > 0.0
7077            except:
7078                opterr("invalid parameter for --aspect")
7079        if opt in ("-G", "--gamma"):
7080            try:
7081                if ':' in arg:
7082                    arg, bl = arg.split(':', 1)
7083                    BlackLevel = int(bl)
7084                Gamma = float(arg)
7085                assert Gamma > 0.0
7086                assert (BlackLevel >= 0) and (BlackLevel < 255)
7087            except:
7088                opterr("invalid parameter for --gamma")
7089        if opt in ("-C", "--cursor"):
7090            try:
7091                if ':' in arg:
7092                    arg = arg.split(':')
7093                    assert len(arg) > 1
7094                    CursorImage = ':'.join(arg[:-1])
7095                    CursorHotspot = tuple(map(int, arg[-1].split(',')))
7096                else:
7097                    CursorImage = arg
7098                assert (BlackLevel >= 0) and (BlackLevel < 255)
7099            except:
7100                opterr("invalid parameter for --cursor")
7101        if opt in ("-z", "--zoom"):
7102            try:
7103                DefaultZoomFactor = float(arg)
7104                assert DefaultZoomFactor > 1
7105            except:
7106                opterr("invalid parameter for --zoom")
7107        if opt in ("--maxzoom", "--max-zoom"):
7108            try:
7109                MaxZoomFactor = float(arg)
7110                assert MaxZoomFactor >= 1.0
7111            except:
7112                opterr("invalid parameter for --maxzoom")
7113        if opt in ("-V", "--overscan"):
7114            try:
7115                Overscan = int(arg)
7116            except:
7117                opterr("invalid parameter for --overscan")
7118        if opt == "--darkness":
7119            try:
7120                BoxFadeDarkness = float(arg) * 0.01
7121            except:
7122                opterr("invalid parameter for --darkness")
7123        if opt in ("--zoom-darkness", "--zoomdarkness"):
7124            try:
7125                BoxZoomDarkness = float(arg) * 0.01
7126            except:
7127                opterr("invalid parameter for --zoom-darkness")
7128        if opt == "--noblur":
7129            UseBlurShader = not(UseBlurShader)
7130        if opt == "--bare":
7131            Bare = not(Bare)
7132        if opt in ("--no-overview", "--nooverview"):
7133            EnableOverview = not(EnableOverview)
7134        if opt in ("-N", "--no-cursor", "--nocursor"):
7135            EnableCursor = not(EnableCursor)
7136        if opt.startswith("--vht"):  # DEBUG OPTION ONLY
7137            Win32FullscreenVideoHackTiming[int(opt[5:])] = float(arg)
7138
7139    for arg in args:
7140        AddFile(arg)
7141    if not(FileList) and not(EventTestMode):
7142        opterr("no playable files specified")
7143
7144
7145# use this function if you intend to use Impressive as a library
7146def run():
7147    try:
7148        run_main()
7149    except SystemExit as e:
7150        return e.code
7151
7152# use this function if you use Impressive as a library and want to call any
7153# Impressive-internal function from a second thread
7154def synchronize(func, *args, **kwargs):
7155    CallQueue.append((func, args, kwargs))
7156    if Platform:
7157        Platform.ScheduleEvent("$call", 1)
7158
7159if __name__ == "__main__":
7160    try:
7161        ParseOptions(sys.argv[1:])
7162        run_main()
7163    finally:
7164        if not(CleanExit) and (os.name == 'nt') and getattr(sys, "frozen", False):
7165            print()
7166            raw_input("<-- press ENTER to quit the program --> ")
7167