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 { 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