Impressive-0.12.0/0000755000175000017500000000000013235666175012360 5ustar mfiemfieImpressive-0.12.0/impressive.py0000755000175000017500000100267713235666653015141 0ustar mfiemfie#!/usr/bin/env python2 # -*- coding: iso-8859-1 -*- # # Impressive, a fancy presentation tool # Copyright (C) 2005-2014 Martin J. Fiedler # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2, as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __title__ = "Impressive" __version__ = "0.12.0" __rev__ = 235 __author__ = "Martin J. Fiedler" __email__ = "martin.fiedler@gmx.net" __website__ = "http://impressive.sourceforge.net/" import sys if __rev__ and (("WIP" in __version__) or ("rc" in __version__) or ("alpha" in __version__) or ("beta" in __version__)): __version__ += " (SVN r%s)" % __rev__ def greet(): print >>sys.stderr, "Welcome to", __title__, "version", __version__ if __name__ == "__main__": greet() TopLeft, BottomLeft, TopRight, BottomRight, TopCenter, BottomCenter = range(6) NoCache, MemCache, CompressedCache, FileCache, PersistentCache = range(5) # for CacheMode Off, First, Last = range(3) # for AutoOverview # You may change the following lines to modify the default settings Verbose = False Fullscreen = True FakeFullscreen = False Scaling = False Supersample = None BackgroundRendering = True PDFRendererPath = None UseAutoScreenSize = True ScreenWidth = 1024 ScreenHeight = 768 WindowPos = None TransitionDuration = 1000 MouseHideDelay = 3000 BoxFadeDuration = 100 ZoomDuration = 250 BlankFadeDuration = 250 BoxFadeBlur = 1.5 BoxFadeDarkness = 0.25 BoxFadeDarknessStep = 0.02 BoxZoomDarkness = 0.96 MarkColor = (1.0, 0.0, 0.0, 0.1) BoxEdgeSize = 4 ZoomBoxEdgeSize = 1 SpotRadius = 64 MinSpotDetail = 13 SpotDetail = 12 CacheMode = FileCache HighQualityOverview = True OverviewBorder = 3 OverviewLogoBorder = 24 AutoOverview = Off EnableOverview = True InitialPage = None Wrap = False AutoAdvance = None AutoAutoAdvance = False RenderToDirectory = None Rotation = 0 DAR = None PAR = 1.0 Overscan = 3 PollInterval = 0 PageRangeStart = 0 PageRangeEnd = 999999 FontSize = 14 FontTextureWidth = 512 FontTextureHeight = 256 Gamma = 1.0 BlackLevel = 0 GammaStep = 1.1 BlackLevelStep = 8 EstimatedDuration = None PageProgress = False AutoAdvanceProgress = False ProgressBarSizeFactor = 0.02 ProgressBarAlpha = 0.5 ProgressBarColorNormal = (0.0, 1.0, 0.0) ProgressBarColorWarning = (1.0, 1.0, 0.0) ProgressBarColorCritical = (1.0, 0.0, 0.0) ProgressBarColorPage = (0.0, 0.5, 1.0) ProgressBarWarningFactor = 1.25 ProgressBarCriticalFactor = 1.5 EnableCursor = True CursorImage = None CursorHotspot = (0, 0) MinutesOnly = False OSDMargin = 16 OSDAlpha = 1.0 OSDTimePos = TopRight OSDTitlePos = BottomLeft OSDPagePos = BottomRight OSDStatusPos = TopLeft DefaultZoomFactor = 2 MaxZoomFactor = 5 MouseWheelZoom = False ZoomStep = 2.0 ** (1.0 / 4) WheelZoomDuration = 30 FadeInOut = False ShowLogo = True Shuffle = False QuitAtEnd = False ShowClock = False HalfScreen = False InvertPages = False MinBoxSize = 20 UseBlurShader = True TimeTracking = False EventTestMode = False Bare = False Win32FullscreenVideoHackTiming = [0, 0] # import basic modules import random, getopt, os, types, re, codecs, tempfile, glob, cStringIO, re import traceback, subprocess, time, itertools, ctypes.util, zlib, urllib from math import * from ctypes import * # import hashlib for MD5 generation, but fall back to old md5 lib if unavailable # (this is the case for Python versions older than 2.5) try: import hashlib md5obj = hashlib.md5 except ImportError: import md5 md5obj = md5.new # initialize some platform-specific settings if os.name == "nt": root = os.path.split(sys.argv[0])[0] or "." _find_paths = [root, os.path.join(root, "win32"), os.path.join(root, "gs")] + filter(None, os.getenv("PATH").split(';')) def FindBinary(binary): if not binary.lower().endswith(".exe"): binary += ".exe" for p in _find_paths: path = os.path.join(p, binary) if os.path.isfile(path): return os.path.abspath(path) return binary # fall-back if not found pdftkPath = FindBinary("pdftk.exe") mutoolPath = FindBinary("mutool.exe") ffmpegPath = FindBinary("ffmpeg.exe") GhostScriptPlatformOptions = ["-I" + os.path.join(root, "gs")] try: import win32api, win32gui HaveWin32API = True MPlayerPath = FindBinary("mplayer.exe") def RunURL(url): win32api.ShellExecute(0, "open", url, "", "", 0) except ImportError: HaveWin32API = False MPlayerPath = "" def RunURL(url): print "Error: cannot run URL `%s'" % url if getattr(sys, "frozen", False): sys.path.append(root) FontPath = [] FontList = ["verdana.ttf", "arial.ttf"] Nice = [] else: def FindBinary(x): return x GhostScriptPlatformOptions = [] MPlayerPath = "mplayer" pdftkPath = "pdftk" mutoolPath = "mutool" ffmpegPath = "ffmpeg" FontPath = ["/usr/share/fonts", "/usr/local/share/fonts", "/usr/X11R6/lib/X11/fonts/TTF"] FontList = ["DejaVuSans.ttf", "Vera.ttf", "Verdana.ttf"] Nice = ["nice", "-n", "7"] def RunURL(url): try: subprocess.Popen(["xdg-open", url]) except OSError: print >>sys.stderr, "Error: cannot open URL `%s'" % url # import special modules try: import pygame from pygame.locals import * from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageChops, ImageOps from PIL import TiffImagePlugin, BmpImagePlugin, JpegImagePlugin, PngImagePlugin, PpmImagePlugin except (ValueError, ImportError), err: print >>sys.stderr, "Oops! Cannot load necessary modules:", err print >>sys.stderr, """To use Impressive, you need to install the following Python modules: - PyGame [python-pygame] http://www.pygame.org/ - PIL [python-imaging] http://www.pythonware.com/products/pil/ or Pillow http://pypi.python.org/pypi/Pillow/ - PyWin32 (OPTIONAL, Win32) http://sourceforge.net/projects/pywin32/ Additionally, please be sure to have pdftoppm or GhostScript installed if you intend to use PDF input.""" sys.exit(1) try: import thread HaveThreads = True def create_lock(): return thread.allocate_lock() def get_thread_id(): return thread.get_ident() except ImportError: HaveThreads = False class pseudolock: def __init__(self): self.state = False def acquire(self, dummy=0): self.state = True def release(self): self.state = False def locked(self): return self.state def create_lock(): return pseudolock() def get_thread_id(): return 0xDEADC0DE CleanExit = False ##### GLOBAL VARIABLES ######################################################### # initialize private variables DocumentTitle = None FileName = "" FileList = [] InfoScriptPath = None AvailableRenderers = [] PDFRenderer = None BaseWorkingDir = '.' Marking = False Tracing = False Panning = False FileProps = {} PageProps = {} PageCache = {} CacheFile = None CacheFileName = None CacheFilePos = 0 CacheMagic = "" MPlayerProcess = None VideoPlaying = False MarkValid, MarkBaseX, MarkBaseY = False, 0, 0 PanValid, PanBaseX, PanBaseY = False, 0, 0 MarkUL = (0, 0) MarkLR = (0, 0) ZoomX0 = 0.0 ZoomY0 = 0.0 ZoomArea = 1.0 ZoomMode = False BoxZoom = False # note: when active, contains the box coordinates IsZoomed = 0 ViewZoomFactor = 1 ResZoomFactor = 1 HighResZoomFailed = False TransitionRunning = False TransitionPhase = 0.0 CurrentCaption = 0 OverviewNeedUpdate = False FileStats = None OSDFont = None CurrentOSDCaption = "" CurrentOSDPage = "" CurrentOSDStatus = "" CurrentOSDComment = "" Lrender = create_lock() Lcache = create_lock() Loverview = create_lock() RTrunning = False RTrestart = False StartTime = 0 CurrentTime = 0 PageEnterTime = 0 PageLeaveTime = 0 PageTimeout = 0 NextPageAfterVideo = False TimeDisplay = False FirstPage = True ProgressBarPos = 0 CursorVisible = True OverviewMode = False LastPage = 0 WantStatus = False GLVendor = "" GLRenderer = "" GLVersion = "" RequiredShaders = [] DefaultScreenTransform = (-1.0, 1.0, 2.0, -2.0) ScreenTransform = DefaultScreenTransform SpotVertices = None SpotIndices = None CallQueue = [] # tool constants (used in info scripts) FirstTimeOnly = 2 ##### PLATFORM-SPECIFIC PYGAME INTERFACE CODE ################################## class Platform_PyGame(object): name = 'pygame' allow_custom_fullscreen_res = True has_hardware_cursor = True use_omxplayer = False _buttons = { 1: "lmb", 2: "mmb", 3: "rmb", 4: "wheelup", 5: "wheeldown" } _keys = dict((getattr(pygame.locals, k), k[2:].lower()) for k in [k for k in dir(pygame.locals) if k.startswith('K_')]) def __init__(self): self.next_events = [] self.schedule_map_ev2flag = {} self.schedule_map_ev2name = {} self.schedule_map_name2ev = {} self.schedule_max = USEREVENT def Init(self): os.environ["SDL_MOUSE_RELATIVE"] = "0" pygame.display.init() def GetTicks(self): return pygame.time.get_ticks() def GetScreenSize(self): return pygame.display.list_modes()[0] def StartDisplay(self): global ScreenWidth, ScreenHeight, Fullscreen, FakeFullscreen, WindowPos pygame.display.set_caption(__title__) flags = OPENGL | DOUBLEBUF if Fullscreen: if FakeFullscreen: print >>sys.stderr, "Using \"fake-fullscreen\" mode." flags |= NOFRAME if not WindowPos: WindowPos = (0,0) else: flags |= FULLSCREEN if WindowPos: os.environ["SDL_VIDEO_WINDOW_POS"] = ','.join(map(str, WindowPos)) pygame.display.set_mode((ScreenWidth, ScreenHeight), flags) pygame.key.set_repeat(500, 30) def LoadOpenGL(self): try: sdl = CDLL(ctypes.util.find_library("SDL") or ctypes.util.find_library("SDL-1.2") or "SDL", RTLD_GLOBAL) get_proc_address = CFUNCTYPE(c_void_p, c_char_p)(('SDL_GL_GetProcAddress', sdl)) except OSError: raise ImportError("failed to load the SDL library") except AttributeError: raise ImportError("failed to load SDL_GL_GetProcAddress from the SDL library") def loadsym(name, prototype): try: addr = get_proc_address(name) except EnvironmentError: return None if not addr: return None return prototype(addr) return OpenGL(loadsym, desktop=True) def SwapBuffers(self): pygame.display.flip() def Done(self): pygame.display.quit() def Quit(self): pygame.quit() def SetWindowTitle(self, text): pygame.display.set_caption(text, __title__) def GetWindowID(self): return pygame.display.get_wm_info()['window'] def GetMousePos(self): return pygame.mouse.get_pos() def SetMousePos(self, coords): pygame.mouse.set_pos(coords) def SetMouseVisible(self, visible): pygame.mouse.set_visible(visible) def _translate_mods(self, key, mods): if mods & KMOD_SHIFT: key = "shift+" + key if mods & KMOD_ALT: key = "alt+" + key if mods & KMOD_CTRL: key = "ctrl+" + key return key def _translate_button(self, ev): try: return self._translate_mods(self._buttons[ev.button], pygame.key.get_mods()) except KeyError: return 'btn' + str(ev.button) def _translate_key(self, ev): try: return self._translate_mods(self._keys[ev.key], ev.mod) except KeyError: return 'unknown-key-' + str(ev.key) def _translate_event(self, ev): if ev.type == QUIT: return ["$quit"] elif ev.type == VIDEOEXPOSE: return ["$expose"] elif ev.type == MOUSEBUTTONDOWN: return ['+' + self._translate_button(ev)] elif ev.type == MOUSEBUTTONUP: ev = self._translate_button(ev) return ['*' + ev, '-' + ev] elif ev.type == MOUSEMOTION: pygame.event.clear(MOUSEMOTION) return ["$move"] elif ev.type == KEYDOWN: if ev.mod & KMOD_ALT: if ev.key == K_F4: return self.PostQuitEvent() elif ev.key == K_TAB: return "$alt-tab" ev = self._translate_key(ev) return ['+' + ev, '*' + ev] elif ev.type == KEYUP: return ['-' + self._translate_key(ev)] elif (ev.type >= USEREVENT) and (ev.type < self.schedule_max): if not(self.schedule_map_ev2flag.get(ev.type)): pygame.time.set_timer(ev.type, 0) return [self.schedule_map_ev2name.get(ev.type)] else: return [] def GetEvent(self, poll=False): if self.next_events: return self.next_events.pop(0) if poll: ev = pygame.event.poll() else: ev = pygame.event.wait() evs = self._translate_event(ev) if evs: self.next_events.extend(evs[1:]) return evs[0] def CheckAnimationCancelEvent(self): while True: ev = pygame.event.poll() if ev.type == NOEVENT: break self.next_events.extend(self._translate_event(ev)) if ev.type in set([KEYDOWN, MOUSEBUTTONUP, QUIT]): return True def ScheduleEvent(self, name, msec=0, periodic=False): try: ev_code = self.schedule_map_name2ev[name] except KeyError: ev_code = self.schedule_max self.schedule_map_name2ev[name] = ev_code self.schedule_map_ev2name[ev_code] = name self.schedule_max += 1 self.schedule_map_ev2flag[ev_code] = periodic pygame.time.set_timer(ev_code, msec) def PostQuitEvent(self): pygame.event.post(pygame.event.Event(QUIT)) def ToggleFullscreen(self): return pygame.display.toggle_fullscreen() def Minimize(self): pygame.display.iconify() def SetGammaRamp(self, gamma, black_level): scale = 1.0 / (255 - black_level) power = 1.0 / gamma ramp = [int(65535.0 * ((max(0, x - black_level) * scale) ** power)) for x in range(256)] return pygame.display.set_gamma_ramp(ramp, ramp, ramp) class Platform_Win32(Platform_PyGame): name = 'pygame-win32' def GetScreenSize(self): if HaveWin32API: dm = win32api.EnumDisplaySettings(None, -1) #ENUM_CURRENT_SETTINGS return (int(dm.PelsWidth), int(dm.PelsHeight)) return Platform_PyGame.GetScreenSize(self) def LoadOpenGL(self): try: opengl32 = WinDLL("opengl32") get_proc_address = WINFUNCTYPE(c_void_p, c_char_p)(('wglGetProcAddress', opengl32)) except OSError: raise ImportError("failed to load the OpenGL library") except AttributeError: raise ImportError("failed to load wglGetProcAddress from the OpenGL library") def loadsym(name, prototype): # try to load OpenGL 1.1 function from opengl32.dll first try: return prototype((name, opengl32)) except AttributeError: pass # if that fails, load the extension function via wglGetProcAddress try: addr = get_proc_address(name) except EnvironmentError: addr = None if not addr: return None return prototype(addr) return OpenGL(loadsym, desktop=True) class Platform_Unix(Platform_PyGame): name = 'pygame-unix' def GetScreenSize(self): re_res = re.compile(r'\s*(\d+)x(\d+)\s+\d+\.\d+\*') res = None try: xrandr = subprocess.Popen(["xrandr"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) for line in xrandr.stdout: m = re_res.match(line) if m: res = tuple(map(int, m.groups())) xrandr.wait() except OSError: pass if res: return res return Platform_PyGame.GetScreenSize(self) class Platform_EGL(Platform_Unix): name = 'egl' egllib = "EGL" gles2lib = "GLESv2" def StartDisplay(self, display=None, window=None, width=None, height=None): global ScreenWidth, ScreenHeight width = width or ScreenWidth height = height or ScreenHeight # load the GLESv2 library before the EGL library (required on the BCM2835) try: self.gles = ctypes.CDLL(ctypes.util.find_library(self.gles2lib)) except OSError: raise ImportError("failed to load the OpenGL ES 2.0 library") # import all functions first try: egl = CDLL(ctypes.util.find_library(self.egllib)) def loadfunc(func, ret, *args): return CFUNCTYPE(ret, *args)((func, egl)) eglGetDisplay = loadfunc("eglGetDisplay", c_void_p, c_void_p) eglInitialize = loadfunc("eglInitialize", c_uint, c_void_p, POINTER(c_int), POINTER(c_int)) eglChooseConfig = loadfunc("eglChooseConfig", c_uint, c_void_p, c_void_p, POINTER(c_void_p), c_int, POINTER(c_int)) eglCreateWindowSurface = loadfunc("eglCreateWindowSurface", c_void_p, c_void_p, c_void_p, c_void_p, c_void_p) eglCreateContext = loadfunc("eglCreateContext", c_void_p, c_void_p, c_void_p, c_void_p, c_void_p) eglMakeCurrent = loadfunc("eglMakeCurrent", c_uint, c_void_p, c_void_p, c_void_p, c_void_p) self.eglSwapBuffers = loadfunc("eglSwapBuffers", c_int, c_void_p, c_void_p) except OSError: raise ImportError("failed to load the EGL library") except AttributeError: raise ImportError("failed to load required symbols from the EGL library") # prepare parameters config_attribs = [ 0x3024, 8, # EGL_RED_SIZE >= 8 0x3023, 8, # EGL_GREEN_SIZE >= 8 0x3022, 8, # EGL_BLUE_SIZE >= 8 0x3021, 0, # EGL_ALPHA_SIZE >= 0 0x3025, 0, # EGL_DEPTH_SIZE >= 0 0x3040, 0x0004, # EGL_RENDERABLE_TYPE = EGL_OPENGL_ES2_BIT 0x3033, 0x0004, # EGL_SURFACE_TYPE = EGL_WINDOW_BIT 0x3038 # EGL_NONE ] context_attribs = [ 0x3098, 2, # EGL_CONTEXT_CLIENT_VERSION = 2 0x3038 # EGL_NONE ] config_attribs = (c_int * len(config_attribs))(*config_attribs) context_attribs = (c_int * len(context_attribs))(*context_attribs) # perform actual initialization eglMakeCurrent(None, None, None, None) self.egl_display = eglGetDisplay(display) if not self.egl_display: raise RuntimeError("could not get EGL display") if not eglInitialize(self.egl_display, None, None): raise RuntimeError("could not initialize EGL") config = c_void_p() num_configs = c_int(0) if not eglChooseConfig(self.egl_display, config_attribs, byref(config), 1, byref(num_configs)): raise RuntimeError("failed to get a framebuffer configuration") if not num_configs.value: raise RuntimeError("no suitable framebuffer configuration found") self.egl_surface = eglCreateWindowSurface(self.egl_display, config, window, None) if not self.egl_surface: raise RuntimeError("could not create EGL surface") context = eglCreateContext(self.egl_display, config, None, context_attribs) if not context: raise RuntimeError("could not create OpenGL ES rendering context") if not eglMakeCurrent(self.egl_display, self.egl_surface, self.egl_surface, context): raise RuntimeError("could not activate OpenGL ES rendering context") def LoadOpenGL(self): def loadsym(name, prototype): return prototype((name, self.gles)) return OpenGL(loadsym, desktop=False) def SwapBuffers(self): self.eglSwapBuffers(self.egl_display, self.egl_surface) class Platform_BCM2835(Platform_EGL): name = 'bcm2835' allow_custom_fullscreen_res = False has_hardware_cursor = False use_omxplayer = True egllib = "brcmEGL" gles2lib = "brcmGLESv2" DISPLAY_ID = 0 def __init__(self, libbcm_host): Platform_EGL.__init__(self) self.libbcm_host_path = libbcm_host def Init(self): try: self.bcm_host = CDLL(self.libbcm_host_path) def loadfunc(func, ret, *args): return CFUNCTYPE(ret, *args)((func, self.bcm_host)) bcm_host_init = loadfunc("bcm_host_init", None) graphics_get_display_size = loadfunc("graphics_get_display_size", c_int32, c_uint16, POINTER(c_uint32), POINTER(c_uint32)) except OSError: raise ImportError("failed to load the bcm_host library") except AttributeError: raise ImportError("failed to load required symbols from the bcm_host library") bcm_host_init() x, y = c_uint32(0), c_uint32(0) if graphics_get_display_size(self.DISPLAY_ID, byref(x), byref(y)) < 0: raise RuntimeError("could not determine display size") self.screen_size = (int(x.value), int(y.value)) def GetScreenSize(self): return self.screen_size def StartDisplay(self): global ScreenWidth, ScreenHeight, Fullscreen, FakeFullscreen, WindowPos class VC_DISPMANX_ALPHA_T(Structure): _fields_ = [("flags", c_int), ("opacity", c_uint32), ("mask", c_void_p)] class EGL_DISPMANX_WINDOW_T(Structure): _fields_ = [("element", c_uint32), ("width", c_int), ("height", c_int)] # first, import everything try: def loadfunc(func, ret, *args): return CFUNCTYPE(ret, *args)((func, self.bcm_host)) vc_dispmanx_display_open = loadfunc("vc_dispmanx_display_open", c_uint32, c_uint32) vc_dispmanx_update_start = loadfunc("vc_dispmanx_update_start", c_uint32, c_int32) vc_dispmanx_element_add = loadfunc("vc_dispmanx_element_add", c_int32, c_uint32, c_uint32, c_int32, # update, display, layer c_void_p, c_uint32, c_void_p, c_uint32, # dest_rect, src, drc_rect, protection POINTER(VC_DISPMANX_ALPHA_T), # alpha c_void_p, c_uint32) # clamp, transform vc_dispmanx_update_submit_sync = loadfunc("vc_dispmanx_update_submit_sync", c_int, c_uint32) except AttributeError: raise ImportError("failed to load required symbols from the bcm_host library") # sanitize arguments width = min(ScreenWidth, self.screen_size[0]) height = min(ScreenHeight, self.screen_size[1]) if WindowPos: x0, y0 = WindowPos else: x0 = (self.screen_size[0] - width) / 2 y0 = (self.screen_size[1] - height) / 2 x0 = max(min(x0, self.screen_size[0] - width), 0) y0 = max(min(y0, self.screen_size[1] - height), 0) # prepare arguments dst_rect = (c_int32 * 4)(x0, y0, width, height) src_rect = (c_int32 * 4)(0, 0, width << 16, height << 16) alpha = VC_DISPMANX_ALPHA_T(1, 255, None) # DISPMANX_FLAGS_ALPHA_FIXED_ALL_PIXELS # perform initialization display = vc_dispmanx_display_open(self.DISPLAY_ID) update = vc_dispmanx_update_start(0) layer = vc_dispmanx_element_add(update, display, 0, byref(dst_rect), 0, byref(src_rect), 0, byref(alpha), None, 0) vc_dispmanx_update_submit_sync(update) self.window = EGL_DISPMANX_WINDOW_T(layer, width, height) Platform_EGL.StartDisplay(self, None, byref(self.window), width, height) # finally, tell PyGame what just happened pygame.display.set_mode((width, height), 0) pygame.mouse.set_pos((width / 2, height / 2)) libbcm_host = ctypes.util.find_library("bcm_host") if libbcm_host: Platform = Platform_BCM2835(libbcm_host) elif os.name == "nt": Platform = Platform_Win32() else: Platform = Platform_Unix() ##### TOOL CODE ################################################################ # read and write the PageProps and FileProps meta-dictionaries def GetProp(prop_dict, key, prop, default=None): if not key in prop_dict: return default if type(prop) == types.StringType: return prop_dict[key].get(prop, default) for subprop in prop: try: return prop_dict[key][subprop] except KeyError: pass return default def SetProp(prop_dict, key, prop, value): if not key in prop_dict: prop_dict[key] = {prop: value} else: prop_dict[key][prop] = value def DelProp(prop_dict, key, prop): try: del prop_dict[key][prop] except KeyError: pass def GetPageProp(page, prop, default=None): global PageProps return GetProp(PageProps, page, prop, default) def SetPageProp(page, prop, value): global PageProps SetProp(PageProps, page, prop, value) def DelPageProp(page, prop): global PageProps DelProp(PageProps, page, prop) def GetTristatePageProp(page, prop, default=0): res = GetPageProp(page, prop, default) if res != FirstTimeOnly: return res return (GetPageProp(page, '_shown', 0) == 1) def GetFileProp(page, prop, default=None): global FileProps return GetProp(FileProps, page, prop, default) def SetFileProp(page, prop, value): global FileProps SetProp(FileProps, page, prop, value) # the Impressive logo (256x64 pixels grayscale PNG) LOGO = """iVBORw0KGgoAAAANSUhEUgAAAQAAAABACAAAAADQNvZiAAAL8ElEQVR4Xu2Ze1hVVfrHv+cc7siAEiF4AW1QEkmD8pJUWlkaaSWWk9pk5ZT5szKvPydvoVhqKuWY9jhkmjZpmZmO9wwzLwhiCImAeEFEkJtyk/se17tZ66yz9zlp+IcPD3z++Z79ujxrne963/XupWjytNCCy5QtuXm/vueAxmBAk8dnWyhpWkhFszTA7VR7qMy ajz+PEUS/RXO7omnyDP/9eBKNNuCdg1Pn/PYUmiQR4HRutAEeiwyA0yo0RVwGg1PYaAO6OQKAfys0Qbq6gHO60QacVQCgoAxNkPa4PQPsmOQumQIoU9BI5gYCyHy/CRuAqb8Pq4jZi0byakcA36MpG4Avv0SjcaQ1ZNxxA5S0xnWB26YTfccZ3Bl8wMmquEMG/BV3MgPcwTmJZmnAX8D55U4ZcA+T8hwArd3xJ3H0gnU8nGENVzfbGRCLW8Xe2 2BpQN/+NwgE0ZV9DgMRPGHp11Gj3SGwD5+8KubtMKM+AwrHLNmdU3S1Mml2F+0K+zPaAHAY/fH6mY+D4/X2ocLKK3nb5z4CS3quPphXXJaxZf6TkPH75KeLpSUXdix+wWQtA0pOMAljk3WChAvN30GMf3Xflarcor0LnobAWKncYAmIbexzOgDD6CMKkTOczzX1okLs84FEhmJB3edekImgaAjw6Dn24Te+rsU1CifaHmY8V9YpnKNmC5znVoh w2kixBSYR/C8Yx9nDRkjMoEXdC8JuernC+aYVz4AOjtIxHsAkDfDf91UfED7fqg4MOL2oPYjHk7pBYOevKao3knvoj4h0dP1BHtgneYodOO8eaA+O76lxRnB67z74CAjnuDnO4HTZkCw2RVMBR+ivwYzbFCbfpKrpHf+RCzgj4oPIAFqiMMDUSTXgheTHIFh5N2CKlPbdaykEHe2gwTu2j9aAnDLP7R4wE7a3MyT6Jt4NFcOX9EkQ9imIRcGQ6 bbexhFwmIrFG4J3WfHVRarG/dwTEoFxQXoDOjowOT2W8iN71yUw7hoL47pZRqA2eUcOGE8NEhs+h+RE9Ai/Li8uOAWGxxZvjQFp9puZcvrupPSr3LXwn5tyyNF5UHlnIIjCUsgMmgCipNhWEyhNFBkgp4D7JCZfp9ELy37awrr90dO+OktH6lIQi1lFVJvAGKgwNrPIpgcNMMyl51h8dkOuR3sDppUUWcsL4GuF8Afh+HE9Pe6BgM6NlTEsys8 Ad4opv3alHN3CwrXBIBJp0L86whQ6cXO5ODPUWTYGwhD05vqCG+FKqDysNLADKrksEAXOHPpyMt8ujgam9KJGoP4M9SSkFaSDGM8XWt3geTw9LGMjAsBwukKLh8oqhagSdftYJQXC+bMTOXLhRihz6aB2Izf8BGAtDdlpBGHYw572qn5Wyuvv+D034HfaEai0/qxOGBDODZgGFbJzn+imV9njGu4FM5T319XsKZXqN1lycJmicomX8VQ+w0FPq KxngVwQwxWV0xBEKbJBCOKOnhTlOoAC59uIA5Ge6VztTh99wRl8hgxwqmXhx8B54Bg3YCQ3gGf9NBa4xvcjkj3V0HnThbrO1XvA3a2iFDACBoqdkc9sFA08yjMYKhufKIRKFhNvmqLDauzN0NwEFmQz6ecHiy/ExcHX0MBkkneK+PPRFCbUqLzB6ATOzu6LmXiaLMMJfd7SdIGy41A5QtFAEG3eZbL2LM1Hmz07U1wd9tCsRsDXWdsFURF+Cg1 Ug9g9qopHFCbl9QDwgcf+59ppDCifR9LN0oDiQZfQQAAVXuZ2CGhRXcxGTjKAU7mBSQ7dcyY4glO/RtMFfq3l3tRIjXAy86dmPg18hQ7RNdpZjXyJmVIXrIng+8/35PSIOnDoFxeRW3//ZYiHi8YAxFszYKRwFC8bmCyvh+A89WjaFuoJw7a1hgXKMSY9D/nbvAoc4IHrSWYDPN9msoa+PoL6zhel2lntrHXB2bsgaEsy4hoE5BEt9M2T4RUPQ GtAhhUDtkjfOIAkOhoS3ABlRRST8OPDEyGzvD+T0MTRO2xcBWLBOcJW1AeMqW4AqqPUdgHGxInaWXkG1J+TKiBOe9W5nqy9/WVQAT1XJtnHKcvRGVA1GQLnXrBKa5JVF1WTD42FzNZ4dcz2eUarGVCeAMiHQHcXAF7UyGKyJAP0s3IDsqjWNT9HRDIVCFx9xZAxWQ121J6HxCXpxHLoyOTzcxD0cIBVikmKnikldVq9xhlm6oZmkRpm7vaylgG Hai0NMLE0mObKvF8Ahsc9NmalEtCcgZXZ+v0mtB7lg9tXC+2IYvmfixJgxoskpxQakkGcfGGzK8jdkOHStLnhe3zAeOLEiEP6DIiVSvsyG9j7F3iPp3afLc2aXwQNmdyATMmAs4qUIp62DSCEfYJ2lMy5mtECT5LXd8EGu3tvoVXgvoRRUqdICf22n/r1sRNXQOCuMwBHhqltYLoLgMoP5Vlnr4IWI9q2kl8D9BWgNSCAR2wZEEySK48+o6v1P Njk9we3gfjLt31h5vKAFSDslr8EQcS9xDEQ8oWw7TgqvpybzGqnvwvq91sfKea55O2mM6A7yTFpdEk+zBSQFME21579YCa1Sqetvc9BUDPh+CpqUoY1WaIK+J9rDWjvO90ZwPWPbjarUdsFb54BmgrQGTCYZLetBEnnLxO2UWa/WA6G1yLIrOmfS+q40sBDvkNeDjLBguM1TIa9QRf5XM2stgxQztpIWIqU52gjGbYNiHiMSfYpqwYIMwPxh3z X7zzpsC4gRI9PIA1+GoT/vks/rku5OBQylSeYLHQCULFQZFU+zWrTgMsVGgNslrirjz4D6s9C4LqMJAaEnZ/OgKKiWzAASQ/G0fKGwoJLD28mfR6MvsmPM/HZGqWvARcAWHFF8t2mAdozsDrrFrugeMyugmBmB6r6aBD+drzFaGpgoBFWcIOgYA5JoCZcOUURYee1raAy4xGtAUT5Ys2sYa42DZDS+1w9BO5eVpuA7S7YbxLJp1d1dglSmPQcC ws69GDyQ6QDOPuoUdCKl8S4g3P+kAi/FsCDhiirBizP18zq8z4s8HwIxrvcb7UL6iN6A8L3OlAn+xC2DVhNsqANzDjNOn0X09BZieJFuc4o/runx2unhkAgwr0gCDWBQzcqovRjmFlfzWRyAMyYxqcHwWjRBTvfvAuS69cKuIUesgGey39wppkjKmQDKnIgc+wQjd0fBM7zqZEuaQD83BF0eLEziOGUfL8BMHaH748bPEGE9OZh3AuBsx8kDoP 4tBBm8jYxcdgTBs6jiSvapMMoX4b97G+jCzo8uTxzApV83atpljcJWPJeLW1rwiRvAE4PTYr93h9l2SwEwDQl+7txAfB4j27utYlsEhcAIy/smNzD4DpqO60xTvO91dn6GihZApmZJUz8DyzoAMA+9P9+jL0PSIedyADbV6HSPE1Ea8D86Wjl5cmz8PpLW/WjZeIjIynvlyzJO+nR097cp+8Do01EBMpagYjKE2HXwYNR7gpiI+1x/N/ASarWG /BJMWQuTFjHxDhjRnGSXaiaZmWXGwzIL/mj14AMXRcUkQBx9xcUDaHViTdLvQGI8nsdhPdAHtrPZFMvXuqtQCTMZ3IwZowJhCuInPEkX0wSLzaRkEmsdgCuLYUlX/k3jGrdn4diAaOuC9Ze+LNdUKZ2VdBhCDo4WDWgfuxCBTJH+k+lNBjaPwESZ0ZTseSN7bkTEvmjikivjq2Fyr+3Q6YqEcCyq9Awb1w1ZFKHDwWMurvg+VoI3Lxv3gVlitY FvZWrsysTOv6/z1EIkoc+dAAqB3qNPCfqen5wGu9hTz9xgoeVmMBYqOzqlUQl+uY/9NeB4mjo+DxoGwTnxwRvVgCDowFArWqlgxFAvWyTE5OaOghM9mQx38ACT/ZUCVQVFOSn7oyrgwVGBz5aT/CQMF/vwtTU06lJ9ZAwdA65PyQoJzllRzpk2oWEhPQoSkn5OR5mTPf39oiPuwYNfV/Bgf/AGp2eHdCubUXqDU7UqNPhdvAoZjIzCk0XIxqLn OLN3IAzzduAFgMKrzZXA8R7cTPOgGZugNvdzdoA0QWbtQEtGdBiQEl+MzagqSdAiwEttPA/JcotzChXXBQAAAAASUVORK5CYII=""" # the default cursor (19x23 pixel RGBA PNG) DEFAULT_CURSOR = """iVBORw0KGgoAAAANSUhEUgAAABMAAAAXCAYAAADpwXTaAAADCklEQVR42qWUXWwMURTH787MznbWbm1VtdWP0KBN+pFWlQRVQlJBQkR4lGqioY0IibSprAchHgQhoh76hAQPJB4IRdBobdFstbZ4oJLup9au3c5Md3fmjnPHdE2qZVsn+c3snDv3v/9zzt2lEcRbx90rnAk/d7x2xdF/BAWwFmv6jm1bal4db95Xp uVmLcbEJfQ9Y0Fu8YZ1yzsvnTu6G3LG2YopPM+HbfMWohTObC0pWXLjWrv9DOS52YjJAi8EKJpBqbZMxNAMlZeXdeTOzdP36/duzYF1w4yciSI/gmUJxLIQw7CIomiUZrOu37m9puukvW51sn0kL2FBEN0Yy2qClGswUIiijYjjUvJXrijuaLt4uCGZPv7qmTAWIGIKMMeajliTGQQNqkOGYbiCxTmXr7e3XC0tXmT5mxhNLtVrq3KWLS3YQxw RjCyHBD6IFPUVclUMHGeqWFVVWJuXm/Gku2cwNK0zr9fvJc5UdwqGqVoRZ56rOjMAFMWon1NTLZU11WXdZ0/Vb56qj2ri0eOXwzAAnBDEGKWl56oCk2FZNqOoMP9e24XG5sl9VMv0+0eM9XW7mhijkSXPpF+M0YRkOY7iMVFfbsKE1cJtrN1UXmrmUjr6XUMi0lmVYKKj5Hjo3dnSshENU9WXS75IxgoOhfmxWEwurSwvaIX96mCYCbFoNBrEW MqnMK0JSurx6HcNhxwOR8TnHx33eALjXt+o4A8EBUVReNjnBgaALGBoQkwWRRGOB1ZFDJhSBV90OoIHmuxOWZZ98E4Q4HVEgDDgAUiZyoQYjsbiI2SSMpRKynrv+jR2sKmlF4TewLpD20RExrXNMY24dpcTYvBj94F1RHC7vdH9Dcf6eF5wwtpDwKk5wZMnoY/fzqIxH3EWiQhS46ETAz7/t3eQfwqQe2g6gT/OGYkfobBHisfkVvv5vg8fP/d D6hnQq/Xqn0KJc0aiorxofq9zkL11+8FXeOwCOgGfVlpSof+vygTWAGagB/iiNTfp0IsRkWxA0hxFZyI0lbBRX/pM4ycZx2V6yAv08AAAAABJRU5ErkJggg==""" # get the contents of a PIL image as a string def img2str(img): if hasattr(img, "tobytes"): return img.tobytes() else: return img.tostring() # create a PIL image from a string def str2img(mode, size, data): if hasattr(Image, "frombytes"): return Image.frombytes(mode, size, data) else: return Image.fromstring(mode, size, data) # determine the next power of two def npot(x): res = 1 while res < x: res <<= 1 return res # convert boolean value to string def b2s(b): if b: return "Y" return "N" # extract a number at the beginning of a string def num(s): s = s.strip() r = "" while s[0] in "0123456789": r += s[0] s = s[1:] try: return int(r) except ValueError: return -1 # linearly interpolate between two floating-point RGB colors represented as tuples def lerpColor(a, b, t): return tuple([min(1.0, max(0.0, x + t * (y - x))) for x, y in zip(a, b)]) # get a representative subset of file statistics def my_stat(filename): try: s = os.stat(filename) except OSError: return None return (s.st_size, s.st_mtime, s.st_ctime, s.st_mode) # determine (pagecount,width,height) of a PDF file def analyze_pdf(filename): f = file(filename,"rb") pdf = f.read() f.close() box = map(float, pdf.split("/MediaBox",1)[1].split("]",1)[0].split("[",1)[1].strip().split()) return (max(map(num, pdf.split("/Count")[1:])), box[2]-box[0], box[3]-box[1]) # unescape { literals in PDF files re_unescape = re.compile(r'&#[0-9]+;') def decode_literal(m): try: code = int(m.group(0)[2:-1]) if code: return chr(code) else: return "" except ValueError: return '?' def unescape_pdf(s): return re_unescape.sub(decode_literal, s) # parse pdftk output def pdftkParse(filename, page_offset=0): f = file(filename, "r") InfoKey = None BookmarkTitle = None Title = None Pages = 0 for line in f: try: key, value = [item.strip() for item in line.split(':', 1)] except ValueError: continue key = key.lower() if key == "numberofpages": Pages = int(value) elif key == "infokey": InfoKey = value.lower() elif (key == "infovalue") and (InfoKey == "title"): Title = unescape_pdf(value) InfoKey = None elif key == "bookmarktitle": BookmarkTitle = unescape_pdf(value) elif key == "bookmarkpagenumber" and BookmarkTitle: try: page = int(value) if not GetPageProp(page + page_offset, '_title'): SetPageProp(page + page_offset, '_title', BookmarkTitle) except ValueError: pass BookmarkTitle = None f.close() if AutoOverview: SetPageProp(page_offset + 1, '_overview', True) for page in xrange(page_offset + 2, page_offset + Pages): SetPageProp(page, '_overview', \ not(not(GetPageProp(page + AutoOverview - 1, '_title')))) SetPageProp(page_offset + Pages, '_overview', True) return (Title, Pages) # parse mutool output def mutoolParse(f, page_offset=0): title = None pages = 0 for line in f: m = re.match("pages:\s*(\d+)", line, re.I) if m and not(pages): pages = int(m.group(1)) m = re.search("/title\s*\(", line, re.I) if m and not(title): title = line[m.end():].replace(')', '\0').replace('\\(', '(').replace('\\\0', ')').split('\0', 1)[0].strip() return (title, pages) # translate pixel coordinates to normalized screen coordinates def MouseToScreen(mousepos): return (ZoomX0 + mousepos[0] * ZoomArea / ScreenWidth, ZoomY0 + mousepos[1] * ZoomArea / ScreenHeight) # normalize rectangle coordinates so that the upper-left point comes first def NormalizeRect(X0, Y0, X1, Y1): return (min(X0, X1), min(Y0, Y1), max(X0, X1), max(Y0, Y1)) # check if a point is inside a box (or a list of boxes) def InsideBox(x, y, box): return (x >= box[0]) and (y >= box[1]) and (x < box[2]) and (y < box[3]) def FindBox(x, y, boxes): for i in xrange(len(boxes)): if InsideBox(x, y, boxes[i]): return i raise ValueError # zoom an image size to a destination size, preserving the aspect ratio def ZoomToFit(size, dest=None): if not dest: dest = (ScreenWidth + Overscan, ScreenHeight + Overscan) newx = dest[0] newy = size[1] * newx / size[0] if newy > dest[1]: newy = dest[1] newx = size[0] * newy / size[1] return (newx, newy) # get the overlay grid screen coordinates for a specific page def OverviewPos(page): return ( \ int(page % OverviewGridSize) * OverviewCellX + OverviewOfsX, \ int(page / OverviewGridSize) * OverviewCellY + OverviewOfsY \ ) def StopMPlayer(): global MPlayerProcess, VideoPlaying, NextPageAfterVideo if not MPlayerProcess: return # first, ask politely try: if Platform.use_omxplayer and VideoPlaying: MPlayerProcess.stdin.write('q') else: MPlayerProcess.stdin.write('quit\n') MPlayerProcess.stdin.flush() for i in xrange(10): if MPlayerProcess.poll() is None: time.sleep(0.1) else: break except: pass # if that didn't work, be rude if MPlayerProcess.poll() is None: print >>sys.stderr, "Audio/video player didn't exit properly, killing PID", MPlayerProcess.pid try: if os.name == 'nt': win32api.TerminateProcess(win32api.OpenProcess(1, False, MPlayerProcess.pid), 0) else: os.kill(MPlayerProcess.pid, 2) MPlayerProcess = None except: pass else: MPlayerProcess = None VideoPlaying = False if os.name == 'nt': win32gui.ShowWindow(Platform.GetWindowID(), 9) # SW_RESTORE if NextPageAfterVideo: NextPageAfterVideo = False TransitionTo(GetNextPage(Pcurrent, 1)) def ClockTime(minutes): if minutes: return time.strftime("%H:%M") else: return time.strftime("%H:%M:%S") def FormatTime(t, minutes=False): if minutes and (t < 3600): return "%d min" % (t / 60) elif minutes: return "%d:%02d" % (t / 3600, (t / 60) % 60) elif t < 3600: return "%d:%02d" % (t / 60, t % 60) else: ms = t % 3600 return "%d:%02d:%02d" % (t / 3600, ms / 60, ms % 60) def SafeCall(func, args=[], kwargs={}): if not func: return None try: return func(*args, **kwargs) except: print >>sys.stderr, "----- Unhandled Exception ----" traceback.print_exc(file=sys.stderr) print >>sys.stderr, "----- End of traceback -----" def Quit(code=0): global CleanExit if not code: CleanExit = True StopMPlayer() Platform.Done() print >>sys.stderr, "Total presentation time: %s." % \ FormatTime((Platform.GetTicks() - StartTime) / 1000) sys.exit(code) ##### OPENGL (ES) 2.0 LOADER AND TOOLKIT ####################################### if os.name == 'nt': GLFUNCTYPE = WINFUNCTYPE else: GLFUNCTYPE = CFUNCTYPE class GLFunction(object): def __init__(self, required, name, ret, *args): self.name = name self.required = required self.prototype = GLFUNCTYPE(ret, *args) class OpenGL(object): FALSE = 0 TRUE = 1 NO_ERROR = 0 INVALID_ENUM = 0x0500 INVALID_VALUE = 0x0501 INVALID_OPERATION = 0x0502 OUT_OF_MEMORY = 0x0505 INVALID_FRAMEBUFFER_OPERATION = 0x0506 VENDOR = 0x1F00 RENDERER = 0x1F01 VERSION = 0x1F02 EXTENSIONS = 0x1F03 POINTS = 0x0000 LINES = 0x0001 LINE_LOOP = 0x0002 LINE_STRIP = 0x0003 TRIANGLES = 0x0004 TRIANGLE_STRIP = 0x0005 TRIANGLE_FAN = 0x0006 BYTE = 0x1400 UNSIGNED_BYTE = 0x1401 SHORT = 0x1402 UNSIGNED_SHORT = 0x1403 INT = 0x1404 UNSIGNED_INT = 0x1405 FLOAT = 0x1406 DEPTH_TEST = 0x0B71 BLEND = 0x0BE2 ZERO = 0 ONE = 1 SRC_COLOR = 0x0300 ONE_MINUS_SRC_COLOR = 0x0301 SRC_ALPHA = 0x0302 ONE_MINUS_SRC_ALPHA = 0x0303 DST_ALPHA = 0x0304 ONE_MINUS_DST_ALPHA = 0x0305 DST_COLOR = 0x0306 ONE_MINUS_DST_COLOR = 0x0307 DEPTH_BUFFER_BIT = 0x00000100 COLOR_BUFFER_BIT = 0x00004000 TEXTURE0 = 0x84C0 TEXTURE_2D = 0x0DE1 TEXTURE_RECTANGLE = 0x84F5 TEXTURE_MAG_FILTER = 0x2800 TEXTURE_MIN_FILTER = 0x2801 TEXTURE_WRAP_S = 0x2802 TEXTURE_WRAP_T = 0x2803 NEAREST = 0x2600 LINEAR = 0x2601 NEAREST_MIPMAP_NEAREST = 0x2700 LINEAR_MIPMAP_NEAREST = 0x2701 NEAREST_MIPMAP_LINEAR = 0x2702 LINEAR_MIPMAP_LINEAR = 0x2703 CLAMP_TO_EDGE = 0x812F REPEAT = 0x2901 ALPHA = 0x1906 RGB = 0x1907 RGBA = 0x1908 LUMINANCE = 0x1909 LUMINANCE_ALPHA = 0x190A ARRAY_BUFFER = 0x8892 ELEMENT_ARRAY_BUFFER = 0x8893 STREAM_DRAW = 0x88E0 STATIC_DRAW = 0x88E4 DYNAMIC_DRAW = 0x88E8 FRAGMENT_SHADER = 0x8B30 VERTEX_SHADER = 0x8B31 COMPILE_STATUS = 0x8B81 LINK_STATUS = 0x8B82 INFO_LOG_LENGTH = 0x8B84 UNPACK_ALIGNMENT = 0x0CF5 MAX_TEXTURE_SIZE = 0x0D33 _funcs = [ GLFunction(True, "GetString", c_char_p, c_uint), GLFunction(True, "Enable", None, c_uint), GLFunction(True, "Disable", None, c_uint), GLFunction(True, "GetError", c_uint), GLFunction(True, "Viewport", None, c_int, c_int, c_int, c_int), GLFunction(True, "Clear", None, c_uint), GLFunction(True, "ClearColor", None, c_float, c_float, c_float, c_float), GLFunction(True, "BlendFunc", None, c_uint, c_uint), GLFunction(True, "GenTextures", None, c_uint, POINTER(c_int)), GLFunction(True, "BindTexture", None, c_uint, c_int), GLFunction(True, "ActiveTexture", None, c_uint), GLFunction(True, "TexParameteri", None, c_uint, c_uint, c_int), GLFunction(True, "TexImage2D", None, c_uint, c_uint, c_uint, c_uint, c_uint, c_uint, c_uint, c_uint, c_void_p), GLFunction(True, "GenerateMipmap", None, c_uint), GLFunction(True, "GenBuffers", None, c_uint, POINTER(c_int)), GLFunction(True, "BindBuffer", None, c_uint, c_int), GLFunction(True, "BufferData", None, c_uint, c_void_p, c_void_p, c_uint), GLFunction(True, "CreateProgram", c_uint), GLFunction(True, "CreateShader", c_uint, c_uint), GLFunction(True, "ShaderSource", None, c_uint, c_uint, c_void_p, c_void_p), GLFunction(True, "CompileShader", None, c_uint), GLFunction(True, "GetShaderiv", None, c_uint, c_uint, POINTER(c_uint)), GLFunction(True, "GetShaderInfoLog", None, c_uint, c_uint, c_void_p, c_void_p), GLFunction(True, "AttachShader", None, c_uint, c_uint), GLFunction(True, "LinkProgram", None, c_uint), GLFunction(True, "GetProgramiv", None, c_uint, c_uint, POINTER(c_uint)), GLFunction(True, "GetProgramInfoLog", None, c_uint, c_uint, c_void_p, c_void_p), GLFunction(True, "UseProgram", None, c_uint), GLFunction(True, "BindAttribLocation", None, c_uint, c_uint, c_char_p), GLFunction(True, "GetAttribLocation", c_int, c_uint, c_char_p), GLFunction(True, "GetUniformLocation", c_int, c_uint, c_char_p), GLFunction(True, "Uniform1f", None, c_uint, c_float), GLFunction(True, "Uniform2f", None, c_uint, c_float, c_float), GLFunction(True, "Uniform3f", None, c_uint, c_float, c_float, c_float), GLFunction(True, "Uniform4f", None, c_uint, c_float, c_float, c_float, c_float), GLFunction(True, "Uniform1i", None, c_uint, c_int), GLFunction(True, "Uniform2i", None, c_uint, c_int, c_int), GLFunction(True, "Uniform3i", None, c_uint, c_int, c_int, c_int), GLFunction(True, "Uniform4i", None, c_uint, c_int, c_int, c_int, c_int), GLFunction(True, "EnableVertexAttribArray", None, c_uint), GLFunction(True, "DisableVertexAttribArray", None, c_uint), GLFunction(True, "VertexAttribPointer", None, c_uint, c_uint, c_uint, c_uint, c_uint, c_void_p), GLFunction(True, "DrawArrays", None, c_uint, c_uint, c_uint), GLFunction(True, "DrawElements", None, c_uint, c_uint, c_uint, c_void_p), GLFunction(True, "PixelStorei", None, c_uint, c_uint), GLFunction(True, "GetIntegerv", None, c_uint, POINTER(c_int)), ] _typemap = { BYTE: c_int8, UNSIGNED_BYTE: c_uint8, SHORT: c_int16, UNSIGNED_SHORT: c_uint16, INT: c_int32, UNSIGNED_INT: c_uint32, FLOAT: c_float } def __init__(self, loader, desktop=False): global GLVendor, GLRenderer, GLVersion self._is_desktop_gl = desktop for func in self._funcs: funcptr = None for suffix in ("", "ARB", "ObjectARB", "EXT", "OES"): funcptr = loader("gl" + func.name + suffix, func.prototype) if funcptr: break if not funcptr: if func.required: raise ImportError("failed to import required OpenGL function 'gl%s'" % func.name) else: def errfunc(*args): raise ImportError("call to unimplemented OpenGL function 'gl%s'" % func.name) funcptr = errfunc if hasattr(self, func.name): setattr(self, '_' + func.name, funcptr) else: setattr(self, func.name, funcptr) if func.name == "GetString": GLVendor = self.GetString(self.VENDOR) or "" GLRenderer = self.GetString(self.RENDERER) or "" GLVersion = self.GetString(self.VERSION) or "" self._init() def GenTextures(self, n=1): bufs = (c_int * n)() self._GenTextures(n, bufs) if n == 1: return bufs[0] return list(bufs) def ActiveTexture(self, tmu): if tmu < self.TEXTURE0: tmu += self.TEXTURE0 self._ActiveTexture(tmu) def GenBuffers(self, n=1): bufs = (c_int * n)() self._GenBuffers(n, bufs) if n == 1: return bufs[0] return list(bufs) def BufferData(self, target, size=0, data=None, usage=STATIC_DRAW, type=None): if isinstance(data, list): if type: type = self._typemap[type] elif isinstance(data[0], int): type = c_int32 elif isinstance(data[0], float): type = c_float else: raise TypeError("cannot infer buffer data type") size = len(data) * sizeof(type) data = (type * len(data))(*data) self._BufferData(target, cast(size, c_void_p), cast(data, c_void_p), usage) def ShaderSource(self, shader, source): source = c_char_p(source) self._ShaderSource(shader, 1, pointer(source), None) def GetShaderi(self, shader, pname): res = (c_uint * 1)() self.GetShaderiv(shader, pname, res) return res[0] def GetShaderInfoLog(self, shader): length = self.GetShaderi(shader, self.INFO_LOG_LENGTH) if not length: return "" buf = create_string_buffer(length + 1) self._GetShaderInfoLog(shader, length + 1, None, buf) return buf.raw.split('\0', 1)[0] def GetProgrami(self, program, pname): res = (c_uint * 1)() self.GetProgramiv(program, pname, res) return res[0] def GetProgramInfoLog(self, program): length = self.GetProgrami(program, self.INFO_LOG_LENGTH) if not length: return "" buf = create_string_buffer(length + 1) self._GetProgramInfoLog(program, length + 1, None, buf) return buf.raw.split('\0', 1)[0] def Uniform(self, location, *values): if not values: raise TypeError("no values for glUniform") if (len(values) == 1) and (isinstance(values[0], list) or isinstance(values[0], tuple)): values = values[0] l = len(values) if l > 4: raise TypeError("uniform vector has too-high order(%d)" % len(values)) if any(isinstance(v, float) for v in values): if l == 1: self.Uniform1f(location, values[0]) elif l == 2: self.Uniform2f(location, values[0], values[1]) elif l == 3: self.Uniform3f(location, values[0], values[1], values[2]) else: self.Uniform4f(location, values[0], values[1], values[2], values[3]) else: if l == 1: self.Uniform1i(location, values[0]) elif l == 2: self.Uniform2i(location, values[0], values[1]) elif l == 3: self.Uniform3i(location, values[0], values[1], values[2]) else: self.Uniform4i(location, values[0], values[1], values[2], values[3]) ##### Convenience Functions ##### def _init(self): self.enabled_attribs = set() def set_enabled_attribs(self, *attrs): want = set(attrs) for a in (want - self.enabled_attribs): self.EnableVertexAttribArray(a) for a in (self.enabled_attribs - want): self.DisableVertexAttribArray(a) self.enabled_attribs = want def set_texture(self, target=TEXTURE_2D, tex=0, tmu=0): self.ActiveTexture(self.TEXTURE0 + tmu) self.BindTexture(target, tex) def make_texture(self, target=TEXTURE_2D, wrap=CLAMP_TO_EDGE, filter=LINEAR_MIPMAP_NEAREST, img=None): tex = self.GenTextures() min_filter = filter if min_filter < self.NEAREST_MIPMAP_NEAREST: mag_filter = min_filter else: mag_filter = self.NEAREST + (min_filter & 1) self.BindTexture(target, tex) self.TexParameteri(target, self.TEXTURE_WRAP_S, wrap) self.TexParameteri(target, self.TEXTURE_WRAP_T, wrap) self.TexParameteri(target, self.TEXTURE_MIN_FILTER, min_filter) self.TexParameteri(target, self.TEXTURE_MAG_FILTER, mag_filter) if img: self.load_texture(target, img) return tex def load_texture(self, target, tex_or_img, img=None): if img: gl.BindTexture(target, tex_or_img) else: img = tex_or_img if img.mode == 'RGBA': format = self.RGBA elif img.mode == 'RGB': format = self.RGB elif img.mode == 'LA': format = self.LUMINANCE_ALPHA elif img.mode == 'L': format = self.LUMINANCE else: raise TypeError("image has unsupported color format '%s'" % img.mode) gl.TexImage2D(target, 0, format, img.size[0], img.size[1], 0, format, self.UNSIGNED_BYTE, img2str(img)) class GLShaderCompileError(SyntaxError): pass class GLInvalidShaderError(GLShaderCompileError): pass class GLShader(object): LOG_NEVER = 0 LOG_ON_ERROR = 1 LOG_IF_NOT_EMPTY = 2 LOG_ALWAYS = 3 LOG_DEFAULT = LOG_ON_ERROR def __init__(self, vs=None, fs=None, attributes=[], uniforms=[], loglevel=None): if not(vs): vs = self.vs if not(fs): fs = self.fs if not(attributes) and hasattr(self, 'attributes'): attributes = self.attributes if isinstance(attributes, dict): attributes = attributes.items() if not(uniforms) and hasattr(self, 'uniforms'): uniforms = self.uniforms if isinstance(uniforms, dict): uniforms = uniforms.items() uniforms = [((u, None) if isinstance(u, basestring) else u) for u in uniforms] if (loglevel is None) and hasattr(self, 'loglevel'): loglevel = self.loglevel if loglevel is None: loglevel = self.LOG_DEFAULT self.program = gl.CreateProgram() def handle_shader_log(status, log_getter, action): force_log = (loglevel >= self.LOG_ALWAYS) or ((loglevel >= self.LOG_ON_ERROR) and not(status)) if force_log or (loglevel >= self.LOG_IF_NOT_EMPTY): log = log_getter().rstrip() else: log = "" if force_log or ((loglevel >= self.LOG_IF_NOT_EMPTY) and log): if status: print >>sys.stderr, "Info: log for %s %s:" % (self.__class__.__name__, action) else: print >>sys.stderr, "Error: %s %s failed - log information follows:" % (self.__class__.__name__, action) for line in log.split('\n'): print >>sys.stderr, '>', line.rstrip() if not status: raise GLShaderCompileError("failure during %s %s" % (self.__class__.__name__, action)) def handle_shader(type_enum, type_name, src): if gl._is_desktop_gl: src = src.replace("highp ", "") src = src.replace("mediump ", "") src = src.replace("lowp ", "") shader = gl.CreateShader(type_enum) gl.ShaderSource(shader, src) gl.CompileShader(shader) handle_shader_log(gl.GetShaderi(shader, gl.COMPILE_STATUS), lambda: gl.GetShaderInfoLog(shader), type_name + " shader compilation") gl.AttachShader(self.program, shader) handle_shader(gl.VERTEX_SHADER, "vertex", vs) handle_shader(gl.FRAGMENT_SHADER, "fragment", fs) for attr in attributes: if not isinstance(attr, basestring): loc, name = attr if isinstance(loc, basestring): loc, name = name, loc setattr(self, name, loc) elif hasattr(self, attr): name = attr loc = getattr(self, name) gl.BindAttribLocation(self.program, loc, name) gl.LinkProgram(self.program) handle_shader_log(gl.GetProgrami(self.program, gl.LINK_STATUS), lambda: gl.GetProgramInfoLog(self.program), "linking") gl.UseProgram(self.program) for name in attributes: if isinstance(name, basestring) and not(hasattr(self, attr)): setattr(self, name, int(gl.GetAttribLocation(self.program, name))) for u in uniforms: loc = int(gl.GetUniformLocation(self.program, u[0])) setattr(self, u[0], loc) if u[1] is not None: gl.Uniform(loc, *u[1:]) def use(self): gl.UseProgram(self.program) return self @classmethod def get_instance(self): try: instance = self._instance if instance: return instance else: raise GLInvalidShaderError("shader failed to compile in the past") except AttributeError: try: self._instance = self() except GLShaderCompileError, e: self._instance = None raise return self._instance # NOTE: OpenGL drawing code in Impressive uses the following conventions: # - program binding is undefined # - vertex attribute layout is undefined # - vertex attribute enable/disable is managed by gl.set_enabled_attribs() # - texture bindings are undefined # - ActiveTexure is TEXTURE0 # - array and element array buffer bindings are undefined # - BLEND is disabled, BlendFunc is (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) ##### STOCK SHADERS ############################################################ class SimpleQuad(object): "vertex buffer singleton for a simple quad (used by various shaders)" vbuf = None @classmethod def draw(self): gl.set_enabled_attribs(0) if not self.vbuf: self.vbuf = gl.GenBuffers() gl.BindBuffer(gl.ARRAY_BUFFER, self.vbuf) gl.BufferData(gl.ARRAY_BUFFER, data=[0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]) else: gl.BindBuffer(gl.ARRAY_BUFFER, self.vbuf) gl.VertexAttribPointer(0, 2, gl.FLOAT, False, 0, 0) gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) class TexturedRectShader(GLShader): vs = """ attribute highp vec2 aPos; uniform highp vec4 uPosTransform; uniform highp vec4 uScreenTransform; uniform highp vec4 uTexTransform; varying mediump vec2 vTexCoord; void main() { highp vec2 pos = uPosTransform.xy + aPos * uPosTransform.zw; gl_Position = vec4(uScreenTransform.xy + pos * uScreenTransform.zw, 0.0, 1.0); vTexCoord = uTexTransform.xy + aPos * uTexTransform.zw; } """ fs = """ uniform lowp vec4 uColor; uniform lowp sampler2D uTex; varying mediump vec2 vTexCoord; void main() { gl_FragColor = uColor * texture2D(uTex, vTexCoord); } """ attributes = { 0: 'aPos' } uniforms = ['uPosTransform', 'uScreenTransform', 'uTexTransform', 'uColor'] def draw(self, x0, y0, x1, y1, s0=0.0, t0=0.0, s1=1.0, t1=1.0, tex=None, color=1.0): self.use() if tex: gl.BindTexture(gl.TEXTURE_2D, tex) if isinstance(color, float): gl.Uniform4f(self.uColor, color, color, color, 1.0) else: gl.Uniform(self.uColor, color) gl.Uniform(self.uPosTransform, x0, y0, x1 - x0, y1 - y0) gl.Uniform(self.uScreenTransform, ScreenTransform) gl.Uniform(self.uTexTransform, s0, t0, s1 - s0, t1 - t0) SimpleQuad.draw() RequiredShaders.append(TexturedRectShader) class TexturedMeshShader(GLShader): vs = """ attribute highp vec3 aPosAndAlpha; uniform highp vec4 uPosTransform; uniform highp vec4 uScreenTransform; uniform highp vec4 uTexTransform; varying mediump vec2 vTexCoord; varying lowp float vAlpha; void main() { highp vec2 pos = uPosTransform.xy + aPosAndAlpha.xy * uPosTransform.zw; gl_Position = vec4(uScreenTransform.xy + pos * uScreenTransform.zw, 0.0, 1.0); vTexCoord = uTexTransform.xy + aPosAndAlpha.xy * uTexTransform.zw; vAlpha = aPosAndAlpha.z; } """ fs = """ uniform lowp sampler2D uTex; varying mediump vec2 vTexCoord; varying lowp float vAlpha; void main() { gl_FragColor = vec4(1.0, 1.0, 1.0, vAlpha) * texture2D(uTex, vTexCoord); } """ attributes = { 0: 'aPosAndAlpha' } uniforms = ['uPosTransform', 'uScreenTransform', 'uTexTransform'] def setup(self, x0, y0, x1, y1, s0=0.0, t0=0.0, s1=1.0, t1=1.0, tex=None): self.use() if tex: gl.BindTexture(gl.TEXTURE_2D, tex) gl.Uniform(self.uPosTransform, x0, y0, x1 - x0, y1 - y0) gl.Uniform(self.uScreenTransform, ScreenTransform) gl.Uniform(self.uTexTransform, s0, t0, s1 - s0, t1 - t0) RequiredShaders.append(TexturedMeshShader) class BlurShader(GLShader): vs = """ attribute highp vec2 aPos; uniform highp vec4 uScreenTransform; varying mediump vec2 vTexCoord; void main() { gl_Position = vec4(uScreenTransform.xy + aPos * uScreenTransform.zw, 0.0, 1.0); vTexCoord = aPos; } """ fs = """ uniform lowp float uIntensity; uniform mediump sampler2D uTex; uniform mediump vec2 uDeltaTexCoord; varying mediump vec2 vTexCoord; void main() { lowp vec3 color = (uIntensity * 0.125) * ( texture2D(uTex, vTexCoord).rgb * 3.0 + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(+0.89, +0.45)).rgb + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(+0.71, -0.71)).rgb + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(-0.45, -0.89)).rgb + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(-0.99, +0.16)).rgb + texture2D(uTex, vTexCoord + uDeltaTexCoord * vec2(-0.16, +0.99)).rgb ); lowp float gray = dot(vec3(0.299, 0.587, 0.114), color); gl_FragColor = vec4(mix(color, vec3(gray, gray, gray), uIntensity), 1.0); } """ attributes = { 0: 'aPos' } uniforms = ['uScreenTransform', 'uDeltaTexCoord', 'uIntensity'] def draw(self, dtx, dty, intensity=1.0, tex=None): self.use() if tex: gl.BindTexture(gl.TEXTURE_2D, tex) gl.Uniform(self.uScreenTransform, ScreenTransform) gl.Uniform2f(self.uDeltaTexCoord, dtx, dty) gl.Uniform1f(self.uIntensity, intensity) SimpleQuad.draw() # (not added to RequiredShaders because this shader is allowed to fail) class ProgressBarShader(GLShader): vs = """ attribute highp vec2 aPos; uniform highp vec4 uPosTransform; uniform lowp vec4 uColor0; uniform lowp vec4 uColor1; varying lowp vec4 vColor; void main() { gl_Position = vec4(uPosTransform.xy + aPos * uPosTransform.zw, 0.0, 1.0); vColor = mix(uColor0, uColor1, aPos.y); } """ fs = """ varying lowp vec4 vColor; void main() { gl_FragColor = vColor; } """ attributes = { 0: 'aPos' } uniforms = ['uPosTransform', 'uColor0', 'uColor1'] def draw(self, x0, y0, x1, y1, color0, color1): self.use() tx0 = ScreenTransform[0] + ScreenTransform[2] * x0 ty0 = ScreenTransform[1] + ScreenTransform[3] * y0 tx1 = ScreenTransform[0] + ScreenTransform[2] * x1 ty1 = ScreenTransform[1] + ScreenTransform[3] * y1 gl.Uniform4f(self.uPosTransform, tx0, ty0, tx1 - tx0, ty1 - ty0) gl.Uniform(self.uColor0, color0) gl.Uniform(self.uColor1, color1) SimpleQuad.draw() RequiredShaders.append(ProgressBarShader) ##### RENDERING TOOL CODE ###################################################### # meshes for highlight boxes and the spotlight are laid out in the same manner: # - vertex 0 is the center vertex # - for each slice, there are two further vertices: # - vertex 2*i+1 is the "inner" vertex with full alpha # - vertex 2*i+2 is the "outer" vertex with zero alpha class HighlightIndexBuffer(object): def __init__(self, npoints, reuse_buf=None, dynamic=False): if not reuse_buf: self.buf = gl.GenBuffers() elif isinstance(reuse_buf, HighlightIndexBuffer): self.buf = reuse_buf.buf else: self.buf = reuse_buf data = [] for i in xrange(npoints): if i: b0 = 2 * i - 1 else: b0 = 2 * npoints - 1 b1 = 2 * i + 1 data.extend([ 0, b1, b0, b1, b1+1, b0, b1+1, b0+1, b0 ]) self.vertices = 9 * npoints if dynamic: usage = gl.DYNAMIC_DRAW else: usage = gl.STATIC_DRAW gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.buf) gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, data=data, type=gl.UNSIGNED_SHORT, usage=usage) def draw(self): gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.buf) gl.DrawElements(gl.TRIANGLES, self.vertices, gl.UNSIGNED_SHORT, 0) def GenerateSpotMesh(): global SpotVertices, SpotIndices rx0 = SpotRadius * PixelX ry0 = SpotRadius * PixelY rx1 = (SpotRadius + BoxEdgeSize) * PixelX ry1 = (SpotRadius + BoxEdgeSize) * PixelY slices = max(MinSpotDetail, int(2.0 * pi * SpotRadius / SpotDetail / ZoomArea)) SpotIndices = HighlightIndexBuffer(slices, reuse_buf=SpotIndices, dynamic=True) vertices = [0.0, 0.0, 1.0] for i in xrange(slices): a = i * 2.0 * pi / slices vertices.extend([ rx0 * sin(a), ry0 * cos(a), 1.0, rx1 * sin(a), ry1 * cos(a), 0.0 ]) if not SpotVertices: SpotVertices = gl.GenBuffers() gl.BindBuffer(gl.ARRAY_BUFFER, SpotVertices) gl.BufferData(gl.ARRAY_BUFFER, data=vertices, usage=gl.DYNAMIC_DRAW) ##### TRANSITIONS ############################################################## # base class for all transitions class Transition(object): # constructor: must instantiate (i.e. compile) all required shaders # and (optionally) perform some additional initialization def __init__(self): pass # called once at the start of each transition def start(self): pass # render a frame of the transition, using the relative time 't' and the # global texture identifiers Tcurrent and Tnext def render(self, t): pass # smoothstep() makes most transitions better :) def smoothstep(t): return t * t * (3.0 - 2.0 * t) # an array containing all possible transition classes AllTransitions = [] class Crossfade(Transition): """simple crossfade""" class CrossfadeShader(GLShader): vs = """ attribute highp vec2 aPos; uniform highp vec4 uTexTransform; varying mediump vec2 vTexCoord; void main() { gl_Position = vec4(vec2(-1.0, 1.0) + aPos * vec2(2.0, -2.0), 0.0, 1.0); vTexCoord = uTexTransform.xy + aPos * uTexTransform.zw; } """ fs = """ uniform lowp sampler2D uTcurrent; uniform lowp sampler2D uTnext; uniform lowp float uTime; varying mediump vec2 vTexCoord; void main() { gl_FragColor = mix(texture2D(uTcurrent, vTexCoord), texture2D(uTnext, vTexCoord), uTime); } """ attributes = { 0: 'aPos' } uniforms = [('uTnext', 1), 'uTexTransform', 'uTime'] def __init__(self): shader = self.CrossfadeShader.get_instance().use() gl.Uniform4f(shader.uTexTransform, 0.0, 0.0, TexMaxS, TexMaxT) def render(self, t): shader = self.CrossfadeShader.get_instance().use() gl.set_texture(gl.TEXTURE_2D, Tnext, 1) gl.set_texture(gl.TEXTURE_2D, Tcurrent, 0) gl.Uniform1f(shader.uTime, t) SimpleQuad.draw() AllTransitions.append(Crossfade) class FadeOutFadeIn(Transition): "fade out to black and fade in again" def render(self, t): if t < 0.5: tex = Tcurrent t = 1.0 - 2.0 * t else: tex = Tnext t = 2.0 * t - 1.0 TexturedRectShader.get_instance().draw( 0.0, 0.0, 1.0, 1.0, s1=TexMaxS, t1=TexMaxT, tex=tex, color=(t, t, t, 1.0) ) AllTransitions.append(FadeOutFadeIn) class Slide(Transition): def render(self, t): t = smoothstep(t) x = self.dx * t y = self.dy * t TexturedRectShader.get_instance().draw( x, y, x + 1.0, y + 1.0, s1=TexMaxS, t1=TexMaxT, tex=Tcurrent ) TexturedRectShader.get_instance().draw( x - self.dx, y - self.dy, x - self.dx + 1.0, y - self.dy + 1.0, s1=TexMaxS, t1=TexMaxT, tex=Tnext ) class SlideUp(Slide): "slide upwards" dx, dy = 0.0, -1.0 class SlideDown(Slide): "slide downwards" dx, dy = 0.0, 1.0 class SlideLeft(Slide): "slide to the left" dx, dy = -1.0, 0.0 class SlideRight(Slide): "slide to the right" dx, dy = 1.0, 0.0 AllTransitions.extend([SlideUp, SlideDown, SlideLeft, SlideRight]) class Squeeze(Transition): def render(self, t): for tex, x0, y0, x1, y1 in self.getparams(smoothstep(t)): TexturedRectShader.get_instance().draw( x0, y0, x1, y1, s1=TexMaxS, t1=TexMaxT, tex=tex ) class SqueezeUp(Squeeze): "squeeze upwards" def getparams(self, t): return ((Tcurrent, 0.0, 0.0, 1.0, 1.0 - t), (Tnext, 0.0, 1.0 - t, 1.0, 1.0)) class SqueezeDown(Squeeze): "squeeze downwards" def getparams(self, t): return ((Tcurrent, 0.0, t, 1.0, 1.0), (Tnext, 0.0, 0.0, 1.0, t)) class SqueezeLeft(Squeeze): "squeeze to the left" def getparams(self, t): return ((Tcurrent, 0.0, 0.0, 1.0 - t, 1.0), (Tnext, 1.0 - t, 0.0, 1.0, 1.0)) class SqueezeRight(Squeeze): "squeeze to the right" def getparams(self, t): return ((Tcurrent, t, 0.0, 1.0, 1.0), (Tnext, 0.0, 0.0, t, 1.0)) AllTransitions.extend([SqueezeUp, SqueezeDown, SqueezeLeft, SqueezeRight]) class Wipe(Transition): band_size = 0.5 # relative size of the wiping band rx, ry = 16, 16 # mask texture resolution class_mask = True # True if the mask shall be shared between all instances of this subclass class WipeShader(GLShader): vs = """ attribute highp vec2 aPos; uniform highp vec4 uTexTransform; uniform highp vec4 uMaskTransform; varying mediump vec2 vTexCoord; varying mediump vec2 vMaskCoord; void main() { gl_Position = vec4(vec2(-1.0, 1.0) + aPos * vec2(2.0, -2.0), 0.0, 1.0); vTexCoord = uTexTransform.xy + aPos * uTexTransform.zw; vMaskCoord = uMaskTransform.xy + aPos * uMaskTransform.zw; } """ fs = """ uniform lowp sampler2D uTcurrent; uniform lowp sampler2D uTnext; uniform mediump sampler2D uMaskTex; uniform mediump vec2 uAlphaTransform; varying mediump vec2 vTexCoord; varying mediump vec2 vMaskCoord; void main() { mediump float mask = texture2D(uMaskTex, vMaskCoord).r; mask = (mask + uAlphaTransform.x) * uAlphaTransform.y; mask = smoothstep(0.0, 1.0, mask); gl_FragColor = mix(texture2D(uTnext, vTexCoord), texture2D(uTcurrent, vTexCoord), mask); // gl_FragColor = texture2D(uMaskTex, vMaskCoord); // uncomment for mask debugging } """ attributes = { 0: 'aPos' } uniforms = [('uTnext', 1), ('uMaskTex', 2), 'uTexTransform', 'uMaskTransform', 'uAlphaTransform'] def __init__(self): GLShader.__init__(self) self.mask_tex = gl.make_texture(gl.TEXTURE_2D, gl.CLAMP_TO_EDGE, gl.LINEAR) mask = None def __init__(self): shader = self.WipeShader.get_instance().use() gl.Uniform4f(shader.uTexTransform, 0.0, 0.0, TexMaxS, TexMaxT) if not self.class_mask: self.mask = self.prepare_mask() elif not self.mask: self.__class__.mask = self.prepare_mask() def start(self): shader = self.WipeShader.get_instance().use() gl.Uniform4f(shader.uMaskTransform, 0.5 / self.rx, 0.5 / self.ry, 1.0 - 1.0 / self.rx, 1.0 - 1.0 / self.ry) gl.BindTexture(gl.TEXTURE_2D, shader.mask_tex) gl.TexImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, self.rx, self.ry, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, self.mask) def bind_mask_tex(self, shader): gl.set_texture(gl.TEXTURE_2D, shader.mask_tex, 2) def render(self, t): shader = self.WipeShader.get_instance().use() self.bind_mask_tex(shader) # own method b/c WipeBrightness overrides it gl.set_texture(gl.TEXTURE_2D, Tnext, 1) gl.set_texture(gl.TEXTURE_2D, Tcurrent, 0) gl.Uniform2f(shader.uAlphaTransform, self.band_size - t * (1.0 + self.band_size), 1.0 / self.band_size) SimpleQuad.draw() def prepare_mask(self): scale = 1.0 / (self.rx - 1) xx = [i * scale for i in xrange((self.rx + 3) & (~3))] scale = 1.0 / (self.ry - 1) yy = [i * scale for i in xrange(self.ry)] def iter2d(): for y in yy: for x in xx: yield (x, y) return ''.join(chr(max(0, min(255, int(self.f(x, y) * 255.0 + 0.5)))) for x, y in iter2d()) def f(self, x, y): return 0.5 class WipeLeft(Wipe): "wipe from right to left" def f(self, x, y): return 1.0 - x class WipeRight(Wipe): "wipe from left to right" def f(self, x, y): return x class WipeUp(Wipe): "wipe upwards" def f(self, x, y): return 1.0 - y class WipeDown(Wipe): "wipe downwards" def f(self, x, y): return y class WipeUpLeft(Wipe): "wipe from the lower-right to the upper-left corner" def f(self, x, y): return 1.0 - 0.5 * (x + y) class WipeUpRight(Wipe): "wipe from the lower-left to the upper-right corner" def f(self, x, y): return 0.5 * (1.0 - y + x) class WipeDownLeft(Wipe): "wipe from the upper-right to the lower-left corner" def f(self, x, y): return 0.5 * (1.0 - x + y) class WipeDownRight(Wipe): "wipe from the upper-left to the lower-right corner" def f(self, x, y): return 0.5 * (x + y) class WipeCenterOut(Wipe): "wipe from the center outwards" rx, ry = 64, 32 def __init__(self): self.scale = 1.0 self.scale = 1.0 / self.f(0.0, 0.0) Wipe.__init__(self) def f(self, x, y): return hypot((x - 0.5) * DAR, y - 0.5) * self.scale class WipeCenterIn(Wipe): "wipe from the corners inwards" rx, ry = 64, 32 def __init__(self): self.scale = 1.0 self.scale = 1.0 / (1.0 - self.f(0.0, 0.0)) Wipe.__init__(self) def f(self, x, y): return 1.0 - hypot((x - 0.5) * DAR, y - 0.5) * self.scale class WipeBlobs(Wipe): """wipe using nice "blob"-like patterns""" rx, ry = 64, 32 class_mask = False def __init__(self): self.x0 = random.random() * 6.2 self.y0 = random.random() * 6.2 self.sx = (random.random() * 15.0 + 5.0) * DAR self.sy = random.random() * 15.0 + 5.0 Wipe.__init__(self) def f(self, x, y): return 0.5 + 0.25 * (cos(self.x0 + self.sx * x) + cos(self.y0 + self.sy * y)) class WipeClouds(Wipe): """wipe using cloud-like patterns""" rx, ry = 128, 128 class_mask = False decay = 0.25 blur = 5 def prepare_mask(self): assert self.rx == self.ry noise = str2img('L', (self.rx * 4, self.ry * 2), ''.join(map(chr, (random.randrange(256) for i in xrange(self.rx * self.ry * 8))))) img = Image.new('L', (1, 1), random.randrange(256)) alpha = 1.0 npos = 0 border = 0 while img.size[0] <= self.rx: border += 2 next = img.size[0] * 2 alpha *= self.decay img = Image.blend( img.resize((next, next), Image.BILINEAR), noise.crop((npos, 0, npos + next, next)), alpha) npos += next img = ImageOps.equalize(ImageOps.autocontrast(img)) for i in xrange(self.blur): img = img.filter(ImageFilter.BLUR) img = img.crop((border, border, img.size[0] - 2 * border, img.size[1] - 2 * border)).resize((self.rx, self.ry), Image.ANTIALIAS) return img2str(img) class WipeBrightness1(Wipe): """wipe based on the current slide's brightness""" band_size = 1.0 def prepare_mask(self): return True # dummy def start(self): shader = self.WipeShader.get_instance().use() gl.Uniform4f(shader.uMaskTransform, 0.0, 0.0, TexMaxS, TexMaxT) def bind_mask_tex(self, dummy): gl.set_texture(gl.TEXTURE_2D, Tcurrent, 2) class WipeBrightness2(WipeBrightness1): """wipe based on the next slide's brightness""" def bind_mask_tex(self, dummy): gl.set_texture(gl.TEXTURE_2D, Tnext, 2) AllTransitions.extend([WipeLeft, WipeRight, WipeUp, WipeDown, WipeUpLeft, WipeUpRight, WipeDownLeft, WipeDownRight, WipeCenterOut, WipeCenterIn, WipeBlobs, WipeClouds, WipeBrightness1, WipeBrightness2]) class PagePeel(Transition): "an unrealistic, but nice page peel effect" class PagePeel_PeeledPageShader(GLShader): vs = """ attribute highp vec2 aPos; uniform highp vec4 uPosTransform; varying mediump vec2 vTexCoord; void main() { highp vec2 pos = uPosTransform.xy + aPos * uPosTransform.zw; gl_Position = vec4(vec2(-1.0, 1.0) + pos * vec2(2.0, -2.0), 0.0, 1.0); vTexCoord = aPos + vec2(0.0, -0.5); } """ fs = """ uniform lowp sampler2D uTex; uniform highp vec4 uTexTransform; uniform highp float uHeight; uniform mediump float uShadowStrength; varying mediump vec2 vTexCoord; void main() { mediump vec2 tc = vTexCoord; tc.y *= 1.0 - tc.x * uHeight; tc.x = mix(tc.x, tc.x * tc.x, uHeight); tc = uTexTransform.xy + (tc + vec2(0.0, 0.5)) * uTexTransform.zw; mediump float shadow_pos = 1.0 - vTexCoord.x; mediump float light = 1.0 - (shadow_pos * shadow_pos) * uShadowStrength; gl_FragColor = vec4(light, light, light, 1.0) * texture2D(uTex, tc); } """ attributes = { 0: 'aPos' } uniforms = ['uPosTransform', 'uTexTransform', 'uHeight', 'uShadowStrength'] class PagePeel_RevealedPageShader(GLShader): vs = """ attribute highp vec2 aPos; uniform highp vec4 uPosTransform; uniform highp vec4 uTexTransform; varying mediump vec2 vTexCoord; varying mediump float vShadowPos; void main() { highp vec2 pos = uPosTransform.xy + aPos * uPosTransform.zw; gl_Position = vec4(vec2(-1.0, 1.0) + pos * vec2(2.0, -2.0), 0.0, 1.0); vShadowPos = 1.0 - aPos.x; vTexCoord = uTexTransform.xy + aPos * uTexTransform.zw; } """ fs = """ uniform lowp sampler2D uTex; uniform mediump float uShadowStrength; varying mediump vec2 vTexCoord; varying mediump float vShadowPos; void main() { mediump float light = 1.0 - (vShadowPos * vShadowPos) * uShadowStrength; gl_FragColor = vec4(light, light, light, 1.0) * texture2D(uTex, vTexCoord); } """ attributes = { 0: 'aPos' } uniforms = ['uPosTransform', 'uTexTransform', 'uShadowStrength'] def __init__(self): shader = self.PagePeel_PeeledPageShader.get_instance().use() gl.Uniform4f(shader.uTexTransform, 0.0, 0.0, TexMaxS, TexMaxT) self.PagePeel_RevealedPageShader.get_instance() def render(self, t): angle = t * 0.5 * pi split = cos(angle) height = sin(angle) # draw the old page that is peeled away gl.BindTexture(gl.TEXTURE_2D, Tcurrent) shader = self.PagePeel_PeeledPageShader.get_instance().use() gl.Uniform4f(shader.uPosTransform, 0.0, 0.0, split, 1.0) gl.Uniform1f(shader.uHeight, height * 0.25) gl.Uniform1f(shader.uShadowStrength, 0.2 * (1.0 - split)); SimpleQuad.draw() # draw the new page that is revealed gl.BindTexture(gl.TEXTURE_2D, Tnext) shader = self.PagePeel_RevealedPageShader.get_instance().use() gl.Uniform4f(shader.uPosTransform, split, 0.0, 1.0 - split, 1.0) gl.Uniform4f(shader.uTexTransform, split * TexMaxS, 0.0, (1.0 - split) * TexMaxS, TexMaxT) gl.Uniform1f(shader.uShadowStrength, split); SimpleQuad.draw() AllTransitions.append(PagePeel) # the AvailableTransitions array contains a list of all transition classes that # can be randomly assigned to pages; # this selection normally only includes "unintrusive" transtitions, i.e. mostly # crossfade/wipe variations AvailableTransitions = [ # from coolest to lamest WipeBlobs, WipeCenterOut, WipeDownRight, WipeRight, WipeDown ] ##### OSD FONT RENDERER ######################################################## # force a string or sequence of ordinals into a unicode string def ForceUnicode(s, charset='iso8859-15'): if type(s) == types.UnicodeType: return s if type(s) == types.StringType: return unicode(s, charset, 'ignore') if type(s) in (types.TupleType, types.ListType): return u''.join(map(unichr, s)) raise TypeError, "string argument not convertible to Unicode" # search a system font path for a font file def SearchFont(root, name): if not os.path.isdir(root): return None infix = "" fontfile = [] while (len(infix) < 10) and not(fontfile): fontfile = filter(os.path.isfile, glob.glob(root + infix + name)) infix += "*/" if not fontfile: return None else: return fontfile[0] # load a system font def LoadFont(dirs, name, size): # first try to load the font directly try: return ImageFont.truetype(name, size, encoding='unic') except: pass # no need to search further on Windows if os.name == 'nt': return None # start search for the font for dir in dirs: fontfile = SearchFont(dir + "/", name) if fontfile: try: return ImageFont.truetype(fontfile, size, encoding='unic') except: pass return None # alignment constants Left = 0 Right = 1 Center = 2 Down = 0 Up = 1 Auto = -1 # font renderer class class GLFont: def __init__(self, width, height, name, size, search_path=[], default_charset='iso8859-15', extend=1, blur=1): self.width = width self.height = height self._i_extend = range(extend) self._i_blur = range(blur) self.feather = extend + blur + 1 self.current_x = 0 self.current_y = 0 self.max_height = 0 self.boxes = {} self.widths = {} self.line_height = 0 self.default_charset = default_charset if isinstance(name, basestring): self.font = LoadFont(search_path, name, size) else: for check_name in name: self.font = LoadFont(search_path, check_name, size) if self.font: break if not self.font: raise IOError, "font file not found" self.img = Image.new('LA', (width, height)) self.alpha = Image.new('L', (width, height)) self.extend = ImageFilter.MaxFilter() self.blur = ImageFilter.Kernel((3, 3), [1,2,1,2,4,2,1,2,1]) self.tex = gl.make_texture(gl.TEXTURE_2D, filter=gl.NEAREST) self.AddString(range(32, 128)) self.vertices = None self.index_buffer = None self.index_buffer_capacity = 0 def AddCharacter(self, c): w, h = self.font.getsize(c) try: ox, oy = self.font.getoffset(c) w += ox h += oy except AttributeError: pass self.line_height = max(self.line_height, h) size = (w + 2 * self.feather, h + 2 * self.feather) glyph = Image.new('L', size) draw = ImageDraw.Draw(glyph) draw.text((self.feather, self.feather), c, font=self.font, fill=255) del draw box = self.AllocateGlyphBox(*size) self.img.paste(glyph, (box.orig_x, box.orig_y)) for i in self._i_extend: glyph = glyph.filter(self.extend) for i in self._i_blur: glyph = glyph.filter(self.blur) self.alpha.paste(glyph, (box.orig_x, box.orig_y)) self.boxes[c] = box self.widths[c] = w del glyph def AddString(self, s, charset=None, fail_silently=False): update_count = 0 try: for c in ForceUnicode(s, self.GetCharset(charset)): if c in self.widths: continue self.AddCharacter(c) update_count += 1 except ValueError: if fail_silently: pass else: raise if not update_count: return self.img.putalpha(self.alpha) gl.load_texture(gl.TEXTURE_2D, self.tex, self.img) def AllocateGlyphBox(self, w, h): if self.current_x + w > self.width: self.current_x = 0 self.current_y += self.max_height self.max_height = 0 if self.current_y + h > self.height: raise ValueError, "bitmap too small for all the glyphs" box = self.GlyphBox() box.orig_x = self.current_x box.orig_y = self.current_y box.size_x = w box.size_y = h box.x0 = self.current_x / float(self.width) box.y0 = self.current_y / float(self.height) box.x1 = (self.current_x + w) / float(self.width) box.y1 = (self.current_y + h) / float(self.height) box.dsx = w * PixelX box.dsy = h * PixelY self.current_x += w self.max_height = max(self.max_height, h) return box def GetCharset(self, charset=None): if charset: return charset return self.default_charset def SplitText(self, s, charset=None): return ForceUnicode(s, self.GetCharset(charset)).split(u'\n') def GetLineHeight(self): return self.line_height def GetTextWidth(self, s, charset=None): return max([self.GetTextWidthEx(line) for line in self.SplitText(s, charset)]) def GetTextHeight(self, s, charset=None): return len(self.SplitText(s, charset)) * self.line_height def GetTextSize(self, s, charset=None): lines = self.SplitText(s, charset) return (max([self.GetTextWidthEx(line) for line in lines]), len(lines) * self.line_height) def GetTextWidthEx(self, u): if u: return sum([self.widths.get(c, 0) for c in u]) else: return 0 def GetTextHeightEx(self, u=[]): return self.line_height def AlignTextEx(self, x, u, align=Left): if not align: return x return x - (self.GetTextWidthEx(u) / align) class FontShader(GLShader): vs = """ attribute highp vec4 aPosAndTexCoord; varying mediump vec2 vTexCoord; void main() { gl_Position = vec4(vec2(-1.0, 1.0) + aPosAndTexCoord.xy * vec2(2.0, -2.0), 0.0, 1.0); vTexCoord = aPosAndTexCoord.zw; } """ fs = """ uniform lowp sampler2D uTex; uniform lowp vec4 uColor; varying mediump vec2 vTexCoord; void main() { gl_FragColor = uColor * texture2D(uTex, vTexCoord); } """ attributes = { 0: 'aPosAndTexCoord' } uniforms = ['uColor'] def BeginDraw(self): self.vertices = [] def EndDraw(self, color=(1.0, 1.0, 1.0), alpha=1.0, beveled=True): if not self.vertices: self.vertices = None return char_count = len(self.vertices) / 16 if char_count > 16383: print >>sys.stderr, "Internal Error: too many characters (%d) to display in one go, truncating." % char_count char_count = 16383 # create an index buffer large enough for the text if not(self.index_buffer) or (self.index_buffer_capacity < char_count): self.index_buffer_capacity = (char_count + 63) & (~63) data = [] for b in xrange(0, self.index_buffer_capacity * 4, 4): data.extend([b+0, b+2, b+1, b+1, b+2, b+3]) if not self.index_buffer: self.index_buffer = gl.GenBuffers() gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.index_buffer) gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, data=data, type=gl.UNSIGNED_SHORT, usage=gl.DYNAMIC_DRAW) else: gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.index_buffer) # set the vertex buffer vbuf = (c_float * len(self.vertices))(*self.vertices) gl.BindBuffer(gl.ARRAY_BUFFER, 0) gl.set_enabled_attribs(0) gl.VertexAttribPointer(0, 4, gl.FLOAT, False, 0, vbuf) # draw it shader = self.FontShader.get_instance().use() gl.BindTexture(gl.TEXTURE_2D, self.tex) if beveled: gl.BlendFunc(gl.ZERO, gl.ONE_MINUS_SRC_ALPHA) gl.Uniform4f(shader.uColor, 0.0, 0.0, 0.0, alpha) gl.DrawElements(gl.TRIANGLES, char_count * 6, gl.UNSIGNED_SHORT, 0) gl.BlendFunc(gl.ONE, gl.ONE) gl.Uniform4f(shader.uColor, color[0] * alpha, color[1] * alpha, color[2] * alpha, 1.0) gl.DrawElements(gl.TRIANGLES, char_count * 6, gl.UNSIGNED_SHORT, 0) gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) self.vertices = None def Draw(self, origin, text, charset=None, align=Left, color=(1.0, 1.0, 1.0), alpha=1.0, beveled=True, bold=False): own_draw = (self.vertices is None) if own_draw: self.BeginDraw() lines = self.SplitText(text, charset) x0, y = origin x0 -= self.feather y -= self.feather for line in lines: sy = y * PixelY x = self.AlignTextEx(x0, line, align) for c in line: if not c in self.widths: continue self.boxes[c].add_vertices(self.vertices, x * PixelX, sy) x += self.widths[c] y += self.line_height if bold and not(beveled): self.Draw((origin[0] + 1, origin[1]), text, charset=charset, align=align, color=color, alpha=alpha, beveled=False, bold=False) if own_draw: self.EndDraw(color, alpha, beveled) class GlyphBox: def add_vertices(self, vertex_list, sx=0.0, sy=0.0): vertex_list.extend([ sx, sy, self.x0, self.y0, sx + self.dsx, sy, self.x1, self.y0, sx, sy + self.dsy, self.x0, self.y1, sx + self.dsx, sy + self.dsy, self.x1, self.y1, ]) # high-level draw function def DrawOSD(x, y, text, halign=Auto, valign=Auto, alpha=1.0): if not(OSDFont) or not(text) or (alpha <= 0.004): return if alpha > 1.0: alpha = 1.0 if halign == Auto: if x < 0: x += ScreenWidth halign = Right else: halign = Left if HalfScreen and (halign == Left): x += ScreenWidth / 2 if valign == Auto: if y < 0: y += ScreenHeight valign = Up else: valign = Down if valign != Down: y -= OSDFont.GetLineHeight() / valign OSDFont.Draw((x, y), text, align=halign, alpha=alpha) # very high-level draw function def DrawOSDEx(position, text, alpha_factor=1.0): xpos = position >> 1 y = (1 - 2 * (position & 1)) * OSDMargin if xpos < 2: x = (1 - 2 * xpos) * OSDMargin halign = Auto else: x = ScreenWidth / 2 halign = Center DrawOSD(x, y, text, halign, alpha = OSDAlpha * alpha_factor) RequiredShaders.append(GLFont.FontShader) ##### PDF PARSER ############################################################### class PDFError(Exception): pass class PDFref: def __init__(self, ref): self.ref = ref def __repr__(self): return "PDFref(%d)" % self.ref re_pdfstring = re.compile(r'\(\)|\(.*?[^\\]\)') pdfstringrepl = [("\\"+x[0], x[1:]) for x in "(( )) n\n r\r t\t".split(" ")] def pdf_maskstring(s): s = s[1:-1] for a, b in pdfstringrepl: s = s.replace(a, b) return " <" + "".join(["%02X"%ord(c) for c in s]) + "> " def pdf_mask_all_strings(s): return re_pdfstring.sub(lambda x: pdf_maskstring(x.group(0)), s) def pdf_unmaskstring(s): return "".join([chr(int(s[i:i+2], 16)) for i in xrange(1, len(s)-1, 2)]) class PDFParser: def __init__(self, filename): self.f = file(filename, "rb") self.errors = 0 # find the first cross-reference table self.f.seek(0, 2) filesize = self.f.tell() self.f.seek(filesize - 128) trailer = self.f.read() i = trailer.rfind("startxref") if i < 0: raise PDFError, "cross-reference table offset missing" try: offset = int(trailer[i:].split("\n")[1].strip()) except (IndexError, ValueError): raise PDFError, "malformed cross-reference table offset" # follow the trailer chain self.xref = {} while offset: newxref = self.xref self.xref, rootref, offset = self.parse_trailer(offset) self.xref.update(newxref) # scan the page and names tree self.obj2page = {} self.page2obj = {} self.annots = {} self.page_count = 0 self.box = {} self.names = {} self.rotate = {} root = self.getobj(rootref, 'Catalog') try: self.scan_page_tree(root['Pages'].ref) except KeyError: raise PDFError, "root page tree node missing" try: self.scan_names_tree(root['Names'].ref) except KeyError: pass def getline(self): while True: line = self.f.readline().strip() if line: return line def find_length(self, tokens, begin, end): level = 1 for i in xrange(1, len(tokens)): if tokens[i] == begin: level += 1 if tokens[i] == end: level -= 1 if not level: break return i + 1 def parse_tokens(self, tokens, want_list=False): res = [] while tokens: t = tokens[0] v = t tlen = 1 if (len(tokens) >= 3) and (tokens[2] == 'R'): v = PDFref(int(t)) tlen = 3 elif t == "<<": tlen = self.find_length(tokens, "<<", ">>") v = self.parse_tokens(tokens[1 : tlen - 1], True) v = dict(zip(v[::2], v[1::2])) elif t == "[": tlen = self.find_length(tokens, "[", "]") v = self.parse_tokens(tokens[1 : tlen - 1], True) elif not(t) or (t[0] == "null"): v = None elif (t[0] == '<') and (t[-1] == '>'): v = pdf_unmaskstring(t) elif t[0] == '/': v = t[1:] elif t == 'null': v = None else: try: v = float(t) v = int(t) except ValueError: pass res.append(v) del tokens[:tlen] if want_list: return res if not res: return None if len(res) == 1: return res[0] return res def parse(self, data): data = pdf_mask_all_strings(data) data = data.replace("<<", " << ").replace("[", " [ ").replace("(", " (") data = data.replace(">>", " >> ").replace("]", " ] ").replace(")", ") ") data = data.replace("/", " /").replace("><", "> <") return self.parse_tokens(filter(None, data.split())) def getobj(self, obj, force_type=None): if isinstance(obj, PDFref): obj = obj.ref if type(obj) != types.IntType: raise PDFError, "object is not a valid reference" offset = self.xref.get(obj, 0) if not offset: raise PDFError, "referenced non-existing PDF object" self.f.seek(offset) header = self.getline().split(None, 3) if (len(header) < 3) or (header[2] != "obj") or (header[0] != str(obj)): raise PDFError, "object does not start where it's supposed to" if len(header) == 4: data = [header[3]] else: data = [] while True: line = self.getline() if line in ("endobj", "stream"): break data.append(line) data = self.parse(" ".join(data)) if force_type: try: t = data['Type'] except (KeyError, IndexError, ValueError): t = None if t != force_type: raise PDFError, "object does not match the intended type" return data def parse_xref_section(self, start, count): xref = {} for obj in xrange(start, start + count): line = self.getline() if line[-1] == 'f': xref[obj] = 0 else: xref[obj] = int(line[:10], 10) return xref def parse_trailer(self, offset): self.f.seek(offset) xref = {} rootref = 0 offset = 0 if self.getline() != "xref": raise PDFError, "cross-reference table does not start where it's supposed to" return (xref, rootref, offset) # no xref table found, abort # parse xref sections while True: line = self.getline() if line == "trailer": break start, count = map(int, line.split()) xref.update(self.parse_xref_section(start, count)) # parse trailer trailer = "" while True: line = self.getline() if line in ("startxref", "%%EOF"): break trailer += line trailer = self.parse(trailer) try: rootref = trailer['Root'].ref except KeyError: raise PDFError, "root catalog entry missing" except AttributeError: raise PDFError, "root catalog entry is not a reference" return (xref, rootref, trailer.get('Prev', 0)) def scan_page_tree(self, obj, mbox=None, cbox=None, rotate=0): try: node = self.getobj(obj) if node['Type'] == 'Pages': for kid in node['Kids']: self.scan_page_tree(kid.ref, \ node.get('MediaBox', mbox), \ node.get('CropBox', cbox), \ node.get('Rotate', 0)) else: page = self.page_count + 1 anode = node.get('Annots', []) if anode.__class__ == PDFref: anode = self.getobj(anode.ref) self.page_count = page self.obj2page[obj] = page self.page2obj[page] = obj self.box[page] = node.get('CropBox', cbox) or node.get('MediaBox', mbox) self.rotate[page] = node.get('Rotate', rotate) self.annots[page] = [a.ref for a in anode] except (KeyError, TypeError, ValueError): self.errors += 1 def scan_names_tree(self, obj, come_from=None, name=None): try: node = self.getobj(obj) # if we came from the root node, proceed to Dests if not come_from: for entry in ('Dests', ): if entry in node: self.scan_names_tree(node[entry], entry) elif come_from == 'Dests': if 'Kids' in node: for kid in node['Kids']: self.scan_names_tree(kid, come_from) elif 'Names' in node: nlist = node['Names'] while (len(nlist) >= 2) \ and (type(nlist[0]) == types.StringType) \ and (nlist[1].__class__ == PDFref): self.scan_names_tree(nlist[1], come_from, nlist[0]) del nlist[:2] elif name and ('D' in node): page = self.dest2page(node['D']) if page: self.names[name] = page # else: unsupported node, don't care except PDFError: self.errors += 1 def dest2page(self, dest): if type(dest) in (types.StringType, types.UnicodeType): return self.names.get(dest, None) if type(dest) != types.ListType: return dest elif dest[0].__class__ == PDFref: return self.obj2page.get(dest[0].ref, None) else: return dest[0] def get_href(self, obj): try: node = self.getobj(obj, 'Annot') if node['Subtype'] != 'Link': return None dest = None if 'Dest' in node: dest = self.dest2page(node['Dest']) elif 'A' in node: a = node['A'] if isinstance(a, PDFref): a = self.getobj(a) action = a['S'] if action == 'URI': dest = a.get('URI', None) for prefix in ("file://", "file:", "run://", "run:"): if dest.startswith(prefix): dest = urllib.unquote(dest[len(prefix):]) break elif action == 'Launch': dest = a.get('F', None) if isinstance(dest, PDFref): dest = self.getobj(dest) if isinstance(dest, dict): dest = dest.get('F', None) or dest.get('Unix', None) if not isinstance(dest, basestring): dest = None # still an unknown type -> ignore it elif action == 'GoTo': dest = self.dest2page(a.get('D', None)) if dest: return tuple(node['Rect'] + [dest]) except PDFError: self.errors += 1 def GetHyperlinks(self): res = {} for page in self.annots: try: a = filter(None, map(self.get_href, self.annots[page])) except (PDFError, TypeError, ValueError): self.errors += 1 a = None if a: res[page] = a return res def rotate_coord(x, y, rot): if rot == 1: x, y = 1.0 - y, x elif rot == 2: x, y = 1.0 - x, 1.0 - y elif rot == 3: x, y = y, 1.0 - x return (x, y) def AddHyperlink(page_offset, page, target, linkbox, pagebox, rotate): page += page_offset if type(target) == types.IntType: target += page_offset # compute relative position of the link on the page w = 1.0 / (pagebox[2] - pagebox[0]) h = 1.0 / (pagebox[3] - pagebox[1]) x0 = (linkbox[0] - pagebox[0]) * w y0 = (pagebox[3] - linkbox[3]) * h x1 = (linkbox[2] - pagebox[0]) * w y1 = (pagebox[3] - linkbox[1]) * h # get effective rotation rotate /= 90 page_rot = GetPageProp(page, 'rotate') if page_rot is None: page_rot = Rotation if page_rot: rotate += page_rot while rotate < 0: rotate += 1000000 rotate &= 3 # rotate the rectangle x0, y0 = rotate_coord(x0, y0, rotate) x1, y1 = rotate_coord(x1, y1, rotate) if x0 > x1: x0, x1 = x1, x0 if y0 > y1: y0, y1 = y1, y0 # save the hyperlink href = (0, target, x0, y0, x1, y1) if GetPageProp(page, '_href'): PageProps[page]['_href'].append(href) else: SetPageProp(page, '_href', [href]) def FixHyperlinks(page): if not(GetPageProp(page, '_box')) or not(GetPageProp(page, '_href')): return # no hyperlinks or unknown page size bx0, by0, bx1, by1 = GetPageProp(page, '_box') bdx = bx1 - bx0 bdy = by1 - by0 href = [] for fixed, target, x0, y0, x1, y1 in GetPageProp(page, '_href'): if fixed: href.append((1, target, x0, y0, x1, y1)) else: href.append((1, target, \ int(bx0 + bdx * x0), int(by0 + bdy * y0), \ int(bx0 + bdx * x1), int(by0 + bdy * y1))) SetPageProp(page, '_href', href) def ParsePDF(filename): if Bare or not(TempFileName): return uncompressed = TempFileName + ".pdf" analyze = filename # uncompress the file with either mutool or pdftk ok = False err = False for args in [ # prefer mutool over pdftk, as it's much faster and doesn't force-decompress images [mutoolPath, "clean", "-g", "-d", "-i", "-f", filename, uncompressed], [pdftkPath, filename, "output", uncompressed, "uncompress"], ]: if not args[0]: continue # program not found try: assert 0 == subprocess.Popen(args).wait() err = not(os.path.isfile(uncompressed)) except (OSError, AssertionError): err = True if not err: ok = True analyze = uncompressed break if ok: pass elif err: print >>sys.stderr, "Note: error while unpacking the PDF file, hyperlinks disabled." return else: print >>sys.stderr, "Note: neither mutool nor pdftk found, hyperlinks disabled." return count = 0 try: try: pdf = PDFParser(analyze) for page, annots in pdf.GetHyperlinks().iteritems(): for page_offset in FileProps[filename]['offsets']: for a in annots: AddHyperlink(page_offset, page, a[4], a[:4], pdf.box[page], pdf.rotate[page]) FixHyperlinks(page + page_offset) count += len(annots) if pdf.errors: print >>sys.stderr, "Note: failed to parse the PDF file, hyperlinks might not work properly" del pdf return count except IOError: print >>sys.stderr, "Note: intermediate PDF file not readable, hyperlinks disabled." except PDFError, e: print >>sys.stderr, "Note: error in PDF file, hyperlinks disabled." print >>sys.stderr, " PDF parser error message:", e finally: try: os.remove(uncompressed) except OSError: pass ##### PAGE CACHE MANAGEMENT #################################################### # helper class that allows PIL to write and read image files with an offset class IOWrapper: def __init__(self, f, offset=0): self.f = f self.offset = offset self.f.seek(offset) def read(self, count=None): if count is None: return self.f.read() else: return self.f.read(count) def write(self, data): self.f.write(data) def seek(self, pos, whence=0): assert(whence in (0, 1)) if whence: self.f.seek(pos, 1) else: self.f.seek(pos + self.offset) def tell(self): return self.f.tell() - self.offset # generate a "magic number" that is used to identify persistent cache files def UpdateCacheMagic(): global CacheMagic pool = [PageCount, ScreenWidth, ScreenHeight, b2s(Scaling), b2s(Supersample), b2s(Rotation)] flist = list(FileProps.keys()) flist.sort(lambda a,b: cmp(a.lower(), b.lower())) for f in flist: pool.append(f) pool.extend(list(GetFileProp(f, 'stat', []))) CacheMagic = md5obj("\0".join(map(str, pool))).hexdigest() # set the persistent cache file position to the current end of the file def UpdatePCachePos(): global CacheFilePos CacheFile.seek(0, 2) CacheFilePos = CacheFile.tell() # rewrite the header of the persistent cache def WritePCacheHeader(reset=False): pages = ["%08x" % PageCache.get(page, 0) for page in range(1, PageCount+1)] CacheFile.seek(0) CacheFile.write(CacheMagic + "".join(pages)) if reset: CacheFile.truncate() UpdatePCachePos() # return an image from the persistent cache or None if none is available def GetPCacheImage(page): if CacheMode != PersistentCache: return # not applicable if persistent cache isn't used Lcache.acquire() try: if page in PageCache: img = Image.open(IOWrapper(CacheFile, PageCache[page])) img.load() return img finally: Lcache.release() # returns an image from the non-persistent cache or None if none is available def GetCacheImage(page): if CacheMode in (NoCache, PersistentCache): return # not applicable in uncached or persistent-cache mode Lcache.acquire() try: if page in PageCache: if CacheMode == FileCache: CacheFile.seek(PageCache[page]) return CacheFile.read(TexSize) elif CacheMode == CompressedCache: return zlib.decompress(PageCache[page]) else: return PageCache[page] finally: Lcache.release() # adds an image to the persistent cache def AddToPCache(page, img): if CacheMode != PersistentCache: return # not applicable if persistent cache isn't used Lcache.acquire() try: if page in PageCache: return # page is already cached and we can't update it safely # -> stop here (the new image will be identical to the old # one anyway) img.save(IOWrapper(CacheFile, CacheFilePos), "ppm") PageCache[page] = CacheFilePos WritePCacheHeader() finally: Lcache.release() # adds an image to the non-persistent cache def AddToCache(page, data): global CacheFilePos if CacheMode in (NoCache, PersistentCache): return # not applicable in uncached or persistent-cache mode Lcache.acquire() try: if CacheMode == FileCache: if not(page in PageCache): PageCache[page] = CacheFilePos CacheFilePos += len(data) CacheFile.seek(PageCache[page]) CacheFile.write(data) elif CacheMode == CompressedCache: PageCache[page] = zlib.compress(data, 1) else: PageCache[page] = data finally: Lcache.release() # invalidates the whole cache def InvalidateCache(): global PageCache, CacheFilePos Lcache.acquire() try: PageCache = {} if CacheMode == PersistentCache: UpdateCacheMagic() WritePCacheHeader(True) else: CacheFilePos = 0 finally: Lcache.release() # initialize the persistent cache def InitPCache(): global CacheFile, CacheMode # try to open the pre-existing cache file try: CacheFile = file(CacheFileName, "rb+") except IOError: CacheFile = None # check the cache magic UpdateCacheMagic() if CacheFile and (CacheFile.read(32) != CacheMagic): print >>sys.stderr, "Cache file mismatch, recreating cache." CacheFile.close() CacheFile = None if CacheFile: # if the magic was valid, import cache data print >>sys.stderr, "Using already existing persistent cache file." for page in range(1, PageCount+1): offset = int(CacheFile.read(8), 16) if offset: PageCache[page] = offset UpdatePCachePos() else: # if the magic was invalid or the file didn't exist, (re-)create it try: CacheFile = file(CacheFileName, "wb+") except IOError: print >>sys.stderr, "Error: cannot write the persistent cache file (`%s')" % CacheFileName print >>sys.stderr, "Falling back to temporary file cache." CacheMode = FileCache WritePCacheHeader() ##### PAGE RENDERING ########################################################### class RenderError(RuntimeError): pass class RendererUnavailable(RenderError): pass class PDFRendererBase(object): name = None binaries = [] test_run_args = [] supports_anamorphic = False required_options = [] needs_tempfile = True @classmethod def supports(self, binary): if not binary: return True binary = os.path.basename(binary).lower() if binary.endswith(".exe"): binary = binary[:-4] return (binary in self.binaries) def __init__(self, binary=None): if self.needs_tempfile and not(TempFileName): raise RendererUnavailable("temporary file creation required, but not available") # search for a working binary and run it to get a list of its options self.command = None for program_spec in map(str.split, ([binary] if binary else self.binaries)): test_binary = FindBinary(program_spec[0]) try: p = subprocess.Popen([test_binary] + program_spec[1:] + self.test_run_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) data = p.stdout.read() p.wait() except OSError: continue self.command = [test_binary] + program_spec[1:] break if not self.command: raise RendererUnavailable("program not found") # parse the output into an option list data = [line.strip().replace('\t', ' ') for line in data.split('\n')] self.options = set([line.split(' ', 1)[0].split('=', 1)[0].strip('-,') for line in data if line.startswith('-')]) if not(set(self.required_options) <= self.options): raise RendererUnavailable("%s does not support all required options" % os.path.basename(self.command[0])) def render(self, filename, page, res, antialias=True): raise RenderError() def execute(self, args, wait=True, redirect=False): args = self.command + args if get_thread_id() == RTrunning: args = Nice + args try: if redirect: process = subprocess.Popen(args, stdout=subprocess.PIPE) else: process = subprocess.Popen(args) if not wait: return process if process.wait() != 0: raise RenderError("rendering failed") except OSError, e: raise RenderError("could not start renderer - %s" % e) def load(self, imgfile, autoremove=False): try: img = Image.open(imgfile) img.load() except (KeyboardInterrupt, SystemExit): raise except IOError, e: raise RenderError("could not read image file - %s" % e) if autoremove: self.remove(imgfile) return img def remove(self, tmpfile): try: os.unlink(tmpfile) except OSError: pass class MuPDFRenderer(PDFRendererBase): name = "MuPDF 1.4 or newer" binaries = ["mudraw", "mutool draw"] test_run_args = [] required_options = ["F", "c", "o", "r"] needs_tempfile = (os.name == 'nt') def render(self, filename, page, res, antialias=True): # direct stdout pipe from mutool on Unix; not possible on Win32 # because mutool does LF->CRLF mangling on the image data pipe = (os.name != 'nt') imgfile = "-" if pipe else (TempFileName + ".ppm") if ("A" in self.options) and not(antialias): aa_opts = ["-A", "0"] else: aa_opts = [] proc = self.execute( ["-F", "pnm", "-c", "rgb", "-o", imgfile, "-r", str(res[0])] \ + aa_opts + [filename, str(page)], wait=not(pipe), redirect=pipe) if pipe: try: out, err = proc.communicate() except EnvironmentError, e: raise RenderError("could not run renderer - %s" % e) if not out: raise RenderError("renderer returned empty image") return self.load(cStringIO.StringIO(out)) else: return self.load(imgfile, autoremove=True) AvailableRenderers.append(MuPDFRenderer) class MuPDFLegacyRenderer(PDFRendererBase): name = "MuPDF (legacy)" binaries = ["mudraw", "pdfdraw"] test_run_args = [] required_options = ["o", "r"] # helper object for communication with the reader thread class ThreadComm(object): def __init__(self, imgfile): self.imgfile = imgfile self.buffer = None self.error = None self.cancel = False def getbuffer(self): if self.buffer: return self.buffer # the reader thread might still be busy reading the last # chunks of the data and converting them into a StringIO; # let's give it some time maxwait = time.time() + (0.1 if self.error else 0.5) while not(self.buffer) and (time.time() < maxwait): time.sleep(0.01) return self.buffer @staticmethod def ReaderThread(comm): try: f = open(comm.imgfile, 'rb') comm.buffer = cStringIO.StringIO(f.read()) f.close() except IOError, e: comm.error = "could not open FIFO for reading - %s" % e def render(self, filename, page, res, antialias=True): imgfile = TempFileName + ".ppm" fifo = False if HaveThreads: self.remove(imgfile) try: os.mkfifo(imgfile) fifo = True comm = self.ThreadComm(imgfile) thread.start_new_thread(self.ReaderThread, (comm, )) except (OSError, IOError, AttributeError): pass if ("b" in self.options) and not(antialias): aa_opts = ["-b", "0"] else: aa_opts = [] try: self.execute([ "-o", imgfile, "-r", str(res[0]), ] + aa_opts + [ filename, str(page) ]) if fifo: if comm.error: raise RenderError(comm.error) if not comm.getbuffer(): raise RenderError("could not read from FIFO") return self.load(comm.buffer, autoremove=False) else: return self.load(imgfile) finally: if fifo: comm.error = True if not comm.getbuffer(): # if rendering failed and the client process didn't write # to the FIFO at all, the reader thread would block in # read() forever; so let's open+close the FIFO to # generate an EOF and thus wake the thead up try: f = open(imgfile, "w") f.close() except IOError: pass self.remove(imgfile) AvailableRenderers.append(MuPDFLegacyRenderer) class XpdfRenderer(PDFRendererBase): name = "Xpdf/Poppler" binaries = ["pdftoppm"] test_run_args = ["-h"] required_options = ["q", "f", "l", "r"] def __init__(self, binary=None): PDFRendererBase.__init__(self, binary) self.supports_anamorphic = ('rx' in self.options) and ('ry' in self.options) def render(self, filename, page, res, antialias=True): if self.supports_anamorphic: args = ["-rx", str(res[0]), "-ry", str(res[1])] else: args = ["-r", str(res[0])] if not antialias: for arg in ("aa", "aaVector"): if arg in self.options: args += ['-'+arg, 'no'] self.execute([ "-q", "-f", str(page), "-l", str(page) ] + args + [ filename, TempFileName ]) digits = GetFileProp(filename, 'digits', 6) try_digits = range(6, 0, -1) try_digits.sort(key=lambda n: abs(n - digits)) try_digits = [(n, TempFileName + ("-%%0%dd.ppm" % n) % page) for n in try_digits] for digits, imgfile in try_digits: if not os.path.exists(imgfile): continue SetFileProp(filename, 'digits', digits) return self.load(imgfile, autoremove=True) raise RenderError("could not find generated image file") AvailableRenderers.append(XpdfRenderer) class GhostScriptRenderer(PDFRendererBase): name = "GhostScript" binaries = ["gs", "gswin32c"] test_run_args = ["--version"] supports_anamorphic = True def render(self, filename, page, res, antialias=True): imgfile = TempFileName + ".tif" aa_bits = (4 if antialias else 1) try: self.execute(["-q"] + GhostScriptPlatformOptions + [ "-dBATCH", "-dNOPAUSE", "-sDEVICE=tiff24nc", "-dUseCropBox", "-sOutputFile=" + imgfile, "-dFirstPage=%d" % page, "-dLastPage=%d" % page, "-r%dx%d" % res, "-dTextAlphaBits=%d" % aa_bits, "-dGraphicsAlphaBits=%s" % aa_bits, filename ]) return self.load(imgfile) finally: self.remove(imgfile) AvailableRenderers.append(GhostScriptRenderer) def InitPDFRenderer(): global PDFRenderer if PDFRenderer: return PDFRenderer fail_reasons = [] for r_class in AvailableRenderers: if not r_class.supports(PDFRendererPath): continue try: PDFRenderer = r_class(PDFRendererPath) print >>sys.stderr, "PDF renderer:", PDFRenderer.name return PDFRenderer except RendererUnavailable, e: if Verbose: print >>sys.stderr, "Not using %s for PDF rendering:" % r_class.name, e else: fail_reasons.append((r_class.name, str(e))) print >>sys.stderr, "ERROR: PDF renderer initialization failed." for item in fail_reasons: print >>sys.stderr, " - %s: %s" % item print >>sys.stderr, " Display of PDF files will not be supported." # generate a dummy image def DummyPage(): img = Image.new('RGB', (ScreenWidth, ScreenHeight)) img.paste(LogoImage, ((ScreenWidth - LogoImage.size[0]) / 2, (ScreenHeight - LogoImage.size[1]) / 2)) return img # load a page from a PDF file def RenderPDF(page, MayAdjustResolution, ZoomMode): if not PDFRenderer: return DummyPage() # load props SourceFile = GetPageProp(page, '_file') RealPage = GetPageProp(page, '_page') OutputSizes = GetPageProp(page, '_out') if not OutputSizes: OutputSizes = GetFileProp(SourceFile, 'out', [(ScreenWidth + Overscan, ScreenHeight + Overscan), (ScreenWidth + Overscan, ScreenHeight + Overscan)]) SetPageProp(page, '_out', OutputSizes) Resolutions = GetPageProp(page, '_res') if not Resolutions: Resolutions = GetFileProp(SourceFile, 'res', [(72.0, 72.0), (72.0, 72.0)]) SetPageProp(page, '_res', Resolutions) rot = GetPageProp(page, 'rotate', Rotation) out = OutputSizes[rot & 1] res = Resolutions[rot & 1] zscale = 1 # handle supersample and zoom mode use_aa = True if ZoomMode: res = (int(ResZoomFactor * res[0]), int(ResZoomFactor * res[1])) out = (int(ResZoomFactor * out[0]), int(ResZoomFactor * out[1])) zscale = ResZoomFactor elif Supersample: res = (Supersample * res[0], Supersample * res[1]) out = (Supersample * out[0], Supersample * out[1]) use_aa = False # prepare the renderer options if PDFRenderer.supports_anamorphic: parscale = False useres = (int(res[0] + 0.5), int(res[1] + 0.5)) else: parscale = (abs(1.0 - PAR) > 0.01) useres = max(res[0], res[1]) res = (useres, useres) useres = int(useres + 0.5) useres = (useres, useres) # call the renderer try: img = PDFRenderer.render(SourceFile, RealPage, useres, use_aa) except RenderError, e: print >>sys.stderr, "ERROR: failed to render page %d:" % page, e return DummyPage() # apply rotation if rot: img = img.rotate(90 * (4 - rot)) # compute final output image size based on PAR if not parscale: got = img.size elif PAR > 1.0: got = (int(img.size[0] / PAR + 0.5), img.size[1]) else: got = (img.size[0], int(img.size[1] * PAR + 0.5)) # if the image size is strange, re-adjust the rendering resolution tolerance = max(4, (ScreenWidth + ScreenHeight) / 400) if MayAdjustResolution and (max(abs(got[0] - out[0]), abs(got[1] - out[1])) >= tolerance): newout = ZoomToFit((img.size[0], img.size[1] * PAR)) rscale = (float(newout[0]) / img.size[0], float(newout[1]) / img.size[1]) if rot & 1: newres = (res[0] * rscale[1], res[1] * rscale[0]) else: newres = (res[0] * rscale[0], res[1] * rscale[1]) # only modify anything if the resolution deviation is large enough if max(abs(1.0 - newres[0] / res[0]), abs(1.0 - newres[1] / res[1])) > 0.05: # create a copy of the old values: they are lists and thus stored # in the PageProps as references; we don't want to influence other # pages though OutputSizes = OutputSizes[:] Resolutions = Resolutions[:] # modify the appropriate rotation slot OutputSizes[rot & 1] = newout Resolutions[rot & 1] = newres # store the new values for this page ... SetPageProp(page, '_out', OutputSizes) SetPageProp(page, '_res', Resolutions) # ... and as a default for the file as well (future pages are likely # to have the same resolution) SetFileProp(SourceFile, 'out', OutputSizes) SetFileProp(SourceFile, 'res', Resolutions) return RenderPDF(page, False, ZoomMode) # downsample a supersampled image if Supersample and not(ZoomMode): img = img.resize((int(float(out[0]) / Supersample + 0.5), int(float(out[1]) / Supersample + 0.5)), Image.ANTIALIAS) parscale = False # don't scale again # perform PAR scaling (required for pdftoppm which doesn't support different # dpi for horizontal and vertical) if parscale: if PAR > 1.0: img = img.resize((int(img.size[0] / PAR + 0.5), img.size[1]), Image.ANTIALIAS) else: img = img.resize((img.size[0], int(img.size[1] * PAR + 0.5)), Image.ANTIALIAS) # crop the overscan (if present) if Overscan: target = (ScreenWidth * zscale, ScreenHeight * zscale) scale = None if (img.size[1] > target[1]) and (img.size[0] < target[0]): scale = float(target[1]) / img.size[1] elif (img.size[0] > target[0]) and (img.size[1] < target[1]): scale = float(target[0]) / img.size[0] if scale: w = int(img.size[0] * scale + 0.5) h = int(img.size[1] * scale + 0.5) if (w <= img.size[0]) and (h <= img.size[1]): x0 = (img.size[0] - w) / 2 y0 = (img.size[1] - h) / 2 img = img.crop((x0, y0, x0 + w, y0 + h)) return img # load a page from an image file def LoadImage(page, zoom=False, img=None): # open the image file with PIL (if not already done so) if not img: try: img = Image.open(GetPageProp(page, '_file')) img.load() except (KeyboardInterrupt, SystemExit): raise except: print >>sys.stderr, "Image file `%s' is broken." % GetPageProp(page, '_file') return DummyPage() # apply rotation rot = GetPageProp(page, 'rotate') if rot is None: rot = Rotation if rot: img = img.rotate(90 * (4 - rot)) # determine destination size newsize = ZoomToFit((img.size[0], int(img.size[1] * PAR + 0.5)), (ScreenWidth, ScreenHeight)) # don't scale if the source size is too close to the destination size if abs(newsize[0] - img.size[0]) < 2: newsize = img.size # don't scale if the source is smaller than the destination if not(Scaling) and (newsize > img.size): newsize = img.size # zoom up (if wanted) if zoom: newsize = (int(ResZoomFactor * newsize[0]), int(ResZoomFactor * newsize[1])) # skip processing if there was no change if newsize == img.size: return img # select a nice filter and resize the image if newsize > img.size: filter = Image.BICUBIC else: filter = Image.ANTIALIAS return img.resize(newsize, filter) # load a preview image from a video file def LoadVideoPreview(page, zoom): global ffmpegWorks, mplayerWorks img = None reason = "no working preview generator application available" if not(img) and ffmpegWorks: try: ffmpegWorks = False reason = "failed to call FFmpeg" out, dummy = subprocess.Popen([ffmpegPath, "-loglevel", "fatal", "-i", GetPageProp(page, '_file'), "-vframes", "1", "-pix_fmt", "rgb24", "-f", "image2pipe", "-vcodec", "ppm", "-"], stdout=subprocess.PIPE).communicate() ffmpegWorks = True reason = "FFmpeg output is not valid" out = cStringIO.StringIO(out) img = Image.open(out) img.load() except (KeyboardInterrupt, SystemExit): raise except EnvironmentError: img = None if not(img) and mplayerWorks and not(Bare): cwd = os.getcwd() try: try: mplayerWorks = False reason = "failed to change into temporary directory" if TempFileName: os.chdir(os.path.dirname(TempFileName)) reason = "failed to call MPlayer" dummy = subprocess.Popen([MPlayerPath, "-really-quiet", "-nosound", "-frames", "1", "-vo", "png", GetPageProp(page, '_file')], stdin=subprocess.PIPE).communicate() mplayerWorks = True reason = "MPlayer output is not valid" img = Image.open("00000001.png") img.load() except (KeyboardInterrupt, SystemExit): raise except EnvironmentError: img = None finally: os.chdir(cwd) if img: return LoadImage(page, zoom, img) else: print >>sys.stderr, "Can not generate preview image for video file `%s' (%s)." % (GetPageProp(page, '_file'), reason) return DummyPage() ffmpegWorks = True mplayerWorks = True # render a page to an OpenGL texture def PageImage(page, ZoomMode=False, RenderMode=False): global OverviewNeedUpdate, HighQualityOverview EnableCacheRead = not(ZoomMode or RenderMode) EnableCacheWrite = EnableCacheRead and \ (page >= PageRangeStart) and (page <= PageRangeEnd) # check for the image in the cache if EnableCacheRead: data = GetCacheImage(page) if data: return data # if it's not in the temporary cache, render it Lrender.acquire() try: # check the cache again, because another thread might have just # rendered the page while we were waiting for the render lock if EnableCacheRead: data = GetCacheImage(page) if data: return data # retrieve the image from the persistent cache or fully re-render it if EnableCacheRead: img = GetPCacheImage(page) else: img = None if not img: if GetPageProp(page, '_page'): img = RenderPDF(page, not(ZoomMode), ZoomMode) elif GetPageProp(page, '_video'): img = LoadVideoPreview(page, ZoomMode) else: img = LoadImage(page, ZoomMode) if GetPageProp(page, 'invert', InvertPages): img = ImageChops.invert(img) if EnableCacheWrite: AddToPCache(page, img) # create black background image to paste real image onto if ZoomMode: TextureImage = Image.new('RGB', (int(ResZoomFactor * TexWidth), int(ResZoomFactor * TexHeight))) TextureImage.paste(img, ((int(ResZoomFactor * ScreenWidth) - img.size[0]) / 2, \ (int(ResZoomFactor * ScreenHeight) - img.size[1]) / 2)) else: TextureImage = Image.new('RGB', (TexWidth, TexHeight)) x0 = (ScreenWidth - img.size[0]) / 2 y0 = (ScreenHeight - img.size[1]) / 2 TextureImage.paste(img, (x0, y0)) SetPageProp(page, '_box', (x0, y0, x0 + img.size[0], y0 + img.size[1])) FixHyperlinks(page) # paste thumbnail into overview image if EnableOverview \ and GetPageProp(page, ('overview', '_overview'), True) \ and (page >= PageRangeStart) and (page <= PageRangeEnd) \ and not(GetPageProp(page, '_overview_rendered')) \ and not(RenderMode): pos = OverviewPos(OverviewPageMapInv[page]) Loverview.acquire() try: # first, fill the underlying area with black (i.e. remove the dummy logo) blackness = Image.new('RGB', (OverviewCellX - OverviewBorder, \ OverviewCellY - OverviewBorder)) OverviewImage.paste(blackness, (pos[0] + OverviewBorder / 2, \ pos[1] + OverviewBorder)) del blackness # then, scale down the original image and paste it if HalfScreen: img = img.crop((0, 0, img.size[0] / 2, img.size[1])) sx = OverviewCellX - 2 * OverviewBorder sy = OverviewCellY - 2 * OverviewBorder if HighQualityOverview: t0 = time.time() img.thumbnail((sx, sy), Image.ANTIALIAS) if (time.time() - t0) > 0.5: print >>sys.stderr, "Note: Your system seems to be quite slow; falling back to a faster," print >>sys.stderr, " but slightly lower-quality overview page rendering mode" HighQualityOverview = False else: img.thumbnail((sx * 2, sy * 2), Image.NEAREST) img.thumbnail((sx, sy), Image.BILINEAR) OverviewImage.paste(img, \ (pos[0] + (OverviewCellX - img.size[0]) / 2, \ pos[1] + (OverviewCellY - img.size[1]) / 2)) finally: Loverview.release() SetPageProp(page, '_overview_rendered', True) OverviewNeedUpdate = True del img # return texture data if RenderMode: return TextureImage data = img2str(TextureImage) del TextureImage finally: Lrender.release() # finally add it back into the cache and return it if EnableCacheWrite: AddToCache(page, data) return data # render a page to an OpenGL texture def RenderPage(page, target): gl.BindTexture(gl.TEXTURE_2D, target) while gl.GetError(): pass # clear all OpenGL errors gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGB, TexWidth, TexHeight, 0, gl.RGB, gl.UNSIGNED_BYTE, PageImage(page)) if gl.GetError(): print >>sys.stderr, "I'm sorry, but your graphics card is not capable of rendering presentations" print >>sys.stderr, "in this resolution. Either the texture memory is exhausted, or there is no" print >>sys.stderr, "support for large textures (%dx%d). Please try to run Impressive in a" % (TexWidth, TexHeight) print >>sys.stderr, "smaller resolution using the -g command-line option." sys.exit(1) # background rendering thread def RenderThread(p1, p2): global RTrunning, RTrestart RTrunning = get_thread_id() or True RTrestart = True while RTrestart: RTrestart = False for pdf in FileProps: if not pdf.lower().endswith(".pdf"): continue if RTrestart: break SafeCall(ParsePDF, [pdf]) if RTrestart: continue for page in xrange(1, PageCount + 1): if RTrestart: break if (page != p1) and (page != p2) \ and (page >= PageRangeStart) and (page <= PageRangeEnd): SafeCall(PageImage, [page]) RTrunning = False if CacheMode >= FileCache: print >>sys.stderr, "Background rendering finished, used %.1f MiB of disk space." %\ (CacheFilePos / 1048576.0) elif CacheMode >= MemCache: print >>sys.stderr, "Background rendering finished, using %.1f MiB of memory." %\ (sum(map(len, PageCache.itervalues())) / 1048576.0) ##### RENDER MODE ############################################################## def DoRender(): global TexWidth, TexHeight TexWidth = ScreenWidth TexHeight = ScreenHeight if os.path.exists(RenderToDirectory): print >>sys.stderr, "Destination directory `%s' already exists," % RenderToDirectory print >>sys.stderr, "refusing to overwrite anything." return 1 try: os.mkdir(RenderToDirectory) except OSError, e: print >>sys.stderr, "Cannot create destination directory `%s':" % RenderToDirectory print >>sys.stderr, e.strerror return 1 print >>sys.stderr, "Rendering presentation into `%s'" % RenderToDirectory for page in xrange(1, PageCount + 1): PageImage(page, RenderMode=True).save("%s/page%04d.png" % (RenderToDirectory, page)) sys.stdout.write("[%d] " % page) sys.stdout.flush() print >>sys.stderr print >>sys.stderr, "Done." return 0 ##### INFO SCRIPT I/O ########################################################## # info script reader def LoadInfoScript(): global PageProps try: os.chdir(os.path.dirname(InfoScriptPath) or BaseWorkingDir) except OSError: pass OldPageProps = PageProps try: execfile(InfoScriptPath, globals()) except IOError: pass except: print >>sys.stderr, "----- Exception in info script ----" traceback.print_exc(file=sys.stderr) print >>sys.stderr, "----- End of traceback -----" NewPageProps = PageProps PageProps = OldPageProps del OldPageProps for page in NewPageProps: for prop in NewPageProps[page]: SetPageProp(page, prop, NewPageProps[page][prop]) del NewPageProps # we can't save lambda expressions, so we need to warn the user # in every possible way ScriptTainted = False LambdaWarning = False def here_was_a_lambda_expression_that_could_not_be_saved(): global LambdaWarning if not LambdaWarning: print >>sys.stderr, "WARNING: The info script for the current file contained lambda expressions that" print >>sys.stderr, " were removed during the a save operation." LambdaWarning = True # "clean" a PageProps entry so that only 'public' properties are left def GetPublicProps(props): props = props.copy() # delete private (underscore) props for prop in list(props.keys()): if str(prop)[0] == '_': del props[prop] # clean props to default values if props.get('overview', False): del props['overview'] if not props.get('skip', True): del props['skip'] if ('boxes' in props) and not(props['boxes']): del props['boxes'] return props # Generate a string representation of a property value. Mainly this converts # classes or instances to the name of the class. def PropValueRepr(value): global ScriptTainted if type(value) == types.FunctionType: if value.__name__ != "": return value.__name__ if not ScriptTainted: print >>sys.stderr, "WARNING: The info script contains lambda expressions, which cannot be saved" print >>sys.stderr, " back. The modifed script will be written into a separate file to" print >>sys.stderr, " minimize data loss." ScriptTainted = True return "here_was_a_lambda_expression_that_could_not_be_saved" elif type(value) == types.ClassType: return value.__name__ elif type(value) == types.InstanceType: return value.__class__.__name__ elif type(value) == types.DictType: return "{ " + ", ".join([PropValueRepr(k) + ": " + PropValueRepr(value[k]) for k in value]) + " }" else: return repr(value) # generate a nicely formatted string representation of a page's properties def SinglePagePropRepr(page): props = GetPublicProps(PageProps[page]) if not props: return None return "\n%3d: {%s\n }" % (page, \ ",".join(["\n " + repr(prop) + ": " + PropValueRepr(props[prop]) for prop in props])) # generate a nicely formatted string representation of all page properties def PagePropRepr(): pages = PageProps.keys() pages.sort() return "PageProps = {%s\n}" % (",".join(filter(None, map(SinglePagePropRepr, pages)))) # count the characters of a python dictionary source code, correctly handling # embedded strings and comments, and nested dictionaries def CountDictChars(s, start=0): context = None level = 0 for i in xrange(start, len(s)): c = s[i] if context is None: if c == '{': level += 1 if c == '}': level -= 1 if c == '#': context = '#' if c == '"': context = '"' if c == "'": context = "'" elif context[0] == "\\": context=context[1] elif context == '#': if c in "\r\n": context = None elif context == '"': if c == "\\": context = "\\\"" if c == '"': context = None elif context == "'": if c == "\\": context = "\\'" if c == "'": context = None if level < 0: return i raise ValueError, "the dictionary never ends" # modify and save a file's info script def SaveInfoScript(filename): # read the old info script try: f = file(filename, "r") script = f.read() f.close() except IOError: script = "" if not script: script = "# -*- coding: iso-8859-1 -*-\n" # replace the PageProps of the old info script with the current ones try: m = re.search("^.*(PageProps)\s*=\s*(\{).*$", script,re.MULTILINE) if m: script = script[:m.start(1)] + PagePropRepr() + \ script[CountDictChars(script, m.end(2)) + 1 :] else: script += "\n" + PagePropRepr() + "\n" except (AttributeError, ValueError): pass if ScriptTainted: filename += ".modified" # write the script back try: f = file(filename, "w") f.write(script) f.close() except: print >>sys.stderr, "Oops! Could not write info script!" ##### OPENGL RENDERING ######################################################### # draw OSD overlays def DrawOverlays(trans_time=0.0): reltime = Platform.GetTicks() - StartTime gl.Enable(gl.BLEND) if (EstimatedDuration or PageProgress or (PageTimeout and AutoAdvanceProgress)) \ and (OverviewMode or GetPageProp(Pcurrent, 'progress', True)): r, g, b = ProgressBarColorPage a = ProgressBarAlpha if PageTimeout and AutoAdvanceProgress: rel = (reltime - PageEnterTime) / float(PageTimeout) if TransitionRunning: a = int(a * (1.0 - TransitionPhase)) elif PageLeaveTime > PageEnterTime: # we'll be called one frame after the transition finished, but # before the new page has been fully activated => don't flash a = 0 elif EstimatedDuration: rel = (0.001 * reltime) / EstimatedDuration if rel < 1.0: r, g, b = ProgressBarColorNormal elif rel < ProgressBarWarningFactor: r, g, b = lerpColor(ProgressBarColorNormal, ProgressBarColorWarning, (rel - 1.0) / (ProgressBarWarningFactor - 1.0)) elif rel < ProgressBarCriticalFactor: r, g, b = lerpColor(ProgressBarColorWarning, ProgressBarColorCritical, (rel - ProgressBarWarningFactor) / (ProgressBarCriticalFactor - ProgressBarWarningFactor)) else: r, g, b = ProgressBarColorCritical else: # must be PageProgress rel = (Pcurrent + trans_time * (Pnext - Pcurrent)) / PageCount if HalfScreen: zero = 0.5 rel = 0.5 + 0.5 * rel else: zero = 0.0 ProgressBarShader.get_instance().draw( zero, 1.0 - ProgressBarSizeFactor, rel, 1.0, color0=(r, g, b, 0.0), color1=(r, g, b, a) ) if OSDFont: OSDFont.BeginDraw() if WantStatus: DrawOSDEx(OSDStatusPos, CurrentOSDStatus) if TimeDisplay: if ShowClock: DrawOSDEx(OSDTimePos, ClockTime(MinutesOnly)) else: t = reltime / 1000 DrawOSDEx(OSDTimePos, FormatTime(t, MinutesOnly)) if CurrentOSDComment and (OverviewMode or not(TransitionRunning)): DrawOSD(ScreenWidth/2, \ ScreenHeight - 3*OSDMargin - FontSize, \ CurrentOSDComment, Center, Up) OSDFont.EndDraw() if EnableCursor and CursorVisible and CursorImage: x, y = Platform.GetMousePos() x -= CursorHotspot[0] y -= CursorHotspot[1] X0 = x * PixelX Y0 = y * PixelY X1 = X0 + CursorSX Y1 = Y0 + CursorSY TexturedRectShader.get_instance().draw( X0, Y0, X1, Y1, s1=CursorTX, t1=CursorTY, tex=CursorTexture ) gl.Disable(gl.BLEND) # draw the complete image of the current page def DrawCurrentPage(dark=1.0, do_flip=True): global ScreenTransform if VideoPlaying: return boxes = GetPageProp(Pcurrent, 'boxes') if BoxZoom: boxes = [BoxZoom] gl.Clear(gl.COLOR_BUFFER_BIT) # pre-transform for zoom if ZoomArea != 1.0: ScreenTransform = ( -2.0 * ZoomX0 / ZoomArea - 1.0, +2.0 * ZoomY0 / ZoomArea + 1.0, +2.0 / ZoomArea, -2.0 / ZoomArea ) # background layer -- the page's image, darkened if it has boxes # note: some code paths enable GL_BLEND here; it stays enabled # during the rest of this function and will be disabled # at the end of DrawOverlays() is_dark = (boxes or Tracing) and (dark > 0.001) if not(is_dark) or BoxZoom: # standard mode if BoxZoom: i = 1.0 - BoxZoomDarkness * dark else: i = 1.0 TexturedRectShader.get_instance().draw( 0.0, 0.0, 1.0, 1.0, s1=TexMaxS, t1=TexMaxT, tex=Tcurrent, color=(i,i,i,1.0) ) if BoxZoom and is_dark: gl.Enable(gl.BLEND) elif UseBlurShader: # blurred background (using shader) blur_scale = BoxFadeBlur * ZoomArea * dark BlurShader.get_instance().draw( PixelX * blur_scale, PixelY * blur_scale, 1.0 - BoxFadeDarkness * dark, tex=Tcurrent ) gl.Enable(gl.BLEND) else: # blurred background (using oldschool multi-pass blend fallback) intensity = 1.0 - BoxFadeDarkness * dark for dx, dy, alpha in ( (0.0, 0.0, 1.0), (-ZoomArea, 0.0, dark / 2), (+ZoomArea, 0.0, dark / 3), (0.0, -ZoomArea, dark / 4), (0.0, +ZoomArea, dark / 5), ): TexturedRectShader.get_instance().draw( 0.0, 0.0, 1.0, 1.0, TexMaxS * PixelX * dx, TexMaxT * PixelY * dy, TexMaxS * (PixelX * dx + 1.0), TexMaxT * (PixelY * dy + 1.0), tex=Tcurrent, color=(intensity, intensity, intensity, alpha) ) gl.Enable(gl.BLEND) # start blending from the second pass on if boxes and is_dark: TexturedMeshShader.get_instance().setup( 0.0, 0.0, 1.0, 1.0, s1=TexMaxS, t1=TexMaxT # tex is already set ) ex = (ZoomBoxEdgeSize if BoxZoom else BoxEdgeSize) * PixelX ey = (ZoomBoxEdgeSize if BoxZoom else BoxEdgeSize) * PixelY for X0, Y0, X1, Y1 in boxes: vertices = (c_float * 27)( X0, Y0, 1.0, # note: this produces two degenerate triangles X0, Y0, 1.0, X0 - ex, Y0 - ey, 0.0, X1, Y0, 1.0, X1 + ex, Y0 - ey, 0.0, X1, Y1, 1.0, X1 + ex, Y1 + ey, 0.0, X0, Y1, 1.0, X0 - ex, Y1 + ey, 0.0, ) gl.BindBuffer(gl.ARRAY_BUFFER, 0) gl.VertexAttribPointer(0, 3, gl.FLOAT, False, 0, vertices) BoxIndexBuffer.draw() if Tracing and is_dark: x, y = MouseToScreen(Platform.GetMousePos()) TexturedMeshShader.get_instance().setup( x, y, x + 1.0, y + 1.0, x * TexMaxS, y * TexMaxT, (x + 1.0) * TexMaxS, (y + 1.0) * TexMaxT # tex is already set ) gl.BindBuffer(gl.ARRAY_BUFFER, SpotVertices) gl.VertexAttribPointer(0, 3, gl.FLOAT, False, 0, 0) SpotIndices.draw() if Marking: x0 = min(MarkUL[0], MarkLR[0]) y0 = min(MarkUL[1], MarkLR[1]) x1 = max(MarkUL[0], MarkLR[0]) y1 = max(MarkUL[1], MarkLR[1]) # red frame (misusing the progress bar shader as a single-color shader) color = (MarkColor[0], MarkColor[1], MarkColor[2], 1.0) ProgressBarShader.get_instance().draw( x0 - PixelX * ZoomArea, y0 - PixelY * ZoomArea, x1 + PixelX * ZoomArea, y1 + PixelY * ZoomArea, color0=color, color1=color ) # semi-transparent inner area gl.Enable(gl.BLEND) TexturedRectShader.get_instance().draw( x0, y0, x1, y1, x0 * TexMaxS, y0 * TexMaxT, x1 * TexMaxS, y1 * TexMaxT, tex=Tcurrent, color=(1.0, 1.0, 1.0, 1.0 - MarkColor[3]) ) # unapply the zoom transform ScreenTransform = DefaultScreenTransform # Done. DrawOverlays() if do_flip: Platform.SwapBuffers() # draw a black screen with the Impressive logo at the center def DrawLogo(): gl.Clear(gl.COLOR_BUFFER_BIT) if not ShowLogo: return if HalfScreen: x0 = 0.25 else: x0 = 0.5 TexturedRectShader.get_instance().draw( x0 - 128.0 / ScreenWidth, 0.5 - 32.0 / ScreenHeight, x0 + 128.0 / ScreenWidth, 0.5 + 32.0 / ScreenHeight, tex=LogoTexture ) if OSDFont: gl.Enable(gl.BLEND) OSDFont.Draw((int(ScreenWidth * x0), ScreenHeight / 2 + 48), \ __version__.split()[0], align=Center, alpha=0.25, beveled=False) gl.Disable(gl.BLEND) # draw the prerender progress bar def DrawProgress(position): x0 = 0.1 x2 = 1.0 - x0 x1 = position * x2 + (1.0 - position) * x0 y1 = 0.9 y0 = y1 - 16.0 / ScreenHeight if HalfScreen: x0 *= 0.5 x1 *= 0.5 x2 *= 0.5 ProgressBarShader.get_instance().draw( x0, y0, x2, y1, color0=(0.25, 0.25, 0.25, 1.0), color1=(0.50, 0.50, 0.50, 1.0) ) ProgressBarShader.get_instance().draw( x0, y0, x1, y1, color0=(0.25, 0.50, 1.00, 1.0), color1=(0.03, 0.12, 0.50, 1.0) ) # fade mode def DrawFadeMode(intensity, alpha): if VideoPlaying: return DrawCurrentPage(do_flip=False) gl.Enable(gl.BLEND) color = (intensity, intensity, intensity, alpha) ProgressBarShader.get_instance().draw( 0.0, 0.0, 1.0, 1.0, color0=color, color1=color ) gl.Disable(gl.BLEND) Platform.SwapBuffers() def EnterFadeMode(intensity=0.0): t0 = Platform.GetTicks() while True: if Platform.CheckAnimationCancelEvent(): break t = (Platform.GetTicks() - t0) * 1.0 / BlankFadeDuration if t >= 1.0: break DrawFadeMode(intensity, t) DrawFadeMode(intensity, 1.0) def LeaveFadeMode(intensity=0.0): t0 = Platform.GetTicks() while True: if Platform.CheckAnimationCancelEvent(): break t = (Platform.GetTicks() - t0) * 1.0 / BlankFadeDuration if t >= 1.0: break DrawFadeMode(intensity, 1.0 - t) DrawCurrentPage() def FadeMode(intensity): EnterFadeMode(intensity) def fade_action_handler(action): if action == "$quit": PageLeft() Quit() elif action == "$expose": DrawFadeMode(intensity, 1.0) elif action == "*quit": Platform.PostQuitEvent() else: return False return True while True: ev = Platform.GetEvent() if ev and not(ProcessEvent(ev, fade_action_handler)) and ev.startswith('*'): break LeaveFadeMode(intensity) # gamma control def SetGamma(new_gamma=None, new_black=None, force=False): global Gamma, BlackLevel if new_gamma is None: new_gamma = Gamma if new_gamma < 0.1: new_gamma = 0.1 if new_gamma > 10.0: new_gamma = 10.0 if new_black is None: new_black = BlackLevel if new_black < 0: new_black = 0 if new_black > 254: new_black = 254 if not(force) and (abs(Gamma - new_gamma) < 0.01) and (new_black == BlackLevel): return Gamma = new_gamma BlackLevel = new_black return Platform.SetGammaRamp(new_gamma, new_black) # cursor image def PrepareCustomCursor(cimg): global CursorTexture, CursorHotspot, CursorSX, CursorSY, CursorTX, CursorTY if not cimg: CursorHotspot = (1,0) cimg = Image.open(cStringIO.StringIO(DEFAULT_CURSOR.decode('base64'))) w, h = cimg.size tw, th = map(npot, cimg.size) if (tw > 256) or (th > 256): print >>sys.stderr, "Custom cursor is ridiculously large, reverting to normal one." return False img = Image.new('RGBA', (tw, th)) img.paste(cimg, (0, 0)) CursorTexture = gl.make_texture(gl.TEXTURE_2D, gl.CLAMP_TO_EDGE, gl.NEAREST) gl.load_texture(gl.TEXTURE_2D, img) CursorSX = w * PixelX CursorSY = h * PixelY CursorTX = w / float(tw) CursorTY = h / float(th) return True ##### CONTROL AND NAVIGATION ################################################### # update the applications' title bar def UpdateCaption(page=0, force=False): global CurrentCaption, CurrentOSDCaption, CurrentOSDPage, CurrentOSDStatus global CurrentOSDComment if (page == CurrentCaption) and not(force): return CurrentCaption = page caption = __title__ if DocumentTitle: caption += " - " + DocumentTitle if page < 1: CurrentOSDCaption = "" CurrentOSDPage = "" CurrentOSDStatus = "" CurrentOSDComment = "" Platform.SetWindowTitle(caption) return CurrentOSDPage = "%d/%d" % (page, PageCount) caption = "%s (%s)" % (caption, CurrentOSDPage) title = GetPageProp(page, 'title') or GetPageProp(page, '_title') if title: caption += ": %s" % title CurrentOSDCaption = title else: CurrentOSDCaption = "" status = [] if GetPageProp(page, 'skip', False): status.append("skipped: yes") if not GetPageProp(page, ('overview', '_overview'), True): status.append("on overview page: no") CurrentOSDStatus = ", ".join(status) CurrentOSDComment = GetPageProp(page, 'comment') Platform.SetWindowTitle(caption) # get next/previous page def GetNextPage(page, direction): try_page = page while True: try_page += direction if try_page == page: return 0 # tried all pages, but none found if Wrap: if try_page < 1: try_page = PageCount if try_page > PageCount: try_page = 1 else: if try_page < 1 or try_page > PageCount: return 0 # start or end of presentation if not GetPageProp(try_page, 'skip', False): return try_page # pre-load the following page into Pnext/Tnext def PreloadNextPage(page): global Pnext, Tnext if (page < 1) or (page > PageCount): Pnext = 0 return 0 if page == Pnext: return 1 RenderPage(page, Tnext) Pnext = page return 1 # perform box fading; the fade animation time is mapped through func() def BoxFade(func): t0 = Platform.GetTicks() while BoxFadeDuration > 0: if Platform.CheckAnimationCancelEvent(): break t = (Platform.GetTicks() - t0) * 1.0 / BoxFadeDuration if t >= 1.0: break DrawCurrentPage(func(t)) DrawCurrentPage(func(1.0)) return 0 # reset the timer def ResetTimer(): global StartTime, PageEnterTime if TimeTracking and not(FirstPage): print "--- timer was reset here ---" StartTime = Platform.GetTicks() PageEnterTime = 0 # start video playback def PlayVideo(video): global MPlayerProcess, VideoPlaying, NextPageAfterVideo if not video: return StopMPlayer() if Platform.use_omxplayer: opts = ["omxplayer"] else: opts = [MPlayerPath, "-quiet", "-slave", \ "-monitorpixelaspect", "1:1", \ "-vo", "gl", \ "-autosync", "100"] try: opts += ["-wid", str(Platform.GetWindowID())] except KeyError: if Fullscreen: opts.append("-fs") else: print >>sys.stderr, "Sorry, but Impressive only supports video on your operating system if fullscreen" print >>sys.stderr, "mode is used." VideoPlaying = False MPlayerProcess = None return if not isinstance(video, list): video = [video] NextPageAfterVideo = False try: MPlayerProcess = subprocess.Popen(opts + video, stdin=subprocess.PIPE) if Platform.use_omxplayer: gl.Clear(gl.COLOR_BUFFER_BIT) Platform.SwapBuffers() if Fullscreen and (os.name == 'nt'): # very ugly Win32-specific hack: in -wid embedding mode, # video display only works if we briefly minimize and restore # the window ... and that's the good case: in -fs, keyboard # focus is messed up and we don't get any input! if Win32FullscreenVideoHackTiming[0] > 0: time.sleep(Win32FullscreenVideoHackTiming[0]) win32gui.ShowWindow(Platform.GetWindowID(), 6) # SW_MINIMIZE if Win32FullscreenVideoHackTiming[1] > 0: time.sleep(Win32FullscreenVideoHackTiming[1]) win32gui.ShowWindow(Platform.GetWindowID(), 9) # SW_RESTORE VideoPlaying = True except OSError: MPlayerProcess = None # called each time a page is entered, AFTER the transition, BEFORE entering box-fade mode def PreparePage(): global SpotRadius, SpotRadiusBase global BoxFadeDarkness, BoxFadeDarknessBase global BoxZoomDarkness, BoxZoomDarknessBase override = GetPageProp(Pcurrent, 'radius') if override: SpotRadius = override SpotRadiusBase = override GenerateSpotMesh() override = GetPageProp(Pcurrent, 'darkness') if override is not None: BoxFadeDarkness = override * 0.01 BoxFadeDarknessBase = override * 0.01 override = GetPageProp(Pcurrent, 'zoomdarkness') if override is not None: BoxZoomDarkness = override * 0.01 BoxZoomDarknessBase = override * 0.01 # called each time a page is entered, AFTER the transition, AFTER entering box-fade mode def PageEntered(update_time=True): global PageEnterTime, PageTimeout, MPlayerProcess, IsZoomed, WantStatus if update_time: PageEnterTime = Platform.GetTicks() - StartTime IsZoomed = 0 # no, we don't have a pre-zoomed image right now WantStatus = False # don't show status unless it's changed interactively PageTimeout = AutoAdvance shown = GetPageProp(Pcurrent, '_shown', 0) try: os.chdir(os.path.dirname(GetPageProp(Pcurrent, '_file'))) except OSError: pass if not(shown) or Wrap: PageTimeout = GetPageProp(Pcurrent, 'timeout', PageTimeout) if GetPageProp(Pcurrent, '_video'): PlayVideo(GetPageProp(Pcurrent, '_file')) if not(shown) or GetPageProp(Pcurrent, 'always', False): if not GetPageProp(Pcurrent, '_video'): video = GetPageProp(Pcurrent, 'video') sound = GetPageProp(Pcurrent, 'sound') PlayVideo(video) if sound and not(video): StopMPlayer() try: MPlayerProcess = subprocess.Popen( \ [MPlayerPath, "-quiet", "-really-quiet", "-novideo", sound], \ stdin=subprocess.PIPE) except OSError: MPlayerProcess = None SafeCall(GetPageProp(Pcurrent, 'OnEnterOnce')) SafeCall(GetPageProp(Pcurrent, 'OnEnter')) if PageTimeout: Platform.ScheduleEvent("$page-timeout", PageTimeout) SetPageProp(Pcurrent, '_shown', shown + 1) # called each time a page is left def PageLeft(overview=False): global FirstPage, LastPage, WantStatus, PageLeaveTime PageLeaveTime = Platform.GetTicks() - StartTime WantStatus = False if not overview: if GetTristatePageProp(Pcurrent, 'reset'): ResetTimer() FirstPage = False LastPage = Pcurrent if GetPageProp(Pcurrent, '_shown', 0) == 1: SafeCall(GetPageProp(Pcurrent, 'OnLeaveOnce')) SafeCall(GetPageProp(Pcurrent, 'OnLeave')) if TimeTracking: t1 = Platform.GetTicks() - StartTime dt = (t1 - PageEnterTime + 500) / 1000 if overview: p = "over" else: p = "%4d" % Pcurrent print "%s%9s%9s%9s" % (p, FormatTime(dt), \ FormatTime(PageEnterTime / 1000), \ FormatTime(t1 / 1000)) # create an instance of a transition class def InstantiateTransition(trans_class): try: return trans_class() except GLInvalidShaderError: return None except GLShaderCompileError: print >>sys.stderr, "Note: all %s transitions will be disabled" % trans_class.__name__ return None # perform a transition to a specified page def TransitionTo(page, allow_transition=True): global Pcurrent, Pnext, Tcurrent, Tnext global PageCount, Marking, Tracing, Panning global TransitionRunning, TransitionPhase # first, stop video and kill the auto-timer if VideoPlaying: StopMPlayer() Platform.ScheduleEvent("$page-timeout", 0) # invalid page? go away if not PreloadNextPage(page): if QuitAtEnd: LeaveZoomMode(allow_transition) if FadeInOut: EnterFadeMode() PageLeft() Quit() return 0 # leave zoom mode now, if enabled LeaveZoomMode(allow_transition) # notify that the page has been left PageLeft() # box fade-out if GetPageProp(Pcurrent, 'boxes') or Tracing: skip = BoxFade(lambda t: 1.0 - t) else: skip = 0 # some housekeeping Marking = False Tracing = False UpdateCaption(page) # check if the transition is valid tpage = max(Pcurrent, Pnext) trans = None if allow_transition: trans = GetPageProp(tpage, 'transition', GetPageProp(tpage, '_transition')) else: trans = None if trans is not None: transtime = GetPageProp(tpage, 'transtime', TransitionDuration) try: dummy = trans.__class__ except AttributeError: # ah, gotcha! the transition is not yet instantiated! trans = InstantiateTransition(trans) PageProps[tpage][tkey] = trans if trans is None: transtime = 0 # backward motion? then swap page buffers now backward = (Pnext < Pcurrent) if Wrap and (min(Pcurrent, Pnext) == 1) and (max(Pcurrent, Pnext) == PageCount): backward = not(backward) # special case: last<->first in wrap mode if backward: Pcurrent, Pnext = (Pnext, Pcurrent) Tcurrent, Tnext = (Tnext, Tcurrent) # transition animation if not(skip) and transtime: transtime = 1.0 / transtime TransitionRunning = True trans.start() t0 = Platform.GetTicks() while not(VideoPlaying): if Platform.CheckAnimationCancelEvent(): skip = 1 break t = (Platform.GetTicks() - t0) * transtime if t >= 1.0: break TransitionPhase = t if backward: t = 1.0 - t gl.Clear(gl.COLOR_BUFFER_BIT) trans.render(t) DrawOverlays(t) Platform.SwapBuffers() TransitionRunning = False # forward motion => swap page buffers now if not backward: Pcurrent, Pnext = (Pnext, Pcurrent) Tcurrent, Tnext = (Tnext, Tcurrent) # prepare the page's changeable metadata PreparePage() # box fade-in if not(skip) and GetPageProp(Pcurrent, 'boxes'): BoxFade(lambda t: t) # finally update the screen and preload the next page DrawCurrentPage() PageEntered() if not PreloadNextPage(GetNextPage(Pcurrent, 1)): PreloadNextPage(GetNextPage(Pcurrent, -1)) return 1 # zoom mode animation def ZoomAnimation(targetx, targety, func, duration_override=None): global ZoomX0, ZoomY0, ZoomArea t0 = Platform.GetTicks() if duration_override is None: duration = ZoomDuration else: duration = duration_override while duration > 0: if Platform.CheckAnimationCancelEvent(): break t = (Platform.GetTicks() - t0) * 1.0 / duration if t >= 1.0: break t = func(t) dark = (t if BoxZoom else 1.0) t = (2.0 - t) * t ZoomX0 = targetx * t ZoomY0 = targety * t ZoomArea = 1.0 - (1.0 - 1.0 / ViewZoomFactor) * t DrawCurrentPage(dark=dark) t = func(1.0) ZoomX0 = targetx * t ZoomY0 = targety * t ZoomArea = 1.0 - (1.0 - 1.0 / ViewZoomFactor) * t GenerateSpotMesh() DrawCurrentPage(dark=(t if BoxZoom else 1.0)) # re-render zoomed page image def ReRenderZoom(factor): global ResZoomFactor, IsZoomed, HighResZoomFailed ResZoomFactor = min(factor, MaxZoomFactor) if (IsZoomed >= ResZoomFactor) or (ResZoomFactor < 1.1) or HighResZoomFailed: return gl.BindTexture(gl.TEXTURE_2D, Tcurrent) while gl.GetError(): pass # clear all OpenGL errors gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGB, int(ResZoomFactor * TexWidth), int(ResZoomFactor * TexHeight), 0, gl.RGB, gl.UNSIGNED_BYTE, PageImage(Pcurrent, True)) if gl.GetError(): print >>sys.stderr, "I'm sorry, but your graphics card is not capable of rendering presentations" print >>sys.stderr, "in this resolution. Either the texture memory is exhausted, or there is no" print >>sys.stderr, "support for large textures (%dx%d). Please try to run Impressive in a" % (TexWidth, TexHeight) print >>sys.stderr, "smaller resolution using the -g command-line option." HighResZoomFailed = True return DrawCurrentPage() IsZoomed = ResZoomFactor # enter zoom mode def EnterZoomMode(factor, targetx, targety): global ZoomMode, ViewZoomFactor ViewZoomFactor = factor ZoomAnimation(targetx, targety, lambda t: t) ZoomMode = True ReRenderZoom(factor) # leave zoom mode (if enabled) def LeaveZoomMode(allow_transition=True): global ZoomMode, BoxZoom, Panning, ViewZoomFactor, ResZoomFactor global ZoomArea, ZoomX0, ZoomY0 if not ZoomMode: return ZoomAnimation(ZoomX0, ZoomY0, lambda t: 1.0 - t, (None if allow_transition else 0)) ZoomMode = False BoxZoom = False Panning = False ViewZoomFactor = 1 ResZoomFactor = 1 ZoomArea = 1.0 ZoomX0 = 0.0 ZoomY0 = 0.0 # change zoom factor in zoom mode def ChangeZoom(target_factor, mousepos): global ZoomMode, ViewZoomFactor, ZoomArea, ZoomX0, ZoomY0 px, py = MouseToScreen(mousepos) log_zf = log(ViewZoomFactor) dlog = log(target_factor) - log_zf t0 = Platform.GetTicks() dt = -1 while dt < WheelZoomDuration: dt = Platform.GetTicks() - t0 rel = min(1.0, float(dt) / WheelZoomDuration) if WheelZoomDuration else 1.0 factor = exp(log_zf + rel * dlog) if factor < 1.001: factor = 1.0 ZoomArea = 1.0 / factor ZoomX0 = max(0.0, min(1.0 - ZoomArea, px - mousepos[0] * ZoomArea / ScreenWidth)) ZoomY0 = max(0.0, min(1.0 - ZoomArea, py - mousepos[1] * ZoomArea / ScreenHeight)) DrawCurrentPage() ViewZoomFactor = factor ZoomMode = (factor > 1.0) # check whether a box mark is too small def BoxTooSmall(): return ((abs(MarkUL[0] - MarkLR[0]) * ScreenWidth) < MinBoxSize) \ or ((abs(MarkUL[1] - MarkLR[1]) * ScreenHeight) < MinBoxSize) # increment/decrement spot radius def IncrementSpotSize(delta): global SpotRadius if not Tracing: return SpotRadius = max(SpotRadius + delta, 8) GenerateSpotMesh() DrawCurrentPage() # post-initialize the page transitions def PrepareTransitions(): Unspecified = 0xAFFED00F # STEP 1: randomly assign transitions where the user didn't specify them cnt = sum([1 for page in xrange(1, PageCount + 1) \ if GetPageProp(page, 'transition', Unspecified) == Unspecified]) newtrans = ((cnt / len(AvailableTransitions) + 1) * AvailableTransitions)[:cnt] random.shuffle(newtrans) for page in xrange(1, PageCount + 1): if GetPageProp(page, 'transition', Unspecified) == Unspecified: SetPageProp(page, '_transition', newtrans.pop()) # STEP 2: instantiate transitions for page in PageProps: for key in ('transition', '_transition'): if not key in PageProps[page]: continue trans = PageProps[page][key] if trans is not None: PageProps[page][key] = InstantiateTransition(trans) # update timer values and screen timer def TimerTick(): global CurrentTime, ProgressBarPos redraw = False newtime = (Platform.GetTicks() - StartTime) * 0.001 if EstimatedDuration: newpos = int(ScreenWidth * newtime / EstimatedDuration) if newpos != ProgressBarPos: redraw = True ProgressBarPos = newpos newtime = int(newtime) if TimeDisplay and (CurrentTime != newtime): redraw = True if PageTimeout and AutoAdvanceProgress: redraw = True CurrentTime = newtime return redraw # enables time tracking mode (if not already done so) def EnableTimeTracking(force=False): global TimeTracking if force or (TimeDisplay and not(TimeTracking) and not(ShowClock) and FirstPage): print >>sys.stderr, "Time tracking mode enabled." TimeTracking = True print "page duration enter leave" print "---- -------- -------- --------" # set cursor visibility def SetCursor(visible): global CursorVisible CursorVisible = visible if EnableCursor and not(CursorImage) and (MouseHideDelay != 1): Platform.SetMouseVisible(visible) # handle a shortcut key event: store it (if shifted) or return the # page number to navigate to (if not) def HandleShortcutKey(key, current=0): if not(key) or (key[0] != '*'): return None shift = key.startswith('*shift+') if shift: key = key[7:] else: key = key[1:] if (len(key) == 1) or ((key >= "f1") and (key <= "f9")): # Note: F10..F12 are implicitly included due to lexicographic sorting page = None for check_page, props in PageProps.iteritems(): if props.get('shortcut') == key: page = check_page break if shift: if page: DelPageProp(page, 'shortcut') SetPageProp(current, 'shortcut', key) elif page and (page != current): return page return None ##### EVENT-TO-ACTION BINDING CODE ############################################# SpecialKeyNames = set(filter(None, """ ampersand asterisk at backquote backslash backspace break capslock caret clear comma down escape euro end exclaim greater hash help home insert kp_divide kp_enter kp_equals kp_minus kp_multiply kp_plus lalt last lctrl left leftbracket leftparen less lmeta lshift lsuper menu minus mode numlock pagedown pageup pause period plus power print question quote quotedbl ralt rctrl return right rightbracket rightparen rmeta rshift rsuper scrollock semicolon slash space sysreq tab underscore up """.split())) KnownEvents = set(list(SpecialKeyNames) + filter(None, """ a 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 kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 lmb mmb rmb wheeldown wheelup """.split()) + ["btn%d" % i for i in xrange(1, 20)]) # event handling model: # - Platform.GetEvent() generates platform-neutral event (= string) that # identifies a key or mouse button, with prefix: # - '+' = key pressed, '-' = key released, '*' = main event ('*' is generated # directly before '-' for keys and directly after '+' for mouse buttons) # - "ctrl+", "alt+", "shift+" modifiers, in that order # - event gets translated into a list of actions via the EventMap dictionary # - actions are processed in order of that list, like priorities: # - list processing terminates at the first action that is successfully handled # - exception: "forced actions" will always be executed, even if a higher-prio # action of that list has already been executed; also, they will not stop # action list execution, even if they have been handled KnownActions = {} EventMap = {} ForcedActions = set() ActivateReleaseActions = set() class ActionNotHandled(Exception): pass def ActionValidIf(cond): if not cond: raise ActionNotHandled() class ActionRelayBase(object): def __init__(self): global KnownActions, ActivateReleaseActions for item in dir(self): if (item[0] == '_') and (item[1] != '_') and (item[1] != 'X') and (item[-1] != '_'): doc = getattr(self, item).__doc__ if item.endswith("_ACTIVATE"): item = item[:-9] ActivateReleaseActions.add(item) elif item.endswith("_RELEASE"): item = item[:-8] ActivateReleaseActions.add(item) item = item[1:].replace('_', '-') olddoc = KnownActions.get(item) if not olddoc: KnownActions[item] = doc def __call__(self, ev): evname = ev[1:].replace('-', '_') if ev[0] == '$': meth = getattr(self, '_X_' + evname, None) elif ev[0] == '*': meth = getattr(self, '_' + evname, None) elif ev[0] == '+': meth = getattr(self, '_' + evname + '_ACTIVATE', None) elif ev[0] == '-': meth = getattr(self, '_' + evname + '_RELEASE', None) if not meth: return False try: meth() return True except ActionNotHandled: return False def ProcessEvent(ev, handler_func): """ calls the appropriate action handlers for an event as returned by Platform.GetEvent() """ if not ev: return False if ev[0] == '$': handler_func(ev) try: events = EventMap[ev[1:]] except KeyError: return False prefix = ev[0] handled = False no_forced = not(any(((prefix + ev) in ForcedActions) for ev in events)) if no_forced and (prefix in "+-"): if not(any((ev in ActivateReleaseActions) for ev in events)): return False for ev in events: ev = prefix + ev if ev in ForcedActions: handler_func(ev) elif not handled: handled = handler_func(ev) if handled and no_forced: break return handled def ValidateEvent(ev, error_prefix=None): for prefix in ("ctrl+", "alt+", "shift+"): if ev.startswith(prefix): ev = ev[len(prefix):] if (ev in KnownEvents) or ev.startswith('unknown-'): return True if error_prefix: error_prefix += ": " else: error_prefix = "" print >>sys.stderr, "ERROR: %signoring unknown event '%s'" % (error_prefix, ev) return False def ValidateAction(ev, error_prefix=None): if not(KnownActions) or (ev in KnownActions): return True if error_prefix: error_prefix += ": " else: error_prefix = "" print >>sys.stderr, "ERROR: %signoring unknown action '%s'" % (error_prefix, ev) return False def BindEvent(events, actions=None, clear=False, remove=False, error_prefix=None): """ bind one or more events to one or more actions - events and actions can be lists or single comma-separated strings - if clear is False, actions will be *added* to the raw events, if clear is True, the specified actions will *replace* the current set, if remove is True, the specified actions will be *removed* from the set - actions can be omitted; instead, events can be a string consisting of raw event and internal event names, separated by one of: '=' -> add or replace, based on the clear flag '+=' -> always add ':=' -> always clear '-=' -> always remove - some special events are recognized: 'clearall' clears *all* actions of *all* raw events; 'defaults' loads all defaults 'include', followed by whitespace and a filename, will include a file (that's what the basedirs option is for) """ global EventMap if isinstance(events, basestring): if not actions: if (';' in events) or ('\n' in events): for cmd in events.replace('\n', ';').split(';'): BindEvent(cmd, clear=clear, remove=remove, error_prefix=error_prefix) return if '=' in events: events, actions = events.split('=', 1) events = events.rstrip() if events.endswith('+'): clear = False events = events[:-1] elif events.endswith(':'): clear = True events = events[:-1] elif events.endswith('-'): remove = True events = events[:-1] events = events.split(',') if actions is None: actions = [] elif isinstance(actions, basestring): actions = actions.split(',') actions = [b.replace('_', '-').strip(' \t$+-').lower() for b in actions] actions = [a for a in actions if ValidateAction(a, error_prefix)] for event in events: event_orig = event.replace('\t', ' ').strip(' \r\n+-$') if not event_orig: continue event = event_orig.replace('-', '_').lower() if event.startswith('include '): filename = event_orig[8:].strip() if (filename.startswith('"') and filename.endswith('"')) \ or (filename.startswith("'") and filename.endswith("'")): filename = filename[1:-1] ParseInputBindingFile(filename) continue elif event == 'clearall': EventMap = {} continue elif event == 'defaults': LoadDefaultBindings() continue event = event.replace(' ', '') if not ValidateEvent(event, error_prefix): continue if remove: if event in EventMap: for a in actions: try: EventMap[event].remove(a) except ValueError: pass elif clear or not(event in EventMap): EventMap[event] = actions[:] else: EventMap[event].extend(actions) def ParseInputBindingFile(filename): """ parse an input configuration file; basically calls BindEvent() for each line; '#' is the comment character """ try: f = open(filename, "r") n = 0 for line in f: n += 1 line = line.split('#', 1)[0].strip() if line: BindEvent(line, error_prefix="%s:%d" % (filename, n)) f.close() except IOError, e: print >>sys.stderr, "ERROR: failed to read the input configuration file '%s' -" % filename, e def EventHelp(): evlist = ["a-z", "0-9", "kp0-kp9", "f1-f12"] + sorted(list(SpecialKeyNames)) print "Event-to-action binding syntax:" print " [,] = [,]" print " By default, this will *add* actions to an event." print " To *overwrite* the current binding for an event, use ':=' instead of '='." print " To remove actions from an event, use '-=' instead of '='." print " Join multiple bindings with a semi-colon (';')." print "Special commands:" print " clearall = clear all bindings" print " defaults = load default bindings" print " include = load bindings from a file" print "Binding files use the same syntax with one binding per line;" print "comments start with a '#' symbol." print print "Recognized keyboard event names:" while evlist: line = " " while evlist and ((len(line) + len(evlist[0])) < 78): line += evlist.pop(0) + ", " line = line.rstrip() if not evlist: line = line.rstrip(',') print line print "Recognized mouse event names:" print " lmb, mmb, rmb (= left, middle and right mouse buttons)," print " wheelup, wheeldown," print " btnX (additional buttons, use --evtest to check their mapping)" print print "Recognized actions:" maxalen = max(map(len, KnownActions)) for action in sorted(KnownActions): doc = KnownActions[action] if doc: print " %s - %s" % (action.ljust(maxalen), doc) else: print " %s" % action print if not EventMap: return print "Current bindings:" maxelen = max(map(len, EventMap)) for event in sorted(EventMap): if EventMap[event]: print " %s = %s" % (event.ljust(maxelen), ", ".join(EventMap[event])) def LoadDefaultBindings(): BindEvent("""clearall escape, return, kp_enter, lmb, rmb = video-stop space = video-pause period = video-step down = video-seek-backward-10 left = video-seek-backward-1 right = video-seek-forward-1 up = video-seek-forward-10 escape = overview-exit, zoom-exit, spotlight-exit, box-clear, quit q = quit f = fullscreen tab = overview-enter, overview-exit s = save t = time-toggle r = time-reset c = box-clear y, z = zoom-enter, zoom-exit o = toggle-overview i = toggle-skip u = zoom-update b, period = fade-to-black w, comma = fade-to-white return, kp_enter = overview-confirm, spotlight-enter, spotlight-exit plus, kp_plus, 0, wheelup = spotlight-grow minus, kp_minus, 9, wheeldown = spotlight-shrink ctrl+9, ctrl+0 = spotlight-reset 7 = fade-less 8 = fade-more ctrl+7, ctrl+8 = fade-reset leftbracket = gamma-decrease rightbracket = gamma-increase shift+leftbracket = gamma-bl-decrease shift+rightbracket = gamma-bl-increase backslash = gamma-reset lmb = box-add, hyperlink, overview-confirm ctrl+lmb = box-zoom, hyperlink-notrans rmb = box-zoom-exit, zoom-pan, box-remove, overview-exit mmb = zoom-pan, zoom-exit, overview-enter, overview-exit left, wheelup = overview-prev right, wheeldown = overview-next up = overview-up down = overview-down wheelup = zoom-in wheeldown = zoom-out lmb, wheeldown, pagedown, down, right, space = goto-next ctrl+lmb, ctrl+wheeldown, ctrl+pagedown, ctrl+down, ctrl+right, ctrl+space = goto-next-notrans rmb, wheelup, pageup, up, left, backspace = goto-prev ctrl+rmb, ctrl+wheelup, ctrl+pageup, ctrl+up, ctrl+left, ctrl+backspace = goto-prev-notrans home = goto-start ctrl+home = goto-start-notrans end = goto-end ctrl+end = goto-end-notrans l = goto-last ctrl+l = goto-last-notrans """, error_prefix="LoadDefaultBindings") # basic action implementations (i.e. stuff that is required to work in all modes) class BaseActions(ActionRelayBase): def _X_quit(self): Quit() def _X_alt_tab(self): ActionValidIf(Fullscreen) SetFullscreen(False) Platform.Minimize() def _quit(self): "quit Impressive immediately" Platform.PostQuitEvent() def _X_move(self): # mouse move in fullscreen mode -> show mouse cursor and reset mouse timer if Fullscreen: Platform.ScheduleEvent("$hide-mouse", MouseHideDelay) SetCursor(True) def _X_call(self): while CallQueue: func, args, kwargs = CallQueue.pop(0) func(*args, **kwargs) ##### OVERVIEW MODE ############################################################ def UpdateOverviewTexture(): global OverviewNeedUpdate Loverview.acquire() try: gl.load_texture(gl.TEXTURE_2D, Tnext, OverviewImage) finally: Loverview.release() OverviewNeedUpdate = False # draw the overview page def DrawOverview(): if VideoPlaying: return gl.Clear(gl.COLOR_BUFFER_BIT) TexturedRectShader.get_instance().draw( 0.0, 0.0, 1.0, 1.0, s1=TexMaxS, t1=TexMaxT, tex=Tnext, color=0.75 ) pos = OverviewPos(OverviewSelection) X0 = PixelX * pos[0] Y0 = PixelY * pos[1] X1 = PixelX * (pos[0] + OverviewCellX) Y1 = PixelY * (pos[1] + OverviewCellY) TexturedRectShader.get_instance().draw( X0, Y0, X1, Y1, X0 * TexMaxS, Y0 * TexMaxT, X1 * TexMaxS, Y1 * TexMaxT, color=1.0 ) gl.Enable(gl.BLEND) if OSDFont: OSDFont.BeginDraw() DrawOSDEx(OSDTitlePos, CurrentOSDCaption) DrawOSDEx(OSDPagePos, CurrentOSDPage) DrawOSDEx(OSDStatusPos, CurrentOSDStatus) OSDFont.EndDraw() DrawOverlays() Platform.SwapBuffers() # overview zoom effect, time mapped through func def OverviewZoom(func): global TransitionRunning if ZoomDuration <= 0: return pos = OverviewPos(OverviewSelection) X0 = PixelX * (pos[0] + OverviewBorder) Y0 = PixelY * (pos[1] + OverviewBorder) X1 = PixelX * (pos[0] - OverviewBorder + OverviewCellX) Y1 = PixelY * (pos[1] - OverviewBorder + OverviewCellY) shader = TexturedRectShader.get_instance() TransitionRunning = True t0 = Platform.GetTicks() while not(VideoPlaying): t = (Platform.GetTicks() - t0) * 1.0 / ZoomDuration if t >= 1.0: break t = func(t) t1 = t*t t = 1.0 - t1 zoom = (t * (X1 - X0) + t1) / (X1 - X0) OX = zoom * (t * X0 - X0) - (zoom - 1.0) * t * X0 OY = zoom * (t * Y0 - Y0) - (zoom - 1.0) * t * Y0 OX = t * X0 - zoom * X0 OY = t * Y0 - zoom * Y0 gl.Clear(gl.COLOR_BUFFER_BIT) shader.draw( # base overview page OX, OY, OX + zoom, OY + zoom, s1=TexMaxS, t1=TexMaxT, tex=Tnext, color=0.75 ) shader.draw( # highlighted part OX + X0 * zoom, OY + Y0 * zoom, OX + X1 * zoom, OY + Y1 * zoom, X0 * TexMaxS, Y0 * TexMaxT, X1 * TexMaxS, Y1 * TexMaxT, color=1.0 ) gl.Enable(gl.BLEND) shader.draw( # overlay of the original high-res page t * X0, t * Y0, t * X1 + t1, t * Y1 + t1, s1=TexMaxS, t1=TexMaxT, tex=Tcurrent, color=(1.0, 1.0, 1.0, 1.0 - t * t * t) ) if OSDFont: OSDFont.BeginDraw() DrawOSDEx(OSDTitlePos, CurrentOSDCaption, alpha_factor=t) DrawOSDEx(OSDPagePos, CurrentOSDPage, alpha_factor=t) DrawOSDEx(OSDStatusPos, CurrentOSDStatus, alpha_factor=t) OSDFont.EndDraw() DrawOverlays() Platform.SwapBuffers() TransitionRunning = False # overview keyboard navigation def OverviewKeyboardNav(delta): global OverviewSelection dest = OverviewSelection + delta if (dest >= OverviewPageCount) or (dest < 0): return OverviewSelection = dest x, y = OverviewPos(OverviewSelection) Platform.SetMousePos((x + (OverviewCellX / 2), y + (OverviewCellY / 2))) # overview mode PageProp toggle def OverviewTogglePageProp(prop, default): if (OverviewSelection < 0) or (OverviewSelection >= len(OverviewPageMap)): return page = OverviewPageMap[OverviewSelection] SetPageProp(page, prop, not(GetPageProp(page, prop, default))) UpdateCaption(page, force=True) DrawOverview() class ExitOverview(Exception): pass # action implementation for overview mode class OverviewActions(BaseActions): def _X_move(self): global OverviewSelection BaseActions._X_move(self) # determine highlighted page x, y = Platform.GetMousePos() OverviewSelection = \ int((x - OverviewOfsX) / OverviewCellX) + \ int((y - OverviewOfsY) / OverviewCellY) * OverviewGridSize if (OverviewSelection < 0) or (OverviewSelection >= len(OverviewPageMap)): UpdateCaption(0) else: UpdateCaption(OverviewPageMap[OverviewSelection]) DrawOverview() def _X_quit(self): PageLeft(overview=True) Quit() def _X_expose(self): DrawOverview() def _X_hide_mouse(self): # mouse timer event -> hide fullscreen cursor SetCursor(False) DrawOverview() def _X_timer_update(self): force_update = OverviewNeedUpdate if OverviewNeedUpdate: UpdateOverviewTexture() if TimerTick() or force_update: DrawOverview() def _overview_exit(self): "exit overview mode and return to the last page" global OverviewSelection OverviewSelection = -1 raise ExitOverview def _overview_confirm(self): "exit overview mode and go to the selected page" raise ExitOverview def _fullscreen(self): SetFullscreen(not(Fullscreen)) def _save(self): SaveInfoScript(InfoScriptPath) def _fade_to_black(self): FadeMode(0.0) def _fade_to_white(self): FadeMode(1.0) def _time_toggle(self): global TimeDisplay TimeDisplay = not(TimeDisplay) DrawOverview() def _time_reset(self): ResetTimer() if TimeDisplay: DrawOverview() def _toggle_skip(self): TogglePageProp('skip', False) def _toggle_overview(self): TogglePageProp('overview', GetPageProp(Pcurrent, '_overview', True)) def _overview_up(self): "move the overview selection upwards" OverviewKeyboardNav(-OverviewGridSize) def _overview_prev(self): "select the previous page in overview mode" OverviewKeyboardNav(-1) def _overview_next(self): "select the next page in overview mode" OverviewKeyboardNav(+1) def _overview_down(self): "move the overview selection downwards" OverviewKeyboardNav(+OverviewGridSize) OverviewActions = OverviewActions() # overview mode entry/loop/exit function def DoOverview(): global Pcurrent, Pnext, Tcurrent, Tnext, Tracing, OverviewSelection global PageEnterTime, OverviewMode Platform.ScheduleEvent("$page-timeout", 0) PageLeft() UpdateOverviewTexture() if GetPageProp(Pcurrent, 'boxes') or Tracing: BoxFade(lambda t: 1.0 - t) Tracing = False OverviewSelection = OverviewPageMapInv[Pcurrent] OverviewMode = True OverviewZoom(lambda t: 1.0 - t) DrawOverview() PageEnterTime = Platform.GetTicks() - StartTime try: while True: ev = Platform.GetEvent() if not ev: continue if not ProcessEvent(ev, OverviewActions): try: page = OverviewPageMap[OverviewSelection] except IndexError: page = 0 page = HandleShortcutKey(ev, page) if page: OverviewSelection = OverviewPageMapInv[page] x, y = OverviewPos(OverviewSelection) Platform.SetMousePos((x + (OverviewCellX / 2), \ y + (OverviewCellY / 2))) DrawOverview() except ExitOverview: PageLeft(overview=True) if (OverviewSelection < 0) or (OverviewSelection >= OverviewPageCount): OverviewSelection = OverviewPageMapInv[Pcurrent] Pnext = Pcurrent else: Pnext = OverviewPageMap[OverviewSelection] if Pnext != Pcurrent: Pcurrent = Pnext RenderPage(Pcurrent, Tcurrent) UpdateCaption(Pcurrent) OverviewZoom(lambda t: t) OverviewMode = False DrawCurrentPage() if GetPageProp(Pcurrent, 'boxes'): BoxFade(lambda t: t) PageEntered() if not PreloadNextPage(GetNextPage(Pcurrent, 1)): PreloadNextPage(GetNextPage(Pcurrent, -1)) ##### EVENT HANDLING ########################################################### # set fullscreen mode def SetFullscreen(fs, do_init=True): global Fullscreen if FakeFullscreen: return # this doesn't work in fake-fullscreen mode if do_init: if fs == Fullscreen: return if not Platform.ToggleFullscreen(): return Fullscreen = fs DrawCurrentPage() if fs: Platform.ScheduleEvent("$hide-mouse", MouseHideDelay) else: Platform.ScheduleEvent("$hide-mouse", 0) SetCursor(True) # PageProp toggle def TogglePageProp(prop, default): global WantStatus SetPageProp(Pcurrent, prop, not(GetPageProp(Pcurrent, prop, default))) UpdateCaption(Pcurrent, force=True) WantStatus = True DrawCurrentPage() # basic action implementations (i.e. stuff that is required to work, except in overview mode) class BaseDisplayActions(BaseActions): def _X_quit(self): if FadeInOut: EnterFadeMode() PageLeft() Quit() def _X_expose(self): DrawCurrentPage() def _X_hide_mouse(self): # mouse timer event -> hide fullscreen cursor SetCursor(False) DrawCurrentPage() def _X_page_timeout(self): global NextPageAfterVideo if VideoPlaying: NextPageAfterVideo = True else: TransitionTo(GetNextPage(Pcurrent, 1)) def _X_poll_file(self): global RTrunning, RTrestart, Pnext dirty = False for f in FileProps: s = my_stat(f) if s != GetFileProp(f, 'stat'): dirty = True SetFileProp(f, 'stat', s) if dirty: # first, check if the new file is valid if not os.path.isfile(GetPageProp(Pcurrent, '_file')): return # invalidate everything we used to know about the input files InvalidateCache() for props in PageProps.itervalues(): for prop in ('_overview_rendered', '_box', '_href'): if prop in props: del props[prop] LoadInfoScript() # force a transition to the current page, reloading it Pnext = -1 TransitionTo(Pcurrent) # restart the background renderer thread. this is not completely safe, # i.e. there's a small chance that we fail to restart the thread, but # this isn't critical if CacheMode and BackgroundRendering: if RTrunning: RTrestart = True else: RTrunning = True thread.start_new_thread(RenderThread, (Pcurrent, Pnext)) def _X_timer_update(self): if VideoPlaying and MPlayerProcess: if MPlayerProcess.poll() is not None: StopMPlayer() DrawCurrentPage() elif TimerTick(): DrawCurrentPage() # action implementations for video playback class VideoActions(BaseDisplayActions): def _video_stop(self): "stop video playback" StopMPlayer() DrawCurrentPage() def player_command(self, mplayer_cmd, omxplayer_cmd): "helper for the various video-* actions" cmd = omxplayer_cmd if Platform.use_omxplayer else (mplayer_cmd + '\n') if not cmd: return try: MPlayerProcess.stdin.write(cmd) MPlayerProcess.stdin.flush() except: StopMPlayer() DrawCurrentPage() def _video_pause(self): "pause video playback" self.player_command("pause", 'p') def _video_step(self): "advance to the next frame in paused video" self.player_command("framestep", None) def _video_seek_backward_10(self): "seek 10 seconds backward in video" self.player_command("seek -10 pausing_keep", '\x1b[D') def _video_seek_backward_1(self): "seek 1 second backward in video" self.player_command("seek -1 pausing_keep", None) def _video_seek_forward_1(self): "seek 1 second forward in video" self.player_command("seek 1 pausing_keep", None) def _video_seek_forward_10(self): "seek 10 seconds forward in video" self.player_command("seek 10 pausing_keep", '\x1b[C') VideoActions = VideoActions() # action implementation for normal page display (i.e. everything except overview mode) class PageDisplayActions(BaseDisplayActions): def _X_move(self): global Marking, MarkLR, Panning, ZoomX0, ZoomY0 BaseActions._X_move(self) x, y = Platform.GetMousePos() # activate marking if mouse is moved away far enough if MarkValid and not(Marking): if (abs(x - MarkBaseX) > 4) and (abs(y - MarkBaseY) > 4): Marking = True # mouse move while marking -> update marking box if Marking: MarkLR = MouseToScreen((x, y)) # mouse move while RMB is pressed -> panning if PanValid and ZoomMode: if not(Panning) and (abs(x - PanBaseX) > 1) and (abs(y - PanBaseY) > 1): Panning = True ZoomX0 = PanAnchorX + (PanBaseX - x) * ZoomArea / ScreenWidth ZoomY0 = PanAnchorY + (PanBaseY - y) * ZoomArea / ScreenHeight ZoomX0 = min(max(ZoomX0, 0.0), 1.0 - ZoomArea) ZoomY0 = min(max(ZoomY0, 0.0), 1.0 - ZoomArea) # if anything changed, redraw the page if Marking or Tracing or Panning or (CursorImage and CursorVisible): DrawCurrentPage() def _zoom_pan_ACTIVATE(self): "pan visible region in zoom mode" global PanValid, Panning, PanBaseX, PanBaseY, PanAnchorX, PanAnchorY ActionValidIf(ZoomMode and not(BoxZoom)) PanValid = True Panning = False PanBaseX, PanBaseY = Platform.GetMousePos() PanAnchorX = ZoomX0 PanAnchorY = ZoomY0 def _zoom_pan(self): ActionValidIf(ZoomMode and Panning) def _zoom_pan_RELEASE(self): global PanValid, Panning PanValid = False Panning = False def _zoom_enter(self): "enter zoom mode" ActionValidIf(not(ZoomMode)) tx, ty = MouseToScreen(Platform.GetMousePos()) EnterZoomMode(DefaultZoomFactor, (1.0 - 1.0 / DefaultZoomFactor) * tx, \ (1.0 - 1.0 / DefaultZoomFactor) * ty) def _zoom_exit(self): "leave zoom mode" ActionValidIf(ZoomMode) LeaveZoomMode() def _box_add_ACTIVATE(self): "draw a new highlight box [mouse-only]" global MarkValid, Marking, MarkBaseX, MarkBaseY, MarkUL, MarkLR MarkValid = True Marking = False MarkBaseX, MarkBaseY = Platform.GetMousePos() MarkUL = MarkLR = MouseToScreen((MarkBaseX, MarkBaseY)) def _box_add(self): global Marking ActionValidIf(Marking) Marking = False if BoxTooSmall(): raise ActionNotHandled() boxes = GetPageProp(Pcurrent, 'boxes', []) oldboxcount = len(boxes) boxes.append(NormalizeRect(MarkUL[0], MarkUL[1], MarkLR[0], MarkLR[1])) SetPageProp(Pcurrent, 'boxes', boxes) if not(oldboxcount) and not(Tracing): BoxFade(lambda t: t) DrawCurrentPage() def _box_add_RELEASE(self): global MarkValid MarkValid = False def _box_remove(self): "remove the highlight box under the mouse cursor" ActionValidIf(not(Panning) and not(Marking)) boxes = GetPageProp(Pcurrent, 'boxes', []) x, y = MouseToScreen(Platform.GetMousePos()) try: # if a box is already present around the clicked position, kill it idx = FindBox(x, y, boxes) if (len(boxes) == 1) and not(Tracing): BoxFade(lambda t: 1.0 - t) del boxes[idx] SetPageProp(Pcurrent, 'boxes', boxes) DrawCurrentPage() except ValueError: # no box present raise ActionNotHandled() def _box_clear(self): "remove all highlight boxes on the current page" ActionValidIf(GetPageProp(Pcurrent, 'boxes')) if not Tracing: BoxFade(lambda t: 1.0 - t) DelPageProp(Pcurrent, 'boxes') DrawCurrentPage() def _box_zoom_ACTIVATE(self): "draw a box to zoom into [mouse-only]" ActionValidIf(not(BoxZoom) and not(Tracing) and not(GetPageProp(Pcurrent, 'boxes'))) return self._box_add_ACTIVATE() def _box_zoom(self): global Marking, BoxZoom, ZoomBox ActionValidIf(Marking and not(BoxZoom) and not(Tracing) and not(GetPageProp(Pcurrent, 'boxes'))) Marking = False if BoxTooSmall(): raise ActionNotHandled() z = min(1.0 / abs(MarkUL[0] - MarkLR[0]), 1.0 / abs(MarkUL[1] - MarkLR[1])) if z <= 1: return DrawCurrentPage() tx = (MarkUL[0] + MarkLR[0]) * 0.5 ty = (MarkUL[1] + MarkLR[1]) * 0.5 tx = tx + (tx - 0.5) / (z - 1.0) ty = ty + (ty - 0.5) / (z - 1.0) tx = (1.0 - 1.0 / z) * tx ty = (1.0 - 1.0 / z) * ty BoxZoom = NormalizeRect(MarkUL[0], MarkUL[1], MarkLR[0], MarkLR[1]) EnterZoomMode(z, tx, ty) def _box_zoom_RELEASE(self): return self._box_add_RELEASE() def _box_zoom_exit(self): "leave box-zoom mode" ActionValidIf(BoxZoom) LeaveZoomMode() def _hyperlink(self, allow_transition=True): "navigate to the hyperlink under the mouse cursor" x, y = Platform.GetMousePos() for valid, target, x0, y0, x1, y1 in GetPageProp(Pcurrent, '_href', []): if valid and (x >= x0) and (x < x1) and (y >= y0) and (y < y1): if type(target) == types.IntType: TransitionTo(target, allow_transition=allow_transition) elif target: RunURL(target) return raise ActionNotHandled() def _hyperlink_notrans(self): "like 'hyperlink', but no transition on page change" return self._hyperlink(allow_transition=False) def _goto_prev(self): "go to the previous page (with transition)" TransitionTo(GetNextPage(Pcurrent, -1), allow_transition=True) def _goto_prev_notrans(self): "go to the previous page (without transition)" TransitionTo(GetNextPage(Pcurrent, -1), allow_transition=False) def _goto_next(self): "go to the next page (with transition)" TransitionTo(GetNextPage(Pcurrent, +1), allow_transition=True) def _goto_next_notrans(self): "go to the next page (without transition)" TransitionTo(GetNextPage(Pcurrent, +1), allow_transition=False) def _goto_last(self): "go to the last visited page (with transition)" TransitionTo(LastPage, allow_transition=True) def _goto_last_notrans(self): "go to the last visited page (without transition)" TransitionTo(LastPage, allow_transition=False) def _goto_start(self): "go to the first page (with transition)" ActionValidIf(Pcurrent != 1) TransitionTo(1, allow_transition=True) def _goto_start_notrans(self): "go to the first page (without transition)" ActionValidIf(Pcurrent != 1) TransitionTo(1, allow_transition=False) def _goto_end(self): "go to the final page (with transition)" ActionValidIf(Pcurrent != PageCount) TransitionTo(PageCount, allow_transition=True) def _goto_end_notrans(self): "go to the final page (without transition)" ActionValidIf(Pcurrent != PageCount) TransitionTo(PageCount, allow_transition=False) def _overview_enter(self): "zoom out to the overview page" if not EnableOverview: return LeaveZoomMode() DoOverview() def _spotlight_enter(self): "enter spotlight mode" global Tracing ActionValidIf(not(Tracing)) Tracing = True if GetPageProp(Pcurrent, 'boxes'): DrawCurrentPage() else: BoxFade(lambda t: t) def _spotlight_exit(self): "exit spotlight mode" global Tracing ActionValidIf(Tracing) if not GetPageProp(Pcurrent, 'boxes'): BoxFade(lambda t: 1.0 - t) Tracing = False DrawCurrentPage() def _spotlight_shrink(self): "decrease the spotlight radius" ActionValidIf(Tracing) IncrementSpotSize(-8) def _spotlight_grow(self): "increase the spotlight radius" ActionValidIf(Tracing) IncrementSpotSize(+8) def _spotlight_reset(self): "reset the spotlight radius to its default value" global SpotRadius ActionValidIf(Tracing) SpotRadius = SpotRadiusBase GenerateSpotMesh() DrawCurrentPage() def _zoom_in(self): "zoom in a small bit" ActionValidIf((MouseWheelZoom or ZoomMode) and not(BoxZoom)) ChangeZoom(ViewZoomFactor * ZoomStep, Platform.GetMousePos()) def _zoom_out(self): "zoom out a small bit" ActionValidIf((MouseWheelZoom or ZoomMode) and not(BoxZoom)) ChangeZoom(ViewZoomFactor / ZoomStep, Platform.GetMousePos()) def _zoom_update(self): "re-render the page in the current zoom resolution" ActionValidIf(ZoomMode) ReRenderZoom(ViewZoomFactor) def _fullscreen(self): "toggle fullscreen mode" SetFullscreen(not(Fullscreen)) def _save(self): "save the info script" SaveInfoScript(InfoScriptPath) def _fade_to_black(self): "fade to a black screen" FadeMode(0.0) def _fade_to_white(self): "fade to a white screen" FadeMode(1.0) def _time_toggle(self): "toggle time display and/or time tracking mode" global TimeDisplay TimeDisplay = not(TimeDisplay) DrawCurrentPage() EnableTimeTracking() def _time_reset(self): "reset the on-screen timer" ResetTimer() if TimeDisplay: DrawCurrentPage() def _toggle_skip(self): "toggle 'skip' flag of current page" TogglePageProp('skip', False) def _toggle_overview(self): "toggle 'visible on overview' flag of current page" TogglePageProp('overview', GetPageProp(Pcurrent, '_overview', True)) def _fade_less(self): "decrease the spotlight/box background darkness" global BoxFadeDarkness, BoxZoomDarkness if BoxZoom: BoxZoomDarkness = max(0.0, BoxZoomDarkness - BoxFadeDarknessStep) else: BoxFadeDarkness = max(0.0, BoxFadeDarkness - BoxFadeDarknessStep) DrawCurrentPage() def _fade_more(self): "increase the spotlight/box background darkness" global BoxFadeDarkness, BoxZoomDarkness if BoxZoom: BoxZoomDarkness = min(1.0, BoxZoomDarkness + BoxFadeDarknessStep) else: BoxFadeDarkness = min(1.0, BoxFadeDarkness + BoxFadeDarknessStep) DrawCurrentPage() def _fade_reset(self): "reset spotlight/box background darkness to default" global BoxFadeDarkness, BoxZoomDarkness BoxFadeDarkness = BoxFadeDarknessBase BoxZoomDarkness = BoxZoomDarknessBase DrawCurrentPage() def _gamma_decrease(self): "decrease gamma" SetGamma(new_gamma=Gamma / GammaStep) def _gamma_increase(self): "increase gamma" SetGamma(new_gamma=Gamma * GammaStep) def _gamma_bl_decrease(self): "decrease black level" SetGamma(new_black=BlackLevel - BlackLevelStep) def _gamma_bl_increase(self): "increase black level" SetGamma(new_black=BlackLevel + BlackLevelStep) def _gamma_reset(self): "reset gamma and black level to the defaults" SetGamma(1.0, 0) PageDisplayActions = PageDisplayActions() ForcedActions.update(("-zoom-pan", "+zoom-pan", "-box-add", "+box-add", "-box-zoom", "+box-zoom")) # main event handling function # takes care that $page-timeout events are handled with least priority def EventHandlerLoop(): poll = True page_timeout = False while True: ev = Platform.GetEvent(poll) poll = bool(ev) if not ev: # no more events in the queue -> can now insert a $page-timeout if page_timeout: ev = "$page-timeout" page_timeout = False else: continue elif ev == "$page-timeout": page_timeout = True continue if VideoPlaying: # video mode -> ignore all non-video actions ProcessEvent(ev, VideoActions) elif ProcessEvent(ev, PageDisplayActions): # normal action has been handled -> done pass elif ev and (ev[0] == '*'): keyfunc = GetPageProp(Pcurrent, 'keys', {}).get(ev[1:], None) if keyfunc: SafeCall(keyfunc) else: # handle a shortcut key ctrl = ev.startswith('*ctrl+') if ctrl: ev = '*' + ev[6:] page = HandleShortcutKey(ev, Pcurrent) if page: TransitionTo(page, allow_transition=not(ctrl)) ##### FILE LIST GENERATION ##################################################### ImageExts = set('.'+x for x in "jpg jpeg png tif tiff bmp ppm pgm".split()) VideoExts = set('.'+x for x in "avi mov mp4 mkv ogv mpg mpeg m1v m2v m4v mts m2ts m2t ts webm 3gp flv qt".split()) AllExts = set(list(ImageExts) + list(VideoExts) + [".pdf"]) def CheckExt(name, exts): return os.path.splitext(name)[1].lower() in exts def IsImageFile(name): return CheckExt(name, ImageExts) def IsVideoFile(name): return CheckExt(name, VideoExts) def IsPlayable(name): return CheckExt(name, AllExts) def AddFile(name, title=None, implicit=False): global FileList, FileName # handle list files if name.startswith('@') and os.path.isfile(name[1:]): name = name[1:] dirname = os.path.dirname(name) try: f = file(name, "r") next_title = None for line in f: line = [part.strip() for part in line.split('#', 1)] if len(line) == 1: subfile = line[0] title = None else: subfile, title = line if subfile: AddFile(os.path.normpath(os.path.join(dirname, subfile)), title, implicit=True) f.close() except IOError: print >>sys.stderr, "Error: cannot read list file `%s'" % name return # generate absolute path path_sep_at_end = name.endswith(os.path.sep) name = os.path.normpath(os.path.abspath(name)).rstrip(os.path.sep) if path_sep_at_end: name += os.path.sep # set FileName to first (explicitly specified) input file if not implicit: if not FileList: FileName = name else: FileName = "" if os.path.isfile(name): if IsPlayable(name): FileList.append(name) if title: SetFileProp(name, 'title', title) else: print >>sys.stderr, "Warning: input file `%s' has unrecognized file type" % name elif os.path.isdir(name): images = [os.path.join(name, f) for f in os.listdir(name) if IsImageFileName(f)] images.sort(lambda a, b: cmp(a.lower(), b.lower())) if not images: print >>sys.stderr, "Warning: no image files in directory `%s'" % name for img in images: AddFile(img, implicit=True) else: files = list(filter(IsPlayable, glob.glob(name))) if files: for f in files: AddFile(f, implicit=True) else: print >>sys.stderr, "Error: input file `%s' not found" % name ##### INITIALIZATION ########################################################### LoadDefaultBindings() def main(): global gl, ScreenWidth, ScreenHeight, TexWidth, TexHeight, TexSize global TexMaxS, TexMaxT, PixelX, PixelY, LogoImage global OverviewGridSize, OverviewCellX, OverviewCellY global OverviewOfsX, OverviewOfsY, OverviewBorder, OverviewImage, OverviewPageCount global OverviewPageMap, OverviewPageMapInv, FileName, FileList, PageCount global DocumentTitle, PageProps, LogoTexture, OSDFont global Pcurrent, Pnext, Tcurrent, Tnext, InitialPage global CacheFile, CacheFileName, BaseWorkingDir, RenderToDirectory global PAR, DAR, TempFileName, Bare, MaxZoomFactor global BackgroundRendering, FileStats, RTrunning, RTrestart, StartTime global CursorImage, CursorVisible, InfoScriptPath global HalfScreen, AutoAdvance, WindowPos global BoxFadeDarknessBase, BoxZoomDarknessBase, SpotRadiusBase global BoxIndexBuffer, UseBlurShader # allocate temporary file TempFileName = None try: TempFileName = tempfile.mktemp(prefix="impressive-", suffix="_tmp") except EnvironmentError: if not Bare: print >>sys.stderr, "Could not allocate temporary file, reverting to --bare mode." Bare = True # some input guesswork BaseWorkingDir = os.getcwd() if not(FileName) and (len(FileList) == 1): FileName = FileList[0] if FileName and not(FileList): AddFile(FileName) if FileName: DocumentTitle = os.path.splitext(os.path.split(FileName)[1])[0] # early graphics initialization Platform.Init() # detect screen size and compute aspect ratio if Fullscreen and (UseAutoScreenSize or not(Platform.allow_custom_fullscreen_res)): size = Platform.GetScreenSize() if size: ScreenWidth, ScreenHeight = size print >>sys.stderr, "Detected screen size: %dx%d pixels" % (ScreenWidth, ScreenHeight) if DAR is None: PAR = 1.0 DAR = float(ScreenWidth) / float(ScreenHeight) else: PAR = DAR / float(ScreenWidth) * float(ScreenHeight) # override some irrelevant settings in event test mode if EventTestMode: FileList = ["XXX.EventTestDummy.XXX"] InfoScriptPath = None RenderToDirectory = False InitialPage = None HalfScreen = False # fill the page list if Shuffle: random.shuffle(FileList) PageCount = 0 for name in FileList: ispdf = name.lower().endswith(".pdf") if ispdf: # PDF input -> initialize renderers and if none available, reject if not InitPDFRenderer(): print >>sys.stderr, "Ignoring unrenderable input file '%s'." % name continue # try to pre-parse the PDF file pages = 0 out = [(ScreenWidth + Overscan, ScreenHeight + Overscan), (ScreenWidth + Overscan, ScreenHeight + Overscan)] res = [(72.0, 72.0), (72.0, 72.0)] # phase 1: internal PDF parser try: pages, pdf_width, pdf_height = analyze_pdf(name) out = [ZoomToFit((pdf_width, pdf_height * PAR)), ZoomToFit((pdf_height, pdf_width * PAR))] res = [(out[0][0] * 72.0 / pdf_width, out[0][1] * 72.0 / pdf_height), (out[1][1] * 72.0 / pdf_width, out[1][0] * 72.0 / pdf_height)] except KeyboardInterrupt: raise except: pass # phase 2: use pdftk if pdftkPath and TempFileName: try: assert 0 == subprocess.Popen([pdftkPath, name, "dump_data", "output", TempFileName + ".txt"]).wait() title, pages = pdftkParse(TempFileName + ".txt", PageCount) if title and (len(FileList) == 1): DocumentTitle = title except KeyboardInterrupt: raise except: pass # phase 3: use mutool (if pdftk wasn't successful) if not(pages) and mutoolPath: try: proc = subprocess.Popen([mutoolPath, "info", name], stdout=subprocess.PIPE) title, pages = mutoolParse(proc.stdout) assert 0 == proc.wait() if title and (len(FileList) == 1): DocumentTitle = title except KeyboardInterrupt: raise except: pass else: # image or video file pages = 1 if IsVideoFile(name): SetPageProp(PageCount + 1, '_video', True) SetPageProp(PageCount + 1, '_title', os.path.split(name)[-1]) # validity check if not pages: print >>sys.stderr, "WARNING: The input file `%s' could not be analyzed." % name continue # add pages and files into PageProps and FileProps pagerange = list(range(PageCount + 1, PageCount + pages + 1)) for page in pagerange: SetPageProp(page, '_file', name) if ispdf: SetPageProp(page, '_page', page - PageCount) title = GetFileProp(name, 'title') if title: SetPageProp(page, '_title', title) SetFileProp(name, 'pages', GetFileProp(name, 'pages', []) + pagerange) SetFileProp(name, 'offsets', GetFileProp(name, 'offsets', []) + [PageCount]) if not GetFileProp(name, 'stat'): SetFileProp(name, 'stat', my_stat(name)) if ispdf: SetFileProp(name, 'out', out) SetFileProp(name, 'res', res) PageCount += pages # no pages? strange ... if not PageCount: print >>sys.stderr, "The presentation doesn't have any pages, quitting." sys.exit(1) # if rendering is wanted, do it NOW if RenderToDirectory: sys.exit(DoRender()) # load and execute info script if not InfoScriptPath: InfoScriptPath = FileName + ".info" LoadInfoScript() # initialize some derived variables BoxFadeDarknessBase = BoxFadeDarkness BoxZoomDarknessBase = BoxZoomDarkness SpotRadiusBase = SpotRadius # get the initial page number if not InitialPage: InitialPage = GetNextPage(0, 1) Pcurrent = InitialPage if (Pcurrent <= 0) or (Pcurrent > PageCount): print >>sys.stderr, "Attempt to start the presentation at an invalid page (%d of %d), quitting." % (InitialPage, PageCount) sys.exit(1) # initialize graphics try: Platform.StartDisplay() except Exception, e: print >>sys.stderr, "FATAL: failed to create rendering surface in the desired resolution (%dx%d)" % (ScreenWidth, ScreenHeight) print >>sys.stderr, " detailed error message:", e sys.exit(1) if Fullscreen: Platform.SetMouseVisible(False) CursorVisible = False if (Gamma <> 1.0) or (BlackLevel <> 0): SetGamma(force=True) # initialize OpenGL try: gl = Platform.LoadOpenGL() print >>sys.stderr, "OpenGL renderer:", GLRenderer # check if graphics are unaccelerated renderer = GLRenderer.lower().replace(' ', '').replace('(r)', '') if not(renderer) \ or (renderer in ("mesaglxindirect", "gdigeneric")) \ or renderer.startswith("software") \ or ("llvmpipe" in renderer): print >>sys.stderr, "WARNING: Using an OpenGL software renderer. Impressive will work, but it will" print >>sys.stderr, " very likely be too slow to be usable." # check for old hardware that can't deal with the blur shader for substr in ("i915", "intel915", "intel945", "intelq3", "intelg3", "inteligd", "gma900", "gma950", "gma3000", "gma3100", "gma3150"): if substr in renderer: UseBlurShader = False # check the OpenGL version (2.0 needed to ensure NPOT texture support) extensions = set((gl.GetString(gl.EXTENSIONS) or "").split()) if (GLVersion < "2") and (not("GL_ARB_shader_objects" in extensions) or not("GL_ARB_texture_non_power_of_two" in extensions)): raise ImportError("OpenGL version %r is below 2.0 and the necessary extensions are unavailable" % GLVersion) except ImportError, e: if GLVendor: print >>sys.stderr, "OpenGL vendor:", GLVendor if GLRenderer: print >>sys.stderr, "OpenGL renderer:", GLRenderer if GLVersion: print >>sys.stderr, "OpenGL version:", GLVersion print >>sys.stderr, "FATAL:", e print >>sys.stderr, "This likely means that your graphics driver or hardware is too old." sys.exit(1) # some further OpenGL configuration if Verbose: GLShader.LOG_DEFAULT = GLShader.LOG_IF_NOT_EMPTY for shader in RequiredShaders: shader.get_instance() if UseBlurShader: try: BlurShader.get_instance() except GLShaderCompileError: UseBlurShader = False if Verbose: if UseBlurShader: print >>sys.stderr, "Using blur-and-desaturate shader for highlight box and spotlight mode." else: print >>sys.stderr, "Using legacy multi-pass blur for highlight box and spotlight mode." gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) BoxIndexBuffer = HighlightIndexBuffer(4) # set up the OpenGL texture size (identical to the screen size because we # require non-power-of-two texture support by now) gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1) TexWidth = ScreenWidth TexHeight = ScreenHeight TexMaxS = 1.0 TexMaxT = 1.0 TexSize = TexWidth * TexHeight * 3 # determine maximum texture size maxsize = c_int(0) gl.GetIntegerv(gl.MAX_TEXTURE_SIZE, ctypes.byref(maxsize)) maxsize = float(maxsize.value) if (maxsize > ScreenWidth) and (maxsize <= 65536): MaxZoomFactor = min(MaxZoomFactor, maxsize / ScreenWidth, maxsize / ScreenHeight) if Verbose: print >>sys.stderr, "Maximum texture size is %.0f pixels, using maximum zoom level of %.1f." % (maxsize, MaxZoomFactor) # set up some variables PixelX = 1.0 / ScreenWidth PixelY = 1.0 / ScreenHeight ScreenAspect = float(ScreenWidth) / float(ScreenHeight) # prepare logo image LogoImage = Image.open(cStringIO.StringIO(LOGO.decode('base64'))) LogoTexture = gl.make_texture(gl.TEXTURE_2D, filter=gl.NEAREST, img=LogoImage) DrawLogo() Platform.SwapBuffers() # initialize OSD font try: OSDFont = GLFont(FontTextureWidth, FontTextureHeight, FontList, FontSize, search_path=FontPath) DrawLogo() titles = [] for key in ('title', '_title'): titles.extend([p[key] for p in PageProps.itervalues() if key in p]) if titles: OSDFont.AddString("".join(titles)) except ValueError: print >>sys.stderr, "The OSD font size is too large, the OSD will be rendered incompletely." except IOError: print >>sys.stderr, "Could not open OSD font file, disabling OSD." except (NameError, AttributeError, TypeError): print >>sys.stderr, "Your version of PIL is too old or incomplete, disabling OSD." # handle event test mode if EventTestMode: DoEventTestMode() # initialize mouse cursor if EnableCursor and (CursorImage or not(Platform.has_hardware_cursor)): img = None if CursorImage and not(CursorImage.lower() in ("-", "default")): try: img = Image.open(CursorImage).convert('RGBA') img.load() except: print >>sys.stderr, "Could not open the mouse cursor image, using standard cursor." img = None CursorImage = PrepareCustomCursor(img) else: CursorImage = None # set up page cache if CacheMode == PersistentCache: if not CacheFileName: CacheFileName = FileName + ".cache" InitPCache() if CacheMode == FileCache: CacheFile = tempfile.TemporaryFile(prefix="impressive-", suffix=".cache") # overview preparations if EnableOverview: # initialize overview metadata OverviewPageMap=[i for i in xrange(1, PageCount + 1) \ if GetPageProp(i, ('overview', '_overview'), True) \ and (i >= PageRangeStart) and (i <= PageRangeEnd)] OverviewPageCount = max(len(OverviewPageMap), 1) OverviewPageMapInv = {} for page in xrange(1, PageCount + 1): OverviewPageMapInv[page] = len(OverviewPageMap) - 1 for i in xrange(len(OverviewPageMap)): if OverviewPageMap[i] >= page: OverviewPageMapInv[page] = i break # initialize overview page geometry OverviewGridSize = 1 while OverviewPageCount > OverviewGridSize * OverviewGridSize: OverviewGridSize += 1 if HalfScreen: # in half-screen mode, temporarily override ScreenWidth saved_screen_width = ScreenWidth ScreenWidth /= 2 OverviewCellX = int(ScreenWidth / OverviewGridSize) OverviewCellY = int(ScreenHeight / OverviewGridSize) OverviewOfsX = int((ScreenWidth - OverviewCellX * OverviewGridSize)/2) OverviewOfsY = int((ScreenHeight - OverviewCellY * \ int((OverviewPageCount + OverviewGridSize - 1) / OverviewGridSize)) / 2) while OverviewBorder and (min(OverviewCellX - 2 * OverviewBorder, OverviewCellY - 2 * OverviewBorder) < 16): OverviewBorder -= 1 OverviewImage = Image.new('RGB', (TexWidth, TexHeight)) if HalfScreen: OverviewOfsX += ScreenWidth ScreenWidth = saved_screen_width # fill overlay "dummy" images dummy = LogoImage.copy() border = max(OverviewLogoBorder, 2 * OverviewBorder) maxsize = (OverviewCellX - border, OverviewCellY - border) if (dummy.size[0] > maxsize[0]) or (dummy.size[1] > maxsize[1]): dummy.thumbnail(ZoomToFit(dummy.size, maxsize), Image.ANTIALIAS) margX = int((OverviewCellX - dummy.size[0]) / 2) margY = int((OverviewCellY - dummy.size[1]) / 2) dummy = dummy.convert(mode='RGB') for page in range(OverviewPageCount): pos = OverviewPos(page) OverviewImage.paste(dummy, (pos[0] + margX, pos[1] + margY)) del dummy # compute auto-advance timeout, if applicable if EstimatedDuration and AutoAutoAdvance: time_left = EstimatedDuration * 1000 pages = 0 p = InitialPage while p: override = GetPageProp(p, 'timeout') if override: time_left -= override else: pages += 1 pnext = GetNextPage(p, 1) if pnext: time_left -= GetPageProp(p, 'transtime', TransitionDuration) p = pnext if pages and (time_left >= pages): AutoAdvance = time_left / pages print >>sys.stderr, "Setting auto-advance timeout to %.1f seconds." % (0.001 * AutoAdvance) else: print >>sys.stderr, "Warning: Could not determine auto-advance timeout automatically." # set up background rendering if not HaveThreads: print >>sys.stderr, "Note: Background rendering isn't available on this platform." BackgroundRendering = False # if caching is enabled, pre-render all pages if CacheMode and not(BackgroundRendering): DrawLogo() DrawProgress(0.0) Platform.SwapBuffers() for pdf in FileProps: if pdf.lower().endswith(".pdf"): ParsePDF(pdf) stop = False progress = 0.0 def prerender_action_handler(action): if action in ("$quit", "*quit"): Quit() for page in range(InitialPage, PageCount + 1) + range(1, InitialPage): while True: ev = Platform.GetEvent(poll=True) if not ev: break ProcessEvent(ev, prerender_action_handler) if ev.startswith('*'): stop = True if stop: break if (page >= PageRangeStart) and (page <= PageRangeEnd): PageImage(page) DrawLogo() progress += 1.0 / PageCount DrawProgress(progress) Platform.SwapBuffers() # create buffer textures DrawLogo() Platform.SwapBuffers() Tcurrent, Tnext = [gl.make_texture(gl.TEXTURE_2D, gl.CLAMP_TO_EDGE, gl.LINEAR) for dummy in (1,2)] # prebuffer current and next page Pnext = 0 RenderPage(Pcurrent, Tcurrent) DrawCurrentPage() PageEntered(update_time=False) PreloadNextPage(GetNextPage(Pcurrent, 1)) # some other preparations PrepareTransitions() GenerateSpotMesh() if PollInterval: Platform.ScheduleEvent("$poll-file", PollInterval * 1000, periodic=True) # start the background rendering thread if CacheMode and BackgroundRendering: RTrunning = True thread.start_new_thread(RenderThread, (Pcurrent, Pnext)) # parse PDF file if caching is disabled if not CacheMode: for pdf in FileProps: if pdf.lower().endswith(".pdf"): SafeCall(ParsePDF, [pdf]) # start output and enter main loop StartTime = Platform.GetTicks() if TimeTracking or TimeDisplay: EnableTimeTracking(True) Platform.ScheduleEvent("$timer-update", 100, periodic=True) if not(Fullscreen) and (not(EnableCursor) or CursorImage): Platform.SetMouseVisible(False) if FadeInOut: LeaveFadeMode() else: DrawCurrentPage() UpdateCaption(Pcurrent) EventHandlerLoop() # never returns # event test mode implementation def DoEventTestMode(): last_event = "(None)" need_redraw = True cx = ScreenWidth / 2 y1 = ScreenHeight / 5 y2 = (ScreenHeight * 4) / 5 if OSDFont: dy = OSDFont.GetLineHeight() Platform.ScheduleEvent('$dummy', 1000) # required to ensure that time measurement works :( print >>sys.stderr, "Entering Event Test Mode." print " timestamp | delta-time | event" t0 = Platform.GetTicks() while True: if need_redraw: DrawLogo() if OSDFont: gl.Enable(gl.BLEND) OSDFont.BeginDraw() OSDFont.Draw((cx, y1 - dy), "Event Test Mode", align=Center, beveled=False, bold=True) OSDFont.Draw((cx, y1), "press Alt+F4 to quit", align=Center, beveled=False) OSDFont.Draw((cx, y2 - dy), "Last Event:", align=Center, beveled=False, bold=True) OSDFont.Draw((cx, y2), last_event, align=Center, beveled=False) OSDFont.EndDraw() gl.Disable(gl.BLEND) Platform.SwapBuffers() need_redraw = False ev = Platform.GetEvent() if ev == '$expose': need_redraw = True elif ev == '$quit': Quit() elif ev and ev.startswith('*'): now = Platform.GetTicks() print "%7d ms | %7d ms | %s" % (int(now), int(now - t0), ev[1:]) t0 = now last_event = ev[1:] need_redraw = True # wrapper around main() that ensures proper uninitialization def run_main(): global CacheFile try: try: main() except SystemExit: raise except KeyboardInterrupt: pass except: print >>sys.stderr print >>sys.stderr, 79 * "=" print >>sys.stderr, "OOPS! Impressive crashed!" print >>sys.stderr, "This shouldn't happen. Please report this incident to the author, including the" print >>sys.stderr, "full output of the program, particularly the following lines. If possible," print >>sys.stderr, "please also send the input files you used." print >>sys.stderr print >>sys.stderr, "Impressive version:", __version__ print >>sys.stderr, "Python version:", sys.version print >>sys.stderr, "PyGame version:", pygame.__version__ print >>sys.stderr, "PIL version:", Image.VERSION if PDFRenderer: print >>sys.stderr, "PDF renderer:", PDFRenderer.name else: print >>sys.stderr, "PDF renderer: None" if GLVendor: print >>sys.stderr, "OpenGL vendor:", GLVendor if GLRenderer: print >>sys.stderr, "OpenGL renderer:", GLRenderer if GLVersion: print >>sys.stderr, "OpenGL version:", GLVersion if hasattr(os, 'uname'): uname = os.uname() print >>sys.stderr, "Operating system: %s %s (%s)" % (uname[0], uname[2], uname[4]) else: print >>sys.stderr, "Python platform:", sys.platform if os.path.isfile("/usr/bin/lsb_release"): lsb_release = subprocess.Popen(["/usr/bin/lsb_release", "-sd"], stdout=subprocess.PIPE) print >>sys.stderr, "Linux distribution:", lsb_release.stdout.read().strip() lsb_release.wait() print >>sys.stderr, "Command line:", ' '.join(('"%s"'%arg if (' ' in arg) else arg) for arg in sys.argv) traceback.print_exc(file=sys.stderr) finally: StopMPlayer() # ensure that background rendering is halted Lrender.acquire() Lcache.acquire() # remove all temp files if 'CacheFile' in globals(): del CacheFile if TempFileName: for tmp in glob.glob(TempFileName + "*"): try: os.remove(tmp) except OSError: pass Platform.Quit() # release all locks try: if Lrender.locked(): Lrender.release() except: pass try: if Lcache.locked(): Lcache.release() except: pass try: if Loverview.locked(): Loverview.release() except: pass ##### COMMAND-LINE PARSER AND HELP ############################################# def if_op(cond, res_then, res_else): if cond: return res_then else: return res_else def HelpExit(code=0): print """A nice presentation tool. Usage: """+os.path.basename(sys.argv[0])+""" [OPTION...] You may either play a PDF file, a directory containing image files or individual image files. Input options: -r, --rotate rotate pages clockwise in 90-degree steps --scale scale images to fit screen (not used in PDF mode) --supersample use supersampling (only used in PDF mode) -s --supersample for PDF files, --scale for image files -I, --script set the path of the info script -u, --poll check periodically if the source file has been updated and reload it if it did -X, --shuffle put input files into random order -h, --help show this help text and exit Output options: -o, --output don't display the presentation, only render to .png --fullscreen start in fullscreen mode -ff, --fake-fullscreen start in "fake fullscreen" mode -f, --windowed start in windowed mode -g, --geometry set window size or fullscreen resolution -A, --aspect adjust for a specific display aspect ratio (e.g. 5:4) -G, --gamma specify startup gamma and black level Page options: -i, --initialpage start with page -p, --pages only cache pages in the specified range; implicitly sets -i -w, --wrap go back to the first page after the last page -O, --autooverview automatically derive page visibility on overview page -O first = show pages with captions -O last = show pages before pages with captions -Q, --autoquit quit after the last slide (no effect with --wrap) --nooverview disable overview page Display options: -t, --transition force a specific transitions or set of transitions -l, --listtrans print a list of available transitions and exit -F, --font use a specific TrueType font file for the OSD -S, --fontsize specify the OSD font size in pixels -C, --cursor use a .png image as the mouse cursor -N, --nocursor don't show a mouse cursor at all -L, --layout set the OSD layout (please read the documentation) -z, --zoom set zoom factor (default: 2.0) --maxzoom maximum factor to render high-resolution zoom -x, --fade fade in at start and fade out at end --spot-radius set the initial radius of the spotlight, in pixels --invert display slides in inverted colors --min-box-size set minimum size of a highlight box, in pixels --box-edge size of highlight box borders, in pixels --zbox-edge size of zoom box borders, in pixels --darkness

set highlight box mode darkness to

percent --zoomdarkness

set box-zoom mode darkness to

percent --noblur use legacy blur implementation Timing options: -M, --minutes display time in minutes, not seconds --clock show current time instead of time elapsed --tracking enable time tracking mode --time-display enable time display (implies --tracking) -a, --auto automatically advance to next page after some seconds -d, --duration

Impressive Documentation

http://impressive.sourceforge.net/
Version: 0.12.0
Author: Martin J. Fiedler
Last updated: 2018-02-04

Table Of Contents

  1. Description
  2. Installation
  3. Command Line Parameters
  4. Usage
  5. Control Configuration
  6. Info Scripts
  7. Other Useful Information

Description

Impressive is a simple presentation program that displays slideshows of PDF documents, image files or video files. Rendering is done via OpenGL, which allows for some “eye candy” effects.

A somewhat-modern GPU (graphics processing unit) supporting OpenGL 2.0 or OpenGL ES 2.0 and appropriate drivers are required to run Impressive.

Installation

There are three basic ways to get an Impressive installation on your computer. The following section will guide you through each of these methods.

The easy method: use a pre-built package

For Windows systems, a ZIP file with a pre-built version of Impressive and all required external tools can be downloaded from the web site. There is no real installation procedure – the ZIP file can be extracted into any folder. One of the extracted file is called impressive.exe. PDF files or directories containing images can simply dropped onto this file in Explorer to run a presentation with default settings.
A more comfortable method is installing Impressive in the »Send To« menu that's part of each file's context menu. This is done by opening the location shell:sendto in Explorer and creating a shortcut to impressive.exe there. After that, a presentation can be started by right-clicking a PDF file and selecting »Send To → Impressive«.

Most GNU/Linux distributions offer Impressive as part of their standard package repositories, among them Debian, Ubuntu, Fedora, OpenSUSE and Arch Linux. The package is typically called impressive and will install all necessary dependencies.

On Mac OS X, there is a py-impressive package in the MacPorts repositories, but at the time of writing, it's extremely outdated and thus not recommended.

In general, the easy method of installing the Windows ZIP or distribution package is simple and convenient, but it has caveats: Releases of Impressive only happen in long and irregular intervals, and the package maintainers may be a few versions behind. Recent features and bugfixes will thus not be available.

Using the source code release

Another way to get Impressive is downloading the platform-independent source code version. The most important file in this archive is impressive.py, which contains all the code of Impressive and can be run like any executable on Unix-like systems. However, the following external libraries and utilities have to be installed as well for Impressive to work:

  • Python 2.7 — www.python.org (required) – Python 3.x will not work!
  • PyGame (the SDL binding for Python) — www.pygame.org (required)
  • PIL, the Python Imaging Library, or Pillow, a compatible fork of it — www.pythonware.com/products/pil (required)
  • pdftk 1.44 or higher for retrieving PDF metadata (page titles) — www.accesspdf.com/pdftk (recommended; required if MuPDF is not available)
  • the MuPDF utilities for PDF rendering and metadata retrieval (hyperlinks) — www.mupdf.com (required)
  • Xpdf or the Poppler utilities for alternate PDF rendering — www.foolabs.com/xpdf/ or http://poppler.freedesktop.org/ (required if MuPDF is not available; not required otherwise)
  • GhostScript for alternate PDF rendering — www.cs.wisc.edu/~ghost (required if neither MuPDF nor Xpdf/Poppler utilities are available; not required otherwise)
  • xdg-utils for starting web or e-mail hyperlinks from PDF documents — portland.freedesktop.org (recommended; does not apply to Win32)
  • PyWin32, the Python for Windows extensions — pywin32.sourceforge.net (optional; applies to Win32 only)
  • MPlayer or MPlayer2 for sound and video playback — www.mplayerhq.hu (optional)
  • omxplayer for video playback on Raspberry Pi — github.com/popcornmix/omxplayer (optional; applies to Raspberry Pi only)
  • FFmpeg for more efficient generation of video preview images — ffmpeg.org (optional)

On Unix-like systems, these dependencies are typically available in the major distributions' package repositories and can be installed easily with the package managers, e.g.:

  • Debian/Ubuntu: aptitude install python-pygame python-imaging pdftk mupdf-tools xdg-utils mplayer ffmpeg
  • Fedora: yum -i pygame python-pillow pdftk mupdf xdg-utils mplayer ffmpeg
  • Arch Linux with AUR: pacman -S python2-pygame python2-pillow pdftk mupdf xdg-utils mplayer ffmpeg
  • Mac OS X with MacPorts: port install py27-game py27-pil pdftk mupdf xdg-utils mplayer ffmpeg

Using the latest version from SVN

The source code of Impressive is maintained in a Subversion (SVN) repository. For users that want the newest version or want to help with debugging or developing Impressive, it is recommended to check out this repository:
svn co http://svn.emphy.de/impressive/trunk/impressive
Regarding external dependencies, the section above applies as well; in addition, Subversion (whose package is usually called svn or subversion) is obviously required too.

After checking out the sources, the impressive.py program will still be missing. Instead, the main program is called impressive_dev.py and is functionally equivalent; it pulls the actual source files from the src subdirectory and runs them. To get a real stand-alone impressive.py file for installation or redistribution, it needs to be generated using either make (if GNU Make is installed) or python compile.py. On Linux systems, sudo make install can then be used to install Impressive at /usr/local/bin/impressive and its manpage at /usr/local/man/man1/impressive.1. These two installed files can be removed again with sudo make uninstall.

Special considerations for Raspberry Pi

Impressive runs on Rasbperry Pi Single-Board Computers with bearable performance, if the following notes are taken into consideration:

  • Overscan must be off (set disable_overscan=1 in /boot/config.txt, or run raspi-config → Advanced Menu → Overscan).
  • The classic Broadcom OpenGL ES driver stack must be used, not the Fake-KMS or Full-KMS Mesa drivers (disable dtoverlay=vc4-(f)kms-v3d in /boot/config.txt, or run raspi-config → Advanced Menu → GL Driver → Legacy: Original non-GL desktop driver).
  • Doesn't require X11 – can also be run from a normal text console in Raspbian Lite.
  • Can be run from X11, but only in fullscreen mode.
  • Video playback uses omxplayer instead of MPlayer. This has the following advantages and drawbacks:
    • Only H.264 and (if licensed on the specific device) MPEG-2 video codecs are supported.
    • These are, howver, hardware-decoded with full performance.
    • Fine-grained seeking (1 second / 10 second) is not supported. 1-second seek operations are ignored, 10-second seek maps to 30-second seek.
  • Since microSD storage is very slow on the Raspberry Pi, consider using the -cz/--cache compressed option for better performance. This will consume more RAM though, which is also a scarce resource on the device, but typical presentation slides with uniformly colored backgrounds compress very well and shouldn't be a problem.

Using Impressive, it is possible to convert a Raspberry Pi into an automated »digital signage« system, i.e. a system with the sole purpose of repeatedly showing a single presentation, which can consist of one or more PDF files, image files, or videos. Currently, there's no automated installation for this mode; however the following script (which is included in an SVN checkout as well) contains the necessary code and installation instructions (for advanced users):
https://svn.emphy.de/impressive/trunk/impressive/boot_impressive.sh

Command Line Parameters

When used from the command line (some terminal and shell in Unix, cmd.exe on Windows), Impressive accepts a range of command-line options in GNU getopt syntax.

The following options are available (in alphabetical order):

-a <seconds> or --auto <seconds>
Automatically advance to the next page after the given number of seconds. Together with the -w option (described below), this can be used to create automatic slideshows.
Note that this option will not interrupt video playback: If a video is longer than the interval specified with --auto, it will be played until the end, but Impressive will change to the next page directly after that.
-A <X>:<Y> or --aspect <X>:<Y>
Specifies the display aspect ratio. Normally, Impressive assumes that the pixel aspect ratio is 1:1 (square pixels), regardless of the display resolution that has been set up. If a resolution has been selected that doesn't match the display's aspect ratio, the screen will be distorted. To overcome this, this option may be used to manually specify the display aspect ratio, e.g. »-A 16:9«. Note that this option has no effect if the MuPDF backend is used for rendering.
-b or --noback
Disables background rendering. By default, Impressive will pre-render all pages in a separate background thread while the presentation runs. If this option is specified, it will instead render all pages immediately on startup. This option has no effect if caching is disabled (--cache none, see below).
-bare
Disables all functionality that relies on temporary files, specifically video frame extraction using MPlayer (extracting video frames with FFmpeg still works), PDF page title extraction, PDF hyperlinks, and even PDF rendering for all backends except MuPDF 1.4 or newer on Unix-like operating systems.
--box-edge <pixels>
Sets the border size (in pixels when unzoomed) of the highlight boxes' and spotlight's smooth edges. The default value is 4 pixels; it can be set to zero, in which case highlight boxes and the spotlight will have hard edges.
-B <ms> or --boxfade <ms>
Sets the duration (in milliseconds) of the highlight box fade-in/fade-out animation. Default value: 100 ms.
-c <mode> or --cache <mode>
Specifies the page cache mode to use. Valid options are:
none
Disables page caching altogether, only the current and the following page will be kept in RAM. Jumping between pages will be very slow, because Impressive will need to render the requested pages on the fly. In addition, the overview page won't be complete until every page has been shown at least once.
memory
Caches all page images in memory. This is the fastest method, but it requires very large amounts of memory (about 3 MiB per page at 1024x768 resolution).
compressed or z
Caches all page images in memory as well, but in compressed form. This will be a little slower than memory mode, but on pages with uniform backgrounds, this will easily reduce the required amount of RAM by a factor of 20.
disk
Like memory, but uses a temporary file rather than memory for storage. This is the default.
persistent
Uses a permanent cache file for caching. This file will not be deleted when Impressive quits and will be reused on subsequent invocations. The default name for the cache file is derived like the names for Info Scripts (see below for an explanation), but with a .cache file name extension instead of .info. This method is a little bit slower than disk mode, but the time span until the overview page is fully populated will be significantly decreased if Impressive is ran again with the same input files and options.
The mode name may be abbreviated at will, down to one character. Thus, --cache persistent, -c persist and even -cp are all synonyms.
-C <filename>[:<X>,<Y>] or --cursor <filename>[:<X>,<Y>]
This option can be used to specify an image that shall be used as a (software) mouse cursor instead of the normal (hardware) one. It can either be a name of an image file (typically a transparent .png) or one of the special values »-« (dash) or »default«, both of which select a built-in cursor image. On platforms that do not support a hardware cursor, this default cursor will also be used as a default if this option is not used.
If the name of an external file is specified, the position of the hotspot (the pixel inside the cursor where the actual mouse position is located) can be specified as well, for example: --cursor mycursor.png:2,4.
--clock
If this option is enabled, the current time will be shown instead of the elapsed time if time display is activated (with the T key in the default control configuration). Compatible with the -M option.
--control-help
This option will output a short help screen of how the control configuration (keyboard and mouse bindings) work, along with a list of all recognized events and actions and a dump of the current bindings (which is the default configuration if no other -e/--bind or -E/--controls options precede this option). After that, Impressive will exit; no presentation will be started.
-d <time> or --duration <time>
Specifies the expected run time of the presentation. The time parameter can be either a number of seconds or a human-readable time like 1:23 (1 minute and 23 seconds), 4:56h (4 hours and 56 minutes), 3m45s (3 minutes and 45 seconds), 5m (5 minutes) or 1:23:45 (1 hour, 23 minutes and 45 seconds).
If an expected duration is specified, Impressive will show a semi-transparent green progress bar at the lower edge of the screen, indicating how much time has already passed. If the time is up, the bar will occupy the whole edge and fade to yellow (at 125% of the expected time) to red (at 150% or more).
-D <ms> or --mousedelay <ms>
Sets the time (in milliseconds) the mouse cursor is shown in fullscreen mode if it is not moved. There are two special values: 0 (zero) shows the mouse cursor permanently, 1 (one) hides it completely. The default value is 3000 ms.
--darkness <percentage>
Specifies how much the screen shall become darker when using highlight boxes or spotlight mode. The value is specified in percent, with 25 being the default. A value of zero would mean no darkening at all (the screen would just be blurred slightly, and desaturated if the graphics hardware supports it), and a value of 100 would make everything but the highlighted parts of the screen black.
-e <bindings> or --bind <bindings>
Configures keyboard or mouse bindings. For the syntax of the argument, refer to the Control Configuration section of the manual.
-E <filename> or --controls <filename>
Loads a configuration file for keyboard and mouse bindings. For the syntax of these files, refer to the Control Configuration section of the manual.
--evtest
If this option is specified, Impressive will not start a normal presentation, but instead enter the so-called »Event Test Mode« after the display has been initialized. In this mode, Impressive will simply capture all keyboard and mouse events and display the name of the last event on the screen. In addition, events will be logged to standard output. This mode helps with determining the event names for each key when creating a custom control configuration (see the Control Configuration section).
-f
Sets the window/fullscreen mode Impressive shall start up with. This option can be specified multiple times; each times it is found, the next of the three options is selected, in the follwing order:
  • True fullscreen mode, including setting the video mode. This is the default mode.
  • Windowed mode, where Impressive runs in a standard window.
  • »Fake-fullscreen« mode, where fullscreen is emulated by uding a window without border that fills the whole screen. Setting the display resolution is not possible in this mode.
Each of these modes has a corresponding long option that enables it directly (--fullscreen, --windowed and --fake-fullscreen).
Note: On Windows systems, toggling fullscreen mode while Impressive runs does not work. So, the command-line options are the only way to set whether fullscreen mode shall be used or not.
--fullscreen
Starts Impressive in fullscreen mode (the default). See the discussion of the -f option for details.
--fake-fullscreen
Starts Impressive in »fake fullscreen« mode. See the discussion of the -f option for details.
-F <file> or --font <file>
Impressive uses some kind of text OSD to overlay the current page number and title (if available) on the overview page. This option can be used to specify a TrueType font file (.ttf) to use for the OSD. If the path specified doesn't directly point to a useable font file, Impressive will try to search the system font directories. On Unix-like systems, it assumes that all fonts are stored below /usr/share/fonts, /usr/local/share/fonts or /usr/X11R6/lib/X11/fonts/TTF for this purpose (the latter one is useful for Mac OS X systems specifically). If this option is not specified, Verdana.ttf will be used as a default if the operating system is Windows, or any of DejaVuSans.ttf or Vera.ttf (the typical file name of Bitstream Vera Sans) if it's not.
-g <width>x<height>[+<posX>+<posY>] or --geometry <width>x<height>[+<posX>+<posY>]
Sets the screen size or resolution Impressive shall use (in pixels). On Win32 platforms and Unix platforms with xrandrinstalled, the default screen size for fullscreen mode is the current screen resolution; on other platforms, Impressive uses the highest resolution available to the graphics system. If a standard resolution cannot be determined, the default is 1024x768 pixels. This is also the default for windowed mode.
Furthermore, if the additional parameters posX and posY are present, they specify the position of the upper-left corner of the window (relative to the upper-left corner of the desktop), in pixels, for windowed and fake-fullscreen mode. The values are ignored in »true« fullscreen mode.
-G <gamma>[:<blacklevel>] or --gamma <gamma>[:<blacklevel>]
Sets up the startup gamma and (optional) black level value. The black level is the original image's intensity level (0...254) that is to be mapped to black in Impressive's output. Note that gamma and black level adjustments may be unavailable or constrained on some systems.
-h or --help
If this option is specified, Impressive writes a short command line help screen to standard output and does not start a presentation.
-H or --half-screen
This option makes Impressive show the overview page and OSD elements on the right half of the screen only. The overview page will only show the left half of the slides as previews. Using a multi-monitor setup with a wide virtual screen and specially crafted slides (e.g. those that can be generated with LaTeX's beamer class), this makes it possible to have presenter's notes on the second screen.
Note that transitions and animations are still operating on the whole screen, making some of them (like PagePeel) look ugly. Impressive also disables the overview zoom animation in half-screen mode; it can be re-enabled by explicitly setting it to another value with a later command line option (--zoomtime).
Another limitation is that the allocation of the screen halves (slides left, overview right) is fixed.
-i <page> or --initialpage <page>
Specifies the page number to start with. The default value is 1 (start with the first page). If another value is specified, the page shown by Impressive right after initialization is not the first one of the PDF or image list. Additionally, pre-rendering (if enabled) will also start at the specified page.
-I <filename> or --script <filename>
Overrides automatic derivation of the info script filename and specifies a script file directly.
--invert
This option makes Impressive invert the colors of each page, turning black to white and vice-versa. Note that it is a full RGB inversion, so it will, for example, turn dark green to light purple (and vice-versa) too.
-k or --auto-progress
This option makes Impressive show a progress bar at the bottom of the screen, visualizing the timeout on pages that have one (either specified as a page property or using the --auto command-line option). Nothing is done on pages that don't have a timeout.
The progress bar shown by this option takes precedence over the --duration or --page-progress bars: If one of these options is specified as well, the timeout progress bar will be shown on pages with a timeout, and the other progress bar will be shown on pages without one.
-l or --listtrans
If this option is specified, Impressive writes a list of all available transition classes to standard output and does not start a presentation. Transitions that are enabled by default are marked with a star (*) left of the class name.
-L <spec> or --layout <spec>
Specifies the OSD layout. Read below for an explanation of this option.
--maxzoom <factor>
The maximum zoom factor up to which Impressive shall attempt to render a high-resolution image of the page when zoomed in. This can be any real number larger than or equal to 1.0; the default is 5.0. A setting of 1.0 causes Impressive to never render a high-resolution image for zooming; in this case, the zoom and box-zoom modes will upscale the image, but not enhance any details.
Note that regardless of this setting, Impressive always checks the graphics hardware capabilities (in particular, the maximum supported texture size) and may reduce the effective value of --maxzoom if the hardware supports less than what has been specified.
--min-box-size <pixels>
This option sets the minimum size of a highlight box, in pixels. Boxes that are not this many pixels wide and high are rejected in order to prevent accidental creation of highlight boxes. The default value for this is 30 pixels.
-M or --minutes
If this option is set, Impressive will show the on-screen timer (activated with the T key) only with 1 minute resolution. By default, it will show a timer with 1 second resolution.
--noblur
By default, Impressive uses a fragment shader to blur and desaturate the image when in highlight box or spotlight mode. This is usually the faster and nicer-looking method; however, some very old hardware implementations can't deal with that shader and fall back to an unusably slow software implementation. Impressive tries to detect scenarios where this would happen and automatically falls back to a different implementation.
There might be situations where this machanism fails and Impressive tries to use the non-functional shader anyway. In these cases, the --noblur option can be used to enforce the fallback implementation.
-N or --nocursor
This option disables any display of a mouse cursor, i.e. neither the system (»hardware«) not bitmap (»software«) cursor is shown at any time. This is mainly useful for automated presentations, where no mouse interaction is needed.
--noclicks
If this option is enabled, switching to the previous or next page with the left and right mouse buttons is deactivated. The keyboard shortcuts are unaffected from this.
Note that this option only works as intended when the default controls are used. If the -e/--bind or -E/--controls options have been used, --noclicks might not have the intended effect.
--nologo
This option disables the Impressive logo and version number display. Instead, the loading screen will be just black or, if background rendering is disabled, only the progress bar will be visible.
--nooverview
This option disables the overview page. Overview thumbnails are not created, and the Tab that normally enters overview mode is ignored. (More precisely, the overview-enter action is ignored.)
--noquit
This option disables the keyboard shortcuts (Q and Esc) that could normally quit Impressive with a single keypress. If it is used, the only way to exit the program is the key combination Alt+F4, and accidental quitting is no longer possible.
Note that this option only works properly if the default controls are used. If the -e/--bind or -E/--controls options have been used, --noquit might not have the intended effect.
-o <directory> or --output <directory>
Do not display the presentation, but render it into a series of PNG image files inside the specified directory. The images will be generated in the current resolution as specified by the -g option. This option is useful if the presentation is to be given on on a foreign PC with an old, broken or otherwise problematic MuPDF/Xpdf/GhostScript installation: By generating images of the PDF pages, is is made sure that no rendering bugs will happen on the target system.
-O <mode> or --autooverview <mode>
Enables or disables automatic derivation of whether a page shall or shall not be shown on the overview page. This feature is based on the fact that some LaTeX presentation packages tag all pages with a title (that can be read by Impressive with the help of pdftk), except those that contain multiple reveal steps.
The following modes are available:
off
Disables automatic overview mode. All pages will be shown on the overview page by default. This is also the default setting.
first
All pages with a PDF title will be shown on the overview page. The purpose is to show the initial state of multi-step slides on the overview page.
last
All pages before a page with a PDF title will be shown on the overview page. The purpose is to show the final state of multi-step slides on the overview page.
Again, the mode may be abbreviated arbitrarily, down to one character, just like with the -c option above..
-p <start>-<end> or --pages <start>-<end>
Using this option, the range of the page displayed can be narrowed down. The presentation will start at the first page in the range. All pages outside of the range will not be shown on the overview page and will not be cached. However, they can be entered manually when cycling through the presentation. Due to the fact that these pages are uncached, preparation of the display will take considerably longer.
-P <path> or --gspath <path>
This option can be used to override the Xpdf / GhostScript path autodetection. The full path to the executable of either GhostScript (gs or gs.exe) or Xpdf's pdftoppm utility must be specified.
-q or --page-progress
If this option is enabled, Impressive will show a light-blue semi-transparent progress bar at the lower edge of the screen that shows the position inside the presentation, i.e. the relation between the current page number and the total number of pages. Note that this progress bar will not be shown if the duration progress bar (-d option) is also enabled.
-Q or --autoquit
If this option is specified, Impressive quits automatically when trying to navigate to the page after the last page or the page before the first page.
This option does not have any effect if --wrap is used.
-r <n> or --rotate <n>
Display all pages rotated by n×90 degrees clockwise. Try -r 1 or -r 3 if there are problems with PDFs generated by LaTeX on some Xpdf or GhostScript versions.
-s or --scale (image input only)
If a directory with image files is used as input, Impressive will scale down images that are too big for the screen. But by default, it will not scale up smaller images to fit the screen; it will leave a black border instead. This option overrides this setting and enables upscaling of smaller images.
-s or --supersample (PDF input only)
This switch enables antialiasing by 4x supersampling instead of the normal multisampling method used by Xpdf or GhostScript. While this usually degrades both visual quality and performance, it may be necessary for circumventing white strips or moiré-like patterns in gradients.
-S <pixels> or --fontsize <pixels>
This option sets the size, in pixels, of the OSD font. The default value is 14.
--spot-radius <pixels>
This option sets the initial radius of the spotlight, in pixels. The default value is 64.
--time-display
This option enables display of the elapsed presentation time, as if the T key has been pressed immediately after startup. This implies time tracking mode (--tracking option).
-t <ms> or --transition <trans1[,trans2...]>
Using this switch, the set of transitions Impressive will randomly draw at page changes can be specified. If only one transition class is specified, this class will be used for all pages that do not have another transition explicitly assigned in their page properties. Multiple transitions have to be separated by commas; they will be used in random order. The -l option can be used to get a list of available transitions.
-T <ms> or --transtime <ms>
Sets the duration (in milliseconds) of page transitions. 0 (zero) disables transitions altogether. Default value: 1000 ms.
--tracking
This option enables time tracking mode. In this mode, a report of all pages visited with their display duration, enter and leave times will be written to standard output. This can be very useful when preparing presentations.
-u <seconds> or --poll <seconds>
If this option is specified, the source file or directory will be checked for changes regularly. If a change in the input PDF file or any of the image files in the input image directory is detected, the page cache will be flushed and the current page as well as the info script will be reloaded. The current page's transition will be shown between the old and the new version of the page.
The new PDF file must have at least as much pages as the old one; also, it should have the same aspect ratio. If the input is a directory, image files must not have disappeared.
-v or --verbose
This option makes Impressive more verbose, i.e. it will print slightly more informational messages than usual.
-V <pixels> or --overscan <pixels>
PDF files often contain tiny amounts of white borders around the edges which look bad in screen presentations. To eliminate this problem, Impressive uses »overscan«: PDF files will not be rendered to fit the screen size exactly, but they will be rendered a bit larger so that the (possibly broken) borders can be cropped off. The amount of overscan, in screen pixels, can be set with this option. The default value is 3 pixels, which should remove borders in most presentations at most common screen resolutions without cropping the pages too much.
-w or --wrap
If this option is set, Impressive will “wrap” over to the first page after the last page. In other words, advancing to the next page at the end of the presentation will restart the whole presentation.
-W or --nowheel
By default, it is possible to change pages using the mouse wheel. This option disables this behavior, which can be useful to prevent spurious page changes if the mouse wheel is likely to be moved by accident. In addition, it makes it possible to enter zoom mode with the mouse wheel.
Note that this option only works properly if the default controls are used. If the -e/--bind or -E/--controls options have been used, --nowheel might not have the intended effect.
--windowed
Starts Impressive in windowed mode. See the discussion of the -f option for details.
-x or --fade
This option enables a smooth fade-in effect at the start of the presentation and a fade-out effect just before Impressive quits.
-X or --shuffle
If this option is enabled, the input files will be shuffled into random order before starting the presentation. The individual pages of PDF input files will stay in their original order, though, so this option is mainly useful for image presentations.
-y or --auto-auto
This option can be used together with --duration to have Impressive compute a page timeout (as with the --auto option) automatically. This results in a presentation that runs automatically, displaying each slide for the same time, so that the desired total duration will be reached (almost) exactly. One exception is the runtime of videos, which is not taken into account.
--zbox-edge <pixels>
Sets the border size (in pixels when unzoomed) of the box-zoom boxes' smooth edges. The default value is 1 pixel; it can be set to zero, in which case the box-zoom area will have a hard edge.
-z <factor> or --zoom <factor>
Sets the scaling factor that is used in zoom mode. It can be any real value above 1.0; the default value is 2.0. Note that it might not be possible to get high-quality zooming for large zoom factors due to hardware restrictions. Furthermore, please be aware that after leaving zoom mode, the image quality of the normal page display may be slightly degraded until another page is displayed if a zoom factor other than 2.0 is used.
--zoomdarkness <percentage>
Specifies how much the non-selected parts of the screen shall become darker in box-zoom mode. The value is specified in percent, with 90 being the default. A value of zero would mean no darkening at all (parts outside of the zoom box would still be perfectly visible), and a value of 100 would make everything but the selected region black.
-Z <ms> or --zoomtime <ms>
Sets the duration (in milliseconds) of the overview page zoom-in/zoom-out effects. Default value: 250 ms.
--cachefile <filename>
Activates persistent cache mode and specifies the name of the cache file to use.

Following the options, the input file name(s) must be specified. Recognized file types are PDF, image files (JPEG, PNG, TIFF, BMP, PGM/PPM) and video files (AVI, MOV/MP4, MKV/WebM, OGV, MPEG, M2TS, FLV). If the name of a directory is put on the command line, all recognized image files (no PDF files!) in this directory will be played in alphanumeric order (case-insensitive).

In addition, Impressive can use a text file containing a list of files or directories to show: This text file must contain at most one file name per line; every character after a hash sign (#) is treated as a comment and will be ignored. If such a comment is put on the same line as an image file name, it will be used as the page's title. List file names must be prefixed with an at sign (@) on the command line, e.g. impressive @my_list_file.

Impressive will also expand wild-card characters (* and ?) if this isn't already done by the shell, but apart from that, it will not reorder the arguments. Thus, it will show the documents in the order specified at the command line.

If video files are specified on the command line, Impressive generates presentation pages for them just like it does for image files. If FFmpeg or MPlayer are installed, the page will be shown as a screenshot of the first video frame. When entering such a page, the video will be played back if a suitable player (MPlayer, or omxplayer on Raspberry Pi) is installed. Note that in fullscreen mode on Windows, Impressive will briefly be minimized and restored again when playback starts.

Layout options

The OSD layout option (-L/--layout) accepts a string with comma-separated key=value pairs. The following keywords are recognized:

alpha
The opacity of the OSD text, either as a floating-point value between 0 and 1 or a percentage between 2 and 100.
margin
The distance (in pixels) of the OSD text to the screen borders.
time
The position of the timer.
title
The position of the page title in overview mode.
page
The position of the page number in overview mode.
status
The position of the status line.

The position specifications are composed by one character that indicates whether the text shall be displayed at the top (T) or bottom (B) edge of the screen and one character that indicates whether it shall appear on the left (L), on the right (R) or centered (C).

For example, the default OSD layout equals the following option string:
-L margin=16,alpha=1,time=TR,title=BL,page=BR,status=TL

Examples

The following examples illustrate some typical command lines. They assume that Impressive can be run by simply typing »impressive« on the command line. Depending on how Impressive is installed, this has to be substituted with the actual way to run Impressive (e.g. »python ~/impressive/impressive_dev.py« for a fresh SVN checkout). Furthermore, the file »demo.pdf« is used as the document to show here; obviously this has to be replaced by the path to the actual PDF file too.

In the simplest case, Impressive is run directly with the name of the file to show and no further parameters. This will start a full-screen presentation with all settings at their defaults:
impressive demo.pdf

To just quickly check a slide deck, it might make sense to run Impressive in a small window and not full-screen. This can be done with something like
impressive -f -g 800x600 demo.pdf

Impressive can also be used in digital signage scenarios, like displays in shop windows with a permanent slideshow. This can be achieved by having Impressive advance to the next page automatically after a specified time (e.g. 10 seconds) and re-start the presentation from the start after the last slide:
impressive -a 10 -w demo.pdf

Usage

On startup, Impressive will display a black screen with the program logo at the center. If caching is enabled, but background rendering is disabled, all pages of the presentation will then be rendered once. A bar in the lower half of the screen displays the progress of this operation. Any key or mouse click (except for those that quit Impressive, typically Q and Esc) skips this process, with the effect that Impressive will render the remaining pages on demand. Please note that the overview page will not be complete until every page has been rendered at least once. In other words, if the precaching process was skipped, placeholders will be displayed for all pages that have not been rendered yet. By default, Impressive will build up the cache in the background while the presentation runs. Thus, the progress bar will not appear and the preparation will only take the amount of time required to render the first two pages of the presentation. After this initialization process, Impressive will switch to the first page directly and without any transition.

The keyboard and mouse controls used by Impressive are configurable (with very few exceptions). The default controls are as follows:

Esc key
Return from the currently active special mode (zoom, box-zoom, overview, spotlight, highlight boxes); if no such special mode is active, quit Impressive altogether.
Q key or Alt+F4
Quit Impressive immediately.
LMB (left mouse button), mouse wheel down, Page Down key, Cursor Down key, Cursor Right key or Spacebar
Go to the next page (using a transition).
RMB (right mouse button), mouse wheel up, Page Up key, Cursor Up key, Cursor Left key or Backspace key
Go to the previous page (using a transition).
Home key / End key
Go directly to the first or last page of the presentation.
Ctrl key (modifier)
If one of the page navigation keys (Page Up/Down, Cursor keys, Space, Backspace, Home, End) is pressed while the Ctrl key is held down, the destination page will be entered immediately, without a transition.
L key
Return to the last (most recently displayed) page. This can be used to toggle back and forth between two pages.
F key
Toggle fullscreen mode. Note that this does not work on Windows systems.
Tab key
Zoom back to the overview page. While in overview mode, a page can be selected with the mouse and activated with the left mouse button. The right or middle mouse button or the Tab key leave overview mode without changing the current page.
MMB (middle mouse button)
In normal display mode, this acts like the Tab key: it zooms back to the overview page. If the page is zoomed in, it will return to normal mode.
LMB over a PDF hyperlink
Jump to the page referenced by the hyperlink. Two types of hyperlinks are supported: Links that point to some other page of the same document, and URL hyperlinks like Web links and e-mail addresses. This feature is only available if pdftk is installed. Furthermore, xdg-open from the freedesktop.org Portland project is required for URL links on non-Win32 platforms. Please note that the hyperlink feature will not work properly when pages are rotated.
click&drag with LMB (left mouse button)
Create a new highlight box. While at least one highlight box is defined on the current page, the page itself will be shown in a darker, blurry and (if supported by the graphics hardware) desaturated rendition. Only the highlight boxes will be displayed in their original lightness, sharpness and color saturation.
If a page with highlight boxes is left, the boxes will be saved and restored the next time this page is shown again.
click&drag with Ctrl+LMB (left mouse button)
Draw a box to zoom into. This enters so-called »box-zoom« mode, where the selected region is zoomed into full view and anything else is displayed in much darker colors (but not blurred like in highlight box mode). The page contents will be re-rendered in a higher resolution, if possible (limited to what has been configured with the --maxzoom command-line option, and the graphics hardware's maximum supported texture size). To leave this mode again, use RMB, MMB or Esc.
Note that box-zoom mode is mutually exclusive with highlight boxes and the spotlight; if any of those is currently active, it's not possible to draw a zoom box.
RMB (right mouse button) over a highlight box
If the right mouse button is clicked while the mouse cursor is above a highlight box, the box will be removed. If the last box on a page is removed, the page will turn bright and sharp again.
S key
Save the info script associated with the current presentation. The main purpose for this is to permanently save highlight boxes or keyboard shortcuts, so they will be restored the next time this presentation is started.
T key
Activate or deactivate the time display at the upper-right corner of the screen. If the timer is activated while the very first page of the presentation is shown, it activates time tracking mode, just as if the command-line option --tracking had been specified.
R key
Reset the presentation timer.
C key
Removes (»clears«) all highlight boxes from the current page.
Return key or Enter key
Toggle spotlight mode. In this mode, the page is darkened in the same way as if highlight boxes are present, but instead of (or in addition to) the boxes, a circular “spotlight” will be shown around the mouse cursor position, following every motion of the mouse cursor.
+ / key, 9 / 0 key or mouse wheel in spotlight mode
Adjust the spotlight radius.
Ctrl+9 or Ctrl+0 keys
Resets the spotlight radius to the default value, i.e. the value that has been set up by the radius page property, the --spot-radius command-line option or Impressive's built-in default.
7 / 8 key
Adjust the amount of darkening applied to the page in spotlight, highlight box or box-zoom mode.
Ctrl+7 or Ctrl+8 keys
Resets the amount of darkening in spotlight, highlight box and box-zoom mode to the default values, i.e. the value that have been set up by the darkness and zoomdarkness page properties, the --darkness and --zoomdarkness command-line options or Impressive's built-in defaults.
Z key
Toggle zoom mode. When this key is first pressed, the current page will zoom in. The page will be displayed at double size, but in its original resolution (i.e. it will be blurry). Impressive will re-render the page at the new resolution if the graphics hardware supports it. During this time, Impressive will not accept any input, so don't even think about clicking the mouse or pressing keys before the image gets crisp again.
In zoom mode, all other functions will work as normal. Any operations that leave the current page, such as flipping the page or entering the overview screen, will leave zoom mode, too.
[ / ] key
Adjust the gamma value of the display (might not be supported on every hardware).
{ / } key
Adjust the black level of the display (might not be supported on every hardware).
\ key
Revert gamma and black level back to normal.
O key
This will toggle the »visible on overview page« flag of the current page. The result will not be visible immediately, but it can be saved to the info script (using the S key) and will be in effect the next time the presentation is started.
I key
This will toggle the skip flag of the current page. A page marked as skipped will not be reachable with the normal forward/backward navigation keys.
B / W key or . (dot) / , (comma) key
Fade to black or white, respectively. This feature can be used if a whiteboard or blackboard in front of the projection area is to be used during the presentation. Using these two keys, the screen will fade into a solid color. On any keypress or mouse click, it will fade back to normal. These keys are not available in overview mode.
click&drag with MMB (middle mouse button) or RMB (right mouse button) in zoom mode
Move the visible part of the page in zoom mode.
mouse wheel up / down in zoom mode, U key
Zoom a bit in or out. When zoomed out fully, zoom mode is left; note that in the default configuration, this means that when trying to zoom out even more using the wheel, the page is left instead. This is different if the -W/--nowheel option is used, where page navigation using the mouse wheel is disabled, and the wheel is always used for zooming. In this mode, the wheel can also be used to enter zoom mode in the first place.
Note that changing the zoom level on the fly with the wheel does not make Impressive automatically re-render the page at a higher resolution to increase the level of detail. To do this, the U key needs to be pressed manually when the desired zoom level has been set up.
Cursor keys in overview mode
Navigate through pages.
Alt+Tab keys
If Impressive is in fullscreen mode, the window will be minimized so that other applications can be used.

Any alphanumeric (A-z, 0-9) or function key (F1-F12) that is not bound to a specific action mentioned above or configured by the user (see below) can be used to assign shortcuts to pages that require quick access. If one of the keys is pressed together with Shift, the currently displayed page is associated with this key. To recall the page later, it is sufficient to press the shortcut key again. Shortcuts can be stored permanently with the S key.

Control Configuration

As already mentioned in the previous chapter, the keyboard and mouse bindings of Impressive can be widely configured. The only exceptions are the Alt+F4 and Alt+Tab key combinations that will always quit or minimize Impressive, respectively. For everything else, there is a versatile configuration system in place; the controls described in the previous section are merely the defaults.

Impressive's control system works by associating events with actions. An event is a key on the keyboard, a mouse button or a mouse wheel movement. An action is something that is performed by Impressive as a result of an event, like going to the next page, switching to overview mode or quitting the program. The association of an event to an an action is called a binding. Multiple events can be bound to the same action (like the page down and space keys in the default setting, both of which go to the next page); furthermore, multiple actions can be bound to the same event. In fact, bindings do not associate events with single actions at all, but with chains of actions. Only the first action in the chain that matches (i.e. makes sense in) the current context will be executed if the event fires; all other actions will be ignored. If no action matches, no action will be performed and the event will be ignored.

One example of such an action chain is the default binding for the left mouse button, which

  • draws a highlight box if the mouse cursor moved, or
  • visits a hyperlink if the mouse cursor hovers over one, or
  • goes to the next page if none of the other conditions are met.

Both events and actions have mnemonic names that are used in the command-line options and configuration files used for setting up bindings. Event and actions names are generally case-insensitive, though the canonical notation is lowercase.
If an event or action specified on the command line or in a configuration file is not recognized by Impressive, an error message will be written to the console and the offending event or action will be ignored. Errors in control configuration are thus always non-fatal.

Supported Events

Keyboard events are generally named after the keys they refer to. Consequently, the events a to z and 0 to 9 mean the respective letter and number keys on the main keyboard, f1 to f12 are the function keys and kp0 to kp9 are the number keys on the numerical keypad. All of these are raw scancodes, which has two implications: First, the key names are not internationalized and refer to the US keyboard layout (e.g. the Z key on a German or French keyboard will actually react to the event name y or w); second, modifiers will be ignored as well (e.g. the numerical keypad will always generate the kpX scancodes, even if Num Lock is off).

The mnemonic names for the other keyboard events are as follows (in alphabetic order): ampersand, asterisk, at, backquote, backslash, backspace, break, capslock, caret, clear, comma, down, end, escape, euro, exclaim, greater, hash, help, home, insert, kp_divide, kp_enter, kp_equals, kp_minus, kp_multiply, kp_plus, lalt, last, lctrl, left, leftbracket, leftparen, less, lmeta, lshift, lsuper, menu, minus, mode, numlock, pagedown, pageup, pause, period, plus, power, print, question, quote, quotedbl, ralt, rctrl, return, right, rightbracket, rightparen, rmeta, rshift, rsuper, scrollock, semicolon, slash, space, sysreq, tab, underscore, up. The events prefixed with kp_ refer to keys on the numerical keypad. Other than that, the names should be reasonably descriptive, so they will not be described futher at this point. Also note that not all keyboards and platforms support the full range of keys defined in this list.

Mouse event names are mapped as follows:

lmb
the left mouse button
mmb
the middle mouse button
rmb
the right mouse button
wheelup
scrolling the mouse wheel upwards
wheeldown
scrolling the mouse wheel downwards
btnX (i.e. btn6, btn7, ...)
additional mouse buttons, the mapping of which is device-specific; use --evtest to see which button is which

The event names can be prefixed with the three modifiers ctrl+, alt+ and shift+ to make the event valid only if the specified set of modifiers is pressed as well. This works for both keyboard and mouse events. Multiple modifiers can be combined, but the order must match the one mentioned in this paragraph. For example, ctrl+shift+x is a valid event name, while shift+ctrl+x is not.

A simple way to determine the name associated with an event is using Impressive's »Event Test Mode« by invoking impressive --evtest. In this mode, the name of each incoming event will be displayed on the screen (and logged to standard output), which makes it possible to determine event names by experimentation.

Supported Actions

The following list describes all actions supported by Impressive, together with the conditions under which they will match. Note that most actions will not match in overview mode and during video playback, unless mentioned otherwise in the description.

box-add
Draw a highlight box if the mouse has been moved since the button has been pressed down. This action must only be bound to a mouse button event, otherwise it will not function properly.
box-clear
Removes all boxes from the current page.
box-remove
Removes the highlight box under the mouse cursor, if there is any.
box-zoom
Define a region to zoom into by drawing a rectangle. This action must only be bound to a mouse button event, otherwise it will not function properly.
box-zoom-exit
If in box-zoom (but not normal zoom) mode, leave box-zoom mode.
fade-less, fade-more
Decrease or increase the amount of darkening applied to the background in spotlight or highlight box mode.
fade-reset
Resets the background darkness in spotlight or highlight box mode to its default value.
fade-to-black, fade-to-white
Fades to a black or white screen. Once the screen is faded out, any event except those bound to the quit action will just leave fade mode and not perform its assigned action.
fullscreen
Toggle fullscreen mode on platforms that support it.
gamma-decrease, gamma-increase
Decrease or increase the gamma level (i.e. roughly the brightness) of the display on platforms that support it.
gamma-bl-decrease, gamma-bl-increase
Decrease or increase the black level of the display on platforms that support it.
gamma-reset
Reset the gamma and black level settings to their defaults.
goto-end, goto-end-notrans
Go to the last page of the presentation, with or without a transition.
goto-last, goto-last-notrans
Go to the last (i.e. most recently) visited page, with or without a transition.
goto-next, goto-next-notrans
Go to the following page of the presentation, with or without a transition.
goto-prev, goto-prev-notrans
Go to the previous page of the presentation, with or without a transition.
goto-start, goto-start-notrans
Go to the first page of the presentation, with or without a transition.
hyperlink, hyperlink-notrans
Navigate to the hyperlink under the mouse cursor, if there is one. If the hyperlink is a reference to another page of the presentation, this page will be activated with or without a transition. If the hyperlink refers to an external object (e.g. an URL), it will be opened externally, if supported by the system.
overview-confirm
When in overview mode, confirm the selection and leave overview mode, navigating to the selected page.
overview-down, overview-up
When in overview mode, select the page above or below the currently selected one in the grid.
overview-enter
When not in overview mode, zoom out of the current page, entering overview mode.
overview-exit
When in overview mode, leave overview mode, zooming back to the page that has been displayed before entering overview mode.
overview-next, overview-prev
When in overview mode, select the following or previous page.
quit
Quits Impressive immediately. This action is available in all modes.
save
Save or update the Info Script for the current presentation.
spotlight-enter
If spotlight mode is not active, enable spotlight mode.
spotlight-exit
If spotlight mode is active, deactivate spotlight mode.
spotlight-grow, spotlight-shrink
When in spotlight mode, increase or decrease the radius of the spotlight.
spotlight-reset
When in spotlight mode, reset the spotlight radius to the default value.
time-reset
Reset the presentation timer.
time-toggle
Toggle on-screen display of the current presentation time, or wall-clock time if the --clock option is used. If this is done at the start of the presentation, before the first page has been left, time tracking mode will be enabled, like the --tracking option would have done.
toggle-overview
This toggles the »page is visible on overview screen« flag for the current page. This will not have an immediate effect, but it can be saved to an Info Script.
toggle-skip
This toggles the »skip page when navigating with goto-prev and goto-next« flag for the current page.
video-pause
In video playback mode, this pauses or unpauses playback.
video-seek-backward-10, video-seek-backward-1, video-seek-forward-1, video-seek-forward-10
In video playback mode, seek forward or backward by 1 or 10 seconds.
video-step
In video playback mode, if playback is paused, advance one frame in the video.
video-stop
In video playback mode, stop playback and return to normal page display mode.
zoom-enter
If not in zoom mode, enter zoom mode with the zoom level configured with the -z/--zoom option.
zoom-exit
If in zoom or box-zoom mode, leave that mode.
zoom-in, zoom-out
Slightly increase or decrease the current zoom level. This does not change the page's rendering resolution, it only zooms the visible part of the screen in or out, centered around the mouse cursor's position. Zoom mode is left automatically if the zoom level goes to down to 1x; conversely, zoom mode is entered when calling zoom-in while zoom mode is not active.
zoom-pan
If in zoom mode, the visible area of the page can be moved around with the mouse while the key or mouse button of the associated event is held down.
zoom-update
If in zoom mode, re-render the current page in a resolution that best fits the current zoom level, up to the limits of the graphics hardware or the --maxzoom setting. This only needs to be done explicitly when zooming with the zoom-in action; zoom-enter and box-zoom do this automatically.

Binding Syntax

The arguments of the -e/--bind command-line option have the following basic syntax:
    <event> [,<event2>...] <operator> <action> [,<action2>...]
In other words, it is a sequence of event names joined with commas, followed by an operator (see below) and a sequence of action names joined with commas. Multiple such binding statements can be combined into one argument by joining them with a semicolon (;).

The used operator defines in which way the action list shall modify the bindings of the referenced events:

= (equals sign), += (plus sign and equals sign)
The specified actions will be added to the bindings of the specified events. In other words, event=action1,action2 does exactly the same as event=action1; event=action2.
:= (colon and equals sign)
The specified actions will replace the bindings of the specified events.
-= (minus sign and equals sign)
The specified actions will be removed from the bindings of the specified events. For example, to make the Esc key in the default bindings not clear highlight boxes, but otherwise preserve its original functionality, escape -= box-clear can be used.

Other than bindings, a statement can also contain one of the following special commands:

clearall
Clears all current bindings.
defaults
Discards all current bindings and (re-)establishes the default bindings.
include <filename>
Loads and executes a control configuration file with a specified name.

The syntax for the configuration files used with the -E/--controls option or include statement is exactly the same as for the ad-hoc configuration option, except that individual bindings can be written on individual lines instead of joining them together to a single long line with semicolons. In addition, everything following a hash sign (#) on a line will be ignored as a comment.

One practical example for such a configuration file can be the following: The author of this program uses a cheap presentation remote control device that has four cursor keys, one »enter« key and a slider that switches between keyboard and mouse mode. Mouse mode works as expected, but what it does in keyboard mode is quite peculiar: the up and down keys act like Page-Up and Page-Down keys on a keyboard, the right arrow key sends the letter B to the computer, and the left key toggles between Esc and F5 each time it's pressed. The following configuration file allows basic navigation and access to overview mode with this device:

    clearall  # don't use the default bindings
    lmb = quit  # quit Impressive by clicking in mouse mode
    # everything else uses keyboard mode:
    return = overview-enter, overview-confirm  # toggle overview mode
    escape, f5 = overview-prev, goto-prev
    b = overview-next, goto-next
    pageup = overview-up, goto-prev
    pagedown = overview-down, goto-next

To get a better idea of how the control configuration system works in practice, it's recommended to study the output of impressive --control-help – this not only gives a concise overview of all events and actions, but also a full dump of Impressive's default bindings that can be used as a starting point for own customizations.

Info Scripts

Impressive offers a powerful way to customize individual presentations using so-called info scripts. An info script is a text file having the same name and located in the same directory as the presentation file itself, except for the additional suffix .info. Thus, a presentation file called BrandNewProduct.pdf would have a info script with the name BrandNewProduct.pdf.info. If multiple arguments were specified on the command line, the info script will be called just .info (a dot file, so to speak). If a directory name was specified as the only argument, either a file called DirectoryName.info or a file called .info inside the directory will be used, depending on whether a path separator was specified at the end of the directory name or not – Impressive simply appends .info to whatever the input path name was.
In any case, the default file name can be overridden by the -I command line option.

Info scripts are actually Python scripts with full access to Impressive's global data structures and functions. (It is possible to write real interactive applications using info scripts.) Thus, they can modify a broad range of settings concerning Impressive. This manual will only cover the most basic ones.

Page Properties

The main part of an info script defines the properties of each page. At the moment, the following properties are defined:

title
Each page can have a title that is displayed in the Impressive window's title bar. If there is no title explicitly specified in the info script, the title of the page will be extracted from PDF metadata if pdftk is installed, or the image file name will be used if the presentation is an image slideshow.
transition
With this property, the transition class to be used for rendering the transition to this page (i.e. between the previous page and this page) can be specified. For pages lacking this property, random transitions will be chosen. A list of available transition classes can be obtained with impressive -l.
transtime
This property overrides the global transition time parameter (-T at the command line). It contains the integer time (in milliseconds) the transition to this page shall take.
overview
This property holds a boolean value (0/False or 1/True) that specifies whether the page shall be included in the overview page. If this property isn't specified, it is assumed to be True.
skip
This boolean property can be set to 1/True if the page shall be skipped during the presentation.
Pages with overview:True, skip:False will be accessible both by cycling through the pages and using the overview page,
pages with overview:True, skip:True will be silently skipped in the normal page cycle, but remain accessible from the overview page,
pages with overview:False, skip:False will appear in the normal cycle, but not on the overview page
and pages with overview:False, skip:True will not be accessible at all.
boxes
This property stores a list of highlight box coordinates. Normally, there is no need to edit this by hand, as Impressive handles this automatically if the S key is pressed.
timeout
If a timeout property is present and the page is shown, Impressive will automatically switch to the next page after the specified number of milliseconds. Normally, the timeout will only be effective the first time the page is shown unless wrap mode is used (command-line option -w or --wrap). This restriction makes it possible to create self-running presentations with individual per-page timeouts.
radius
This property takes an integer value that, if defined, will be used to set a new spotlight radius every time the page is entered. This overrides the current setting as defined by the --spot-radius command line option or run-time adjustments. Note that the value is not reset to the default value after the page has been left again.
darkness
This property takes an integer or floating-point percentage value that, if defined, will be used to set the background darkness in spotlight or highlight box mode each time the page is entered. This overrides the current setting as defined by the --darkness command line option or run-time adjustments. Note that the value is not reset to the default value after the page has been left again.
zoomdarkness
This property takes an integer or floating-point percentage value that, if defined, will be used to set the background darkness in box-zoom mode each time the page is entered. This overrides the current setting as defined by the --zoomdarkness command line option or run-time adjustments. Note that the value is not reset to the default value after the page has been left again.
comment
This property can hold a string with a single line of text that will be displayed on screen while the page is shown. Display of this text can not be disabled.
sound
Specifies the file name of a sound file to be played (via MPlayer) when the page is first entered. Once started, the sound will be played until its end is reached, another sound or video is played, or Impressive is exited.
video
Specifies the file name of a video file to be played when the page is first entered. The video will be displayed full-screen. Any key or mouse click stops playback, except the cursor keys, which are used to seek in the video file, and space, which can be used to pause playback.
Please not that this function is still somewhat experimental. It uses an external player application (MPlayer, or omxplayer on Raspberry Pi) to perform the actual playback, and embeds it into Impressive's window. On Windows systems, Impressive will also be minimized briefly and restored again when a video starts in fullscreen mode.
always
If this property is present and set to 1 or True, the media file specified in the sound or video properties will be played every time the page is entered, not just the first time.
progress
If this property is set to zero, the presentation progress bar (which is usually set up with the -d/--duration command line switch) will not be shown on this page. In practice, it might be useful to hide the bar from the first page so that it is not visible during the introduction.
reset
If this property is set to 1 or True, the timer will be reset each time this page is left, just as if the R has been pressed. If the special value 2 or FirstTimeOnly is used, the reset will only take place if the page was shown for the first time. Again, this is particularly useful on the first page: A combination of progress:False, reset:FirstTimeOnly makes it possible to set up the presentation long before it actually begins – the first page can be showed as long as desired, actual timing starts at the second page.
rotate
This property is a per-page override of the global -r command line option: It specifies how the page shall be rotated, in 90-degree steps clockwise.
OnEnter, OnLeave, OnEnterOnce, OnLeaveOnce
These properties can contain a Python callable (i.e. a function reference or lambda expression) that is executed when the page is entered or left. The ~Once variants will only be executed when the page is entered or left for the first time. The callable must not take any arguments. This feature can be used to associate arbitrary Python code with specific pages, for example to execute external programs.
Warning: Lambda expressions cannot be properly processed by the Info Script save function (S key). If Impressive encounters lambda expressions when saving, it will remove them. In addition, it will not overwrite the original info script, but generate an extra file that needs to be merged withe the original script by hand.
keys
This property can be assigned a dictionary that maps alphanumerical keys to Python functions. For example, 'keys': { 'x': SomeFunction } will invoke SomeFunction() if the lowercase character 'x' is typed while the page is displayed. Regarding the functions, the same restrictions as for the OnEnter/OnLeave family apply: the function must not take any parameters and lambda functions can not be saved. Also note that it is not possible to overwrite Impressive's pre-defined key bindings with this method.
invert
This property specifies whether the colors of that page shall be inverted in the same way the --invert command-line switch does. It overrides the --invert setting on a per-page basis: If set to True, the page will always be inverted; if set to False, the page will never be inverted even if --invert has been specified on the command line.

Note that in Impressive versions prior to 0.11.0, the transition and transtime properties defined the transition from the current page to the next, not from the previous page to the current one.

The properties are stored together in a Python dictionary called PageProps. The syntax looks like in this example:

PageProps = {
  1: {
       'title': "Title Page",
       'transition': PagePeel,
       'sound': "background_music.mp3"
     },
  2: {
       'title': "Another Page",
       'timeout': 5000
     }
}

The PageProps definition (and only the PageProps definition) will be rewritten by Impressive if the S key is pressed. User-defined PageProps entries will also be left untouched, except for some pretty-printing.

Global Presentation Properties

The name of the presentation is shown in the title bar of the Impressive window (if not in fullscreen mode). By default, the file name or (if available) PDF metadata title will be used for this purpose, but the presentation title can also be explicitly set by overwriting the DocumentTitle variable:
DocumentTitle = "My Presentation"

Another useful variable, AvailableTransitions, contains a list of all transition classes that may be used for randomly assigning transitions to pages lacking the transition property. Thus, if a certain transition is undesired (either because of personal dislike or because it shall be used exclusively on pages where it is manually assigned using PageProps), something like the following can be written:
AvailableTransitions.remove(WipeBlobs)
On the other side, it's possible to activate transitions that are not enabled by default:
AvailableTransitions += [SlideUp, SlideDown]
Alternatively, AvailableTransitions can be completely overwritten to have the same transition (or set of transitions) assigned to all pages:
AvailableTransitions = [Crossfade]

Option Overrides

Another use of info scripts is overriding the default or command-line settings on a per-file basis. This can be done by simply overwriting one of the variables that are defined at the beginning of impressive.py. Each of these variables corresponds either to a command-line setting, or to some constant related to visual appearance or performance. So, for example, to force fullscreen mode for a presentation, write
Fullscreen = True

Working Directories

The working directory while executing the info scripts themselves is always the directory in which the info script is stored in.

The base directory for external actions that originate from Page Properties or PDF hyperlinks is always the directory of the PDF or image file this page belongs to. In other words, if e.g. 'sound': "music.mp3" is written in the info script for one page of presentation.pdf, the file music.mp3 is expected to be located in the same directory as presentation.pdf.

Other Useful Information

Useful Third-Party Tools and Distributions

Joel Berger has written a Perl script that extracts transition information from special comments in LaTeX beamer source files and generates a Impressive info script from the gathered information: makebeamerinfo.

Juan Carlos Paco has written a very simple graphical interface for Impressive that runs on Debian and Ubuntu systems, called GUImpressive.

Another GUI comes from B. Pratibha, called »BOSS Presentation Tool«.

License

Impressive is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for any particular purpose. See the GNU General Public License for more details.

Contact

You can always contact the author of Impressive to send him ...

  • bug reports
  • feature suggestions
  • fan mail
  • corrections regarding the web page (I'm not a native speaker – any grammatical, ortographical or stylistic corrections or suggestions are welcome!)
  • new silly quotations for the web page's title bar :)
Impressive-0.12.0/changelog.txt0000644000175000017500000003733613235666653015065 0ustar mfiemfie0.12.0 [2018-02-04] - many zoom mode related changes - box-zoom mode: use Ctrl+LMB-drag to draw a box and zoom it to fit the screen; everything else is darkened - support for fractional zoom levels - ability to zoom further in than the hardware's maximum texture size (without additional sharpness though) - maximum zoom detail re-rendering level now limited to hardware capabilities or --maxzoom option - mouse wheel zooming - new effect of --nowheel option: can now enter zoom mode with mouse wheel - zoom mode panning now also with MMB (like in GIMP, Inkscape etc.) - many video related changes - allow videos in the file list (in addition to PDF files and images) - made --auto not interrupt videos - using omxplayer instead of MPlayer on Raspberry Pi - Win32 build now ships with a much less outdated version of MPlayer - changed MPlayer platform options - added workaround for Win32 MPlayer fullscreen issue that hid the Impressive window every time a video was played - made Impressive suitable for Raspberry Pi-based digital signage: - added boot_impressive.sh script (SVN only) that replaces init - added --bare option: disables any features that require temporary files - added --nooverview option: disables the overview page - added --nocursor option: disables any type of mouse cursor - made additional buttons of multi-button mice accessible as btn6, btn7, etc. - added options --box-edge, --zbox-edge and --zoomdarkess to configure highlight box / spotlight / zoom box appearance - added --time-display option: start with time display and tracking - some Makefile improvements: install and uninstall targets, PREFIX parameter 0.11.3 [2018-02-04] - fixed --auto mode that was broken in 0.11.2 (only worked properly for the first page) - fixed excessive mouse sensitivity in full-screen mode - fixed crashes when clicking hyperlinks in some PDFs - fixed non-working hyperlinks when more than one input document is specified 0.11.2 [2017-12-19] - added new MuPDF renderer, uses no temporary files or named pipes on POSIX with MuPDF >= 1.4 - can now use MuPDF's mutool instead of pdftk to get basic file information and unpack PDF files (for hyperlink decoding) - pdftk is no longer required to open "modern" PDF files - only page title extraction still needs pdftk - added --noquit option to prevent accidental single-key quitting - input files are now checked for playability (to prevent users from accidentally playing .info files) - fixed non-responsiveness to any controls on slow systems where rendering a page takes longer than the --auto interval - when cancelling a transition using a key or mouse click, the key press or click is no longer discarded - recommended to use a modifier key (Ctrl, Alt, or Shift) to cancel transitions without "side effects" - first page is now shown as early as possible (before fonts etc. have been preloaded) - fixed occasional re-rendering of pages that have already been rendered - fixed 'keys' PageProp that was broken since 0.11.0 - fixed missing OSD fonts in Win32 - fixed path detection of helper programs in Win32 - fixed Raspberry Pi compability (some libraries have been renamed) - fixed crash in --verbose mode - changed shebang line to 'python2' to avoid issues with Linux distributions that use Python 3.x by default - fixed --overscan option - fixed arrows in manpage - added section about Raspberry Pi to manual - Win32 build now uses Python 2.7 and PyInstaller 3.x and uses MuPDF (mutool) instead of Xpdf (pdftoppm) 0.11.1 [2015-11-12] - fix for newer PIL/Pillow versions that broke compatibility by removing .tostring() and .fromstring() - fixed crash when clicking on run: URI hyperlinks - PDF hyperlinks with file:// and run:// URIs now work correctly with quoted characters (e.g. spaces) in paths - added top-level synchronize() function to schedule calls in Impressive's main thead (useful for library mode) - explicitly printing traceback on crashes to ensure proper display on Win32 systems - early lookup of OpenGL vendor/renderer/version strings (to get more useful crash logs) - added Win32-specific OpenGL loader (instead of SDL fallback) - fixed SDL library lookup issue on newer Linux systems - OSD font lookup now works if the same font file exists multiple times in the font path - fixed crash when no OSD font is available 0.11.0 [2014-12-21] - full port from OpenGL 1.1 to OpenGL (ES) 2.0 - fully customizable key/mouse bindings (-e/--bind, -E/--controls, --control-help, --evtest) - MuPDF (mudraw/pdfdraw) rendering backend - 'transition' PageProp describes transition *to* the page, not after the page - Raspberry Pi compatibility - new shader-based blurring with desaturation + fallback to old method for old HW (autodetect or --noblur) - re-implemented all transitions with shaders (missing: PageTurn, SpinOutIn, SpiralOutIn, ZoomOutIn; new: WipeClouds, WipeBrightness1, WipeBrightness2) - compressed in-memory cache (-cz / --cache compressed) - removed -e/--noext and -R/--meshres due to obsolescence - --tracking enables time tracking right from the start - added -v/--verbose mode - MuPDF renderer backend can work via named pipes instead of temp files on Unix - alternate overview downscaling algorithm for slower systems (auto-detected) - internal default mouse cursor image - fixed OSD font rendering bug with Pillow - most platform-specific code now handled by abstraction layer; no direct PyGame calls in core - custom ctypes-based OpenGL loader instead of PyOpenGL - generic PDF renderer framework (necessary now that there's 3 renderers) - external PDF renderers are now called with 'nice' when background rendering on POSIX 0.10.5 [2014-05-02] - fixed bug that cause file lists ("@list.txt" parameters) to not work in 0.10.4 - some minor additions to the release script, only relevant for distributors 0.10.4 [2013-12-29] - changed Esc key behavior: doesn't quit immediately, leaves special modes first - middle mouse button in zoom mode now returns to normal mode (not overview) - mouse wheel can now be used to change pages (can be turned off with --nowheel) - info scripts now run from their (or the document's) directory - added 'C' key: clear all highlight boxes on current page - added --autoquit: quit Impressive at the end of the presentation - added --noclicks: don't change pages with mouse clicks - added --clock: display wall-clock time instead of elapsed time - added --half-screen: support for LaTeX-beamer's "show notes on second screen" - added --invert and 'invert' PageProp: RGB-invert pages - added --auto-auto: determine page timeout automatically from target duration - added --auto-progress: per-page timeout progress bar - added command-line option (--spot-radius), PageProp ('radius'), keyboard shortcuts ('9'/'0'/'-'/'+') and mouse wheel controls to set spotlight radius - added command-line option (--darkness), PageProp ('darkness') and keyboard shortcuts ('7'/'8') to set spotlight / highlight box background darkness - added "fake fullscreen" (borderless window) mode - window positioning is now possible with the --geometry option - Alt+Tab always minimizes the application when in fullscreen mode - Ctrl+PgUp/PgDn (and some other) key combo switches pages without transition - 'video' PageProp now also accepts a list of multiple files to be played - added support for Launch actions and file: and run: URIs in PDF hyperlinks - minimum highlight box size is now configurable, and measured in pixels - overscan is now cropped before adding letterbox/pillarbox borders - added more system information to the crash reporter - last-to-first-page transitions in wrap mode are no longer reversed - option parser accepts resolutions of up to 32Kx32K now - fixed an issue with zooming in presentations with varying page sizes - fixed compatibility with pdftk 1.45 and the PIL fork "Pillow" - fixed behavior of .info file name generation (didn't match documentation) - crash fixes with zero times in animations (e.g. --boxfade) - fixed crash with very many pages on very small screens - fixed info script error handling (could accidentally break PageProps) - many fixes in the PDF parser, manpage generator, Makefile and build system - Win32: fixed video support - Win32: added "press ENTER to quit" wait phase to crash handler - Win32: external tools (pdftk, pdftoppm, mplayer) are now searched in PATH - Win32: binary build system switched from py2exe to python-installer - Win32: added icon and version info block to binary build - minimized PyGame initialization: only display is initialized, no audio - added a simple "smoke test" to Makefile (based on Xvfb) - various other fixes and dead code removal 0.10.3 [2010-10-17] - fixed display problems when using odd window sizes - loading image data earlier so that no unhandled exceptions will be thrown when a file is broken - fixed typo in --aspect parameter - using -u doesn't periodically re-load the input file any longer if it changed only once - fixed a bunch of problems related to non-standard aspect ratios - adjustable zoom factor - black-out and white-out are now assigned to keys [.] and [,] - PDF hyperlinks are now also available if the cache is disabled - releasing all locks at the end of run_main() - included some library version numbers in crash messages - added option to fade in at program start and fade out at program end - added --nologo option - added shuffle feature - right mouse button will no longer switch to previous page in zoom mode (prevents accidental page switches) - improved MPlayer interface - added 'always' PageProp to play sound or video every time a page is entered - 'timeout' PageProp is now respected every time a page is entered if wrap mode (-w) is enabled - added finer-grained control over the progress bar colors and timing; added --page-progress option - added overscan option - display will now be quickly uninitialized when Impressive quits - improved PDF parser robustness - now using hashlib instead of md5 module, if available, to avoid a DeprecationWarning on Python 2.6 - software rendering warning now also reacts on any OpenGL renderer string beginning with 'software' - added FadeOutFadeIn transition (idea by Antonio Terceiro) - hyperlinks now work correctly on rotated pages 0.10.2 [2008-02-03] - --listtrans now shows which transitions are enabled by default - number of digits in pdftoppm output file names is cached - using xrandr to get current resolution on X11 - enabled URL hyperlinks using ShellExecute (Win32) or fd.o's xdg-open (UNIX) - 'keys' PageProp for arbitrary key bindings - exceptions in info scripts are now non-fatal - 'video' PageProp and PlayVideo() function for (HIGHLY EXPERIMENTAL!!!) fullscreen video playback - PDF parser now emits warnings instead of failing silently if something isn't OK - support for list files using the @filename syntax - 'comment' PageProp displays overlay text on a slide permanently - added auto-overview mode (idea by Tobias Maier) - restore compatibility with KeyJnoteGUI 0.10.1a [2007-11-27] - restored compatibility with poppler-based pdftoppm - added Alt+Tab as alias for "exit fullscreen mode" - improved cleanup code 0.10.1 [2007-09-09] - re-enabled GL_ARB_texture_non_power_of_two support (was broken in 0.10.0) - PDF hyperlink support - added compatibility fix for PyOpenGL 3.x - generalized and simplified PageProp system - rotate PageProp - multi-file support - persistent cache 0.10.0 [2007-06-02] - added pdftoppm rendering backend - overview page update fix - more font search paths (DejaVuSans.ttf, OSX paths) - graceful handling of failed renders - converted logo to PNG - fixed -T0 crash - allowed None as a value for the 'transition' PageProp and -t parameter - added 'transtime' PageProp to set transition duration for single pages - R key: reset timer - added 'progress' PageProp to show/hide progress bar - added 'reset' PageProp to reset the timer when the page is left - allowed special value FirstTimeOnly for the 'reset' prop - L key: return to last (= most recently used) page or back - added OnEnter, OnLeave, OnEnterOnce and OnLeaveOnce page props - customizable OSD layout and alpha - I and O keys change 'skip' and 'overlay' PageProps interactively - now accepting image file names at the command line - fixed supersample mode - added key repeat to quickly change the spotlight size via the keyboard - Home and End keys now navigate to the start and end of the presentation - shortcut function for any unused letter, number or function key - removed PagePeel from the default transitions list - two new families of transitions: {Slide,Squeeze}{Left,Right,Up,Down} 0.9.4 [2007-03-19] - middle mouse button now acts as overview toggle - automatic display mode guessing now implemented on Unix, too - fixed 'T' key - fixed crash in the OSD code if a broken version of PIL is used - added gamma controls (courtesy of Luke Campagnola) - added progress bar / estimated duration - added loadable cursor image 0.9.3 [2007-02-26] - fixed indentation bug (removing a box caused a transition to the prev page) - improved pdftk parser (handles non-ASCII characters gracefully) - all messages are redirected to stderr - added timer ([T] key) and time tracking mode - made all main keyboard bindings unicode key queries 0.9.2 [2007-02-17] - fixed memory cache - added "polling" option - added "cache range" option - added overview page OSD 0.9.1 [2007-01-24] - FIX: 'whitening' mode didn't work with GL_EXT_texture_rectangle - FIX: crash when clicking a mouse button in fade mode - FIX: occasionally left behind temp files 0.9.0 [2007-01-07] - fixed all OpenGL calls to use the explicit form (glTexCoord2d instead of glTexCoord) - corrected texture target specification for rare transitions - added unicode key aliases for the [+] and [-] keys (should improve i18n) - added "fade to solid color" feature - added 'skip' PageProp - improved/fixed window caption in overview mode - GhostScript is now called with -dUseCropBox, should improve handling of some LaTeX-beamer generated PDFs - using a temp file for the page cache now - added background rendering - Win32: true fullscreen by default 0.8.3 [2006-09-28] - 'overview' PageProp to remove pages from the overview screen - experimental aspect ratio option - new transition contributed by Ronan Le Hy - OpenGL non-power-of-two texture size extension support 0.8.2 [2006-07-13] - fixed crash on some files - added overview mode keyboard navigation - added "--rotate" option 0.8.1 [2006-02-04] - made OpenGL error reporting more concise - added "render" option to produce PNG files from a PDF 0.8.0 [2005-11-23] - fixed command-line option parser - changed some cmdline options (see documentation or -h) - added new --transition option to specify which transitions can be chosen randomly - added some new transitions from Rob Reid - fixed image file mode - fixed display of portrait images or pages - added experimental 2x zoom mode 0.7.2 [2005-10-24] - fixed two annoying bugs introduced in 0.7.1, related to mouse key remapping - fixed bug that caused parts of the old page content to be visible after transitions - added two slideshow-related options 0.7.1 [2005-10-22] - more intuitive highlight box handling (the middle mouse button is no longer used to create and destroy highlight boxes; instead, dragging the mouse while LMB is pressed creates a box and clicking with RMB deletes a box) - added 'initial page number' option (-i) 0.7.0 [2005-09-29] - fixed bug in overview page that caused the program to crash if the user clicked outside the area occupied by thumbnails - added 'timeout' PageProp to automatically advance to the next page - added EXPERIMENTAL support for sound via 'sound' PageProp 0.6.3 [2005-09-07] - Some PDF files generated by pdflatex would not play because of negative object counts. Fixed. 0.6.2 [2006-09-06] - bugfix: filenames with spaces didn't work on Win32 0.6.1 [2005-09-05] - initial public release