Hack to fix powerdot/landscape hyperlink bug
[accentuate.git] / accentuate
blobd97d2df40452017546a312987a543b68037168e2
1 #!/usr/bin/env python
2 # -*- coding: iso-8859-1 -*-
4 # ACCENTUATE
5 # Based on K******, a fancy presentation tool, by Martin Fiedler
6 # Copyright (C) 2005-2007 Martin J. Fiedler <martin.fiedler@gmx.net>
7 # portions Copyright (C) 2005 Rob Reid <rreid@drao.nrc.ca>
8 # portions Copyright (C) 2006 Ronan Le Hy <rlehy@free.fr>
9 # portions Copyright (C) 2007 Luke Campagnola <luke.campagnola@gmail.com>
11 # New Maintainer of the Accentuate fork.
13 # portions Copyright (C) 2008 Jorden Mauro <jrm8005@gmail.com>
15 # This program is free software; you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License, version 2, as
17 # published by the Free Software Foundation.
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
28 __title__ = "Accentuate"
29 __version__ = "1.0"
30 __author__ = "Jorden Mauro (based on code by Martin Fiedler"
31 __email__ = "jrm8005@gmail.com"
32 __website__ = "http://www.cs.rit.edu/~jrm8005"
33 import sys
35 def greet(): print >>sys.stderr, "Welcome to", __title__, "version", __version__
36 if __name__ == "__main__": greet()
39 TopLeft, BottomLeft, TopRight, BottomRight, TopCenter, BottomCenter = range(6)
40 NoCache, MemCache, FileCache, PersistentCache = range(4) # for CacheMode
41 Off, First, Last = range(3) # for AutoOverview
43 # You may change the following lines to modify the default settings
44 Fullscreen = True
45 Scaling = False
46 Supersample = None
47 BackgroundRendering = True
48 UseGhostScript = False
49 UseAutoScreenSize = True
50 ScreenWidth = 1024
51 ScreenHeight = 768
52 TransitionDuration = 1000
53 MouseHideDelay = 3000
54 BoxFadeDuration = 100
55 ZoomDuration = 250
56 BlankFadeDuration = 250
57 MeshResX = 48
58 MeshResY = 36
59 MarkColor = (1.0, 0.0, 0.0, 0.1)
60 BoxEdgeSize = 4
61 SpotRadius = 64
62 SpotDetail = 16
63 CacheMode = FileCache
64 OverviewBorder = 3
65 AutoOverview = Off
66 InitialPage = None
67 Wrap = False
68 FadeToBlackAtEnd = False
69 AutoAdvance = None
70 RenderToDirectory = None
71 Rotation = 0
72 AllowExtensions = True
73 DAR = None
74 PAR = 1.0
75 PollInterval = 0
76 PageRangeStart = 0
77 PageRangeEnd = 999999
78 FontSize = 14
79 FontTextureWidth = 512
80 FontTextureHeight = 256
81 Gamma = 1.0
82 BlackLevel = 0
83 GammaStep = 1.1
84 BlackLevelStep = 8
85 EstimatedDuration = None
86 ProgressBarSize = 16
87 ProgressBarAlpha = 128
88 CursorImage = None
89 CursorHotspot = (0, 0)
90 MinutesOnly = False
91 OSDMargin = 16
92 OSDAlpha = 1.0
93 OSDTimePos = TopRight
94 OSDTitlePos = BottomLeft
95 OSDPagePos = BottomRight
96 OSDStatusPos = TopLeft
97 UseLIRC = False
98 PowerdotFix = False
100 # Support for LIRC remotes
101 try:
102 import pylirc, time, select
103 from threading import Thread
104 UseLIRC = True
105 except:
106 print "LIRC support unavailable."
107 UseLIRC = False
109 if(UseLIRC is True):
110 class IRRec(Thread):
111 def __init__(self):
112 Thread.__init__(self)
114 def run(self):
115 lirchandle = pylirc.init("Accentuate", '/etc/lircrc', 0)
116 print "Succesfully opened lirc, handle: " + str(lirchandle)
117 print "LIRC event loop started..."
119 # Man this is nasty indentation, but I use 8 space tabs
120 # while apparently most Pythoners use 3 :(
121 while(1):
122 # Read next code
123 s = pylirc.nextcode()
125 if(s):
126 # Handle the event
127 for (code) in s:
129 if(code == "next"):
130 pygame.event.post(pygame.event.Event(KEYDOWN, {'key': 275, 'unicode': u'', 'mod': 0}))
131 pygame.event.post(pygame.event.Event(KEYUP, {'key': 275, 'mod': 0}))
132 elif(code == "prev"):
133 pygame.event.post(pygame.event.Event(KEYDOWN, {'key': 276, 'unicode': u'', 'mod': 0}))
134 pygame.event.post(pygame.event.Event(KEYUP, {'key': 276, 'mod': 0}))
135 elif(code == "menu"):
136 pygame.event.post(pygame.event.Event(KEYDOWN, {'key': 9, 'unicode': u'\t', 'mod': 0}))
137 pygame.event.post(pygame.event.Event(KEYUP, {'key': 9, 'mod': 0}))
138 elif(code == "play"):
139 pygame.event.post(pygame.event.Event(KEYDOWN, {'key': 13, 'unicode': u'\r', 'mod': 0}))
140 pygame.event.post(pygame.event.Event(KEYUP, {'key': 13, 'mod': 0}))
141 elif(code == "volume_down"):
142 pygame.event.post(pygame.event.Event(KEYDOWN, {'key': 274, 'unicode': u'', 'mod': 0}))
143 pygame.event.post(pygame.event.Event(KEYUP, {'key': 274, 'mod': 0}))
144 elif(code == "volume_up"):
145 pygame.event.post(pygame.event.Event(KEYDOWN, {'key': 273, 'unicode': u'\r', 'mod': 0}))
146 pygame.event.post(pygame.event.Event(KEYUP, {'key': 273, 'mod': 0}))
148 time.sleep(1);
150 # End LIRC support code
152 # import basic modules
153 import random, getopt, os, types, re, codecs, tempfile, glob, StringIO, md5, re
154 import traceback
155 from math import *
157 # initialize some platform-specific settings
158 if os.name == "nt":
159 root = os.path.split(sys.argv[0])[0] or "."
160 pdftoppmPath = os.path.join(root, "pdftoppm.exe")
161 GhostScriptPath = os.path.join(root, "gs\\gswin32c.exe")
162 GhostScriptPlatformOptions = ["-I" + os.path.join(root, "gs")]
163 try:
164 import win32api
165 MPlayerPath = os.path.join(root, "mplayer.exe")
166 def GetScreenSize():
167 dm = win32api.EnumDisplaySettings(None, -1) #ENUM_CURRENT_SETTINGS
168 return (int(dm.PelsWidth), int(dm.PelsHeight))
169 def RunURL(url):
170 win32api.ShellExecute(0, "open", url, "", "", 0)
171 except ImportError:
172 MPlayerPath = ""
173 def GetScreenSize(): return pygame.display.list_modes()[0]
174 def RunURL(url): print "Error: cannot run URL `%s'" % url
175 MPlayerPlatformOptions = [ "-colorkey", "0x000000" ]
176 MPlayerColorKey = True
177 pdftkPath = os.path.join(root, "pdftk.exe")
178 FileNameEscape = '"'
179 spawn = os.spawnv
180 if getattr(sys, "frozen", None):
181 sys.path.append(root)
182 FontPath = []
183 FontList = ["Verdana.ttf", "Arial.ttf"]
184 else:
185 pdftoppmPath = "pdftoppm"
186 GhostScriptPath = "gs"
187 GhostScriptPlatformOptions = []
188 MPlayerPath = "mplayer"
189 MPlayerPlatformOptions = [ "-vo", "gl" ]
190 MPlayerColorKey = False
191 pdftkPath = "pdftk"
192 spawn = os.spawnvp
193 FileNameEscape = ""
194 FontPath = ["/usr/share/fonts", "/usr/local/share/fonts", "/usr/X11R6/lib/X11/fonts/TTF"]
195 FontList = ["DejaVuSans.ttf", "Vera.ttf", "Verdana.ttf"]
196 def RunURL(url):
197 try:
198 spawn(os.P_NOWAIT, "xdg-open", ["xdg-open", url])
199 except OSError:
200 print >>sys.stderr, "Error: cannot open URL `%s'" % url
201 def GetScreenSize():
202 res_re = re.compile(r'\s*(\d+)x(\d+)\s+\d+\.\d+\*')
203 for path in os.getenv("PATH").split(':'):
204 fullpath = os.path.join(path, "xrandr")
205 if os.path.exists(fullpath):
206 res = None
207 try:
208 for line in os.popen(fullpath, "r"):
209 m = res_re.match(line)
210 if m:
211 res = tuple(map(int, m.groups()))
212 except OSError:
213 pass
214 if res:
215 return res
216 return pygame.display.list_modes()[0]
218 # import special modules
219 try:
220 from OpenGL.GL import *
221 import pygame
222 from pygame.locals import *
223 import Image, ImageDraw, ImageFont, ImageFilter
224 import TiffImagePlugin, BmpImagePlugin, JpegImagePlugin, PngImagePlugin, PpmImagePlugin
225 except (ValueError, ImportError), err:
226 print >>sys.stderr, "Oops! Cannot load necessary modules:", err
227 print >>sys.stderr, """To use Accentuate, you need to install the following Python modules:
228 - PyOpenGL [python-opengl] http://pyopengl.sourceforge.net/
229 - PyGame [python-pygame] http://www.pygame.org/
230 - PIL [python-imaging] http://www.pythonware.com/products/pil/
231 - PyWin32 (OPTIONAL, Win32) http://starship.python.net/crew/mhammond/win32/
232 Additionally, please be sure to have pdftoppm or GhostScript installed if you
233 intend to use PDF input."""
234 sys.exit(1)
236 try:
237 import thread
238 EnableBackgroundRendering = True
239 def create_lock(): return thread.allocate_lock()
240 except ImportError:
241 EnableBackgroundRendering = False
242 class pseudolock:
243 def __init__(self): self.state = False
244 def acquire(self, dummy=0): self.state = True
245 def release(self): self.state = False
246 def locked(self): return self.state
247 def create_lock(): return pseudolock()
250 ##### TOOL CODE ################################################################
252 # initialize private variables
253 FileName = ""
254 FileList = []
255 InfoScriptPath = None
256 Marking = False
257 Tracing = False
258 Panning = False
259 FileProps = {}
260 PageProps = {}
261 PageCache = {}
262 CacheFile = None
263 CacheFileName = None
264 CacheFilePos = 0
265 CacheMagic = ""
266 MPlayerPID = 0
267 VideoPlaying = False
268 MouseDownX = 0
269 MouseDownY = 0
270 MarkUL = (0, 0)
271 MarkLR = (0, 0)
272 ZoomX0 = 0.0
273 ZoomY0 = 0.0
274 ZoomArea = 1.0
275 ZoomMode = False
276 IsZoomed = False
277 ZoomWarningIssued = False
278 TransitionRunning = False
279 CurrentCaption = 0
280 OverviewNeedUpdate = False
281 FileStats = None
282 OSDFont = None
283 CurrentOSDCaption = ""
284 CurrentOSDPage = ""
285 CurrentOSDStatus = ""
286 CurrentOSDComment = ""
287 Lrender = create_lock()
288 Lcache = create_lock()
289 Loverview = create_lock()
290 RTrunning = False
291 RTrestart = False
292 StartTime = 0
293 CurrentTime = 0
294 PageEnterTime = 0
295 TimeDisplay = False
296 TimeTracking = False
297 FirstPage = True
298 ProgressBarPos = 0
299 CursorVisible = True
300 OverviewMode = False
301 LastPage = 0
302 WantStatus = False
304 # tool constants (used in info scripts)
305 FirstTimeOnly = 2
307 # event constants
308 USEREVENT_HIDE_MOUSE = USEREVENT
309 USEREVENT_PAGE_TIMEOUT = USEREVENT + 1
310 USEREVENT_POLL_FILE = USEREVENT + 2
311 USEREVENT_TIMER_UPDATE = USEREVENT + 3
314 # read and write the PageProps and FileProps meta-dictionaries
315 def GetProp(prop_dict, key, prop, default=None):
316 if not key in prop_dict: return default
317 if type(prop) == types.StringType:
318 return prop_dict[key].get(prop, default)
319 for subprop in prop:
320 try:
321 return prop_dict[key][subprop]
322 except KeyError:
323 pass
324 return default
325 def SetProp(prop_dict, key, prop, value):
326 if not key in prop_dict:
327 prop_dict[key] = {prop: value}
328 else:
329 prop_dict[key][prop] = value
331 def GetPageProp(page, prop, default=None):
332 global PageProps
333 return GetProp(PageProps, page, prop, default)
334 def SetPageProp(page, prop, value):
335 global PageProps
336 SetProp(PageProps, page, prop, value)
337 def GetTristatePageProp(page, prop, default=0):
338 res = GetPageProp(page, prop, default)
339 if res != FirstTimeOnly: return res
340 return (GetPageProp(page, '_shown', 0) == 1)
342 def GetFileProp(page, prop, default=None):
343 global FileProps
344 return GetProp(FileProps, page, prop, default)
345 def SetFileProp(page, prop, value):
346 global FileProps
347 SetProp(FileProps, page, prop, value)
349 # the Accentuate logo (256x64 pixels grayscale PNG)
350 LOGO = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x00@\x08\x00\x00\x00\x00\xd06\xf6b\x00'+ \
351 '\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a'+ \
352 '\x9c\x18\x00\x00\x00\x07tIME\x07\xd8\t\x01\x10\x06\x0bZ\xbb\xec\x88\x00\x00\x00\x19tEXtComment'+ \
353 '\x00Created with GIMPW\x81\x0e\x17\x00\x00\x05TIDATx\xda\xedXkL\\E\x18=\xfb\xa0\xbc\x16\x02\x94W'+ \
354 '\xdd"4\x88\x8d\x85\xb6P\x8b\xa9\xa6\xb5)-\x11\xaa\xa8\xa4\xc4Tc\xd26bc\xd5h\xc4\x1a\x8d\xa6\x06\xac5FM'+ \
355 '\x94\xd8\x88\xb1\xc1\xbe\xb4\xbe\xfda1\xd1\xa8\xd8\x07M\x9a`\x81D\xc4\x18\x0cH\x1f\xb6\xa6\xa5\xd8'+ \
356 '\x07\xb0@w\xaf?\xbeYv\xe6\xee\x9d\xb9+\x90\xf4\xc7\xce\xf9\xc3|\xe7\x9b\xfb\xdds\xcf^\xbe\x99\xb9\x80'+ \
357 '\x86\x86\x86\x86\x86\x86\x86\x86\x86\x86\x86\x86\x86\x86\x86\x86F\xf4b\xb9\x11\xc4\xf2\xe8yh\'7\xde49'+ \
358 '\xdax]\xb40\xf7\xaf[\xa5\xc4+\x93o\xc0\xe5\x84\xa81\x80{\x03j<\x93\xc3\xa4\x9ahl\x01?\x1b!\xb4F\xe1'+ \
359 '\xbf\xc0\xbc\x00g@ /Z\x0cp\x87\x1a\x9f\x03\x000\x1a\x0f\x00\x8e\x8d\xf5\xa6\x89q\x95\xe5\xc5\xb9i'+ \
360 '\xb3|\x83\xa7\xba\xdb[\xfb\xec\xf9\x8c\xeaU\x0b\xbd\x9e\xc0`\xef\xb1/:\xadn\xec}\xac\xf2\xe6\x84\xa1'+ \
361 '\x9eov]\x9e\xfas\xban[\xb3t~\xb6\xc7\x18\xb9\xd0\xd7\xf9C\xab\xdfv\xbeR\x93\xa3\x9f\\\xdbB\x7f\xfa'+ \
362 '\x1cB6\xa1\xfe"\xf7~\x18\xb6|F\x93\x8f\xfb\x7fZ\x18\xfe\xd3l\x1de\xc3s+\x85\x8cX\xcbTV\x0c\x97'+ \
363 '\xbc\'\xdc\xfb\xe4&(*)41\x94Q\xa2\x17=4X\xc5\'\x17\xfdiYR\xca\xaf\xfcG\xa0}\x1bL\x8f\xe0\xdc\x1bJ'+ \
364 '\x8e\xdc:E\x03\xc2\xae\xd8\xe7T\x19 \xd5\xc4\xb0\x9f\xf8z\xbc\xc8\xaaq\xb9\xd2K\xd6\x9e\xca\xf82'+ \
365 '\x9fY\xc3\x06Q\xf4N>\xd7\xe5\x98!\x03\x8cm\n\x03\xe4\x9a\xd8\xc27L\xecM\xc8\xa3f8\x9c\x14z\x9f\xff'+ \
366 '\xb6\xf6T\xc6{\x07\xc34\xf8\x8ad\xa2\r\xc3\xa8\x98)\x03F\xb3\xa4\x95\x14\x9a\x08\xb5D\x1e\x07\xd0F'+ \
367 '\xc3G&s\xcd\xec\x8a\x0b\xdb\x8a\x93]\xde\xfb?\x1a3\xd4\xfc\x01\xf6n7\xdc\x12\x9b\xbc\xb6\x83\x82'+ \
368 '\x9fL\xa2\xf7\x14\xc7\xe5\xbe\xc3\xc6\xef\xdb\x18\x10\x90\x19\xd0\xbd\xe3\xee\xfcDg\xacwu\xd3\x04e'+ \
369 '\xea\xa4\x95\x14\x9a\x08\xc7\x88{\x12\x00k\x83m\xc1\xd4\\V\xbd#\x9b\x119_*\xf9<?\x19\xbc\x0c\x00'+ \
370 '\x10\xdfN\xb3\x8a\x05\x03\x1ax\x07OH\x17/"\xaeY\x1b\xf0\xdd\xed\xdc.\x9e2\xdf\xca*\xa94\x01\x00\n'+ \
371 '\x88\x99\xc8\x000{\x9c\x82\x02\x96\xab\xa3\xf0\xdf\xb9\xa6\xae!\xe3\x9f#\xfe\r\x16\x96S\xf8*\xaf'+ \
372 '\xac\xc7\x05\x00XA\xd1\xa0\x8d\x01\x13\xf6\xcb{\x1ce\xfae\x13U\x9a\x00\x00\xaf\xf1\x0e\x1e\xa4`\x07'+ \
373 '\xcb\xb1\xf0u\xf3Me|\x0b\xf1%\xc1#\x06\x85\x87xeu\x94\x9a#\xfe\xc22\x03\xc6e\x06d?\xba\xfb\xf8\x99a?'+ \
374 '\xb7\xa2\xc8*\xa94\x01\x80\xf341\x0f\x02\x00\xd6Sp\x8a\x9d\x13\xfa(\xbc\xc3\xfc\xa02\xbe\xdf\xb2\xd3'+ \
375 '\x9d\xe1\x95-\x15t\x186\x06\xf8\xac\r\xf0~|\xcd|\x93\x80\xac\x92J\x13\x00T\x08\x87\xc0\x04v,\xbc\x8b'+ \
376 '\x92#\x14\xa5\x9a\x1f\xd4\x867\xf7\\^Y\n\xdb\x86N\xc7\x80\xe2\xf3\x16w\x91URi\x02\x80O\x89\xd8\xcb\xc2}'+ \
377 '\x14~B\x11;#\xb8\xcd\x0fj\xc3[jcc\x17\xdb}N\xc3\x80\xc4\x01\xe3\x7f\x18\xa0\xd2\x04 \xd5g\x99\x1eM\x99'+ \
378 '\xc17\xc0@\xa4\x0b\xbb\xb4\tz\xf8\t\xcf\xd0\xd8\xdfX\x9a\x14A\xa5\x11\xb5\x01\x8f[\xa7\x8d-\xd3\xe9\x01'+ \
379 '\x19\x11\x1c\xd3"3\xc0`\xcdh>?\xe1\xb0\xd0N\x93m*\xc959U_\xc0\x88\xff\x8d\x82*sV\xc6\xff\xce\xbe0\xce'+ \
380 '\xc4\xb1\x96\xfe\xb0\x96!X\xcd\xb6q\x1f\xb2\x86`SH\xad\xa9\xd0\x90a\x01\x00<K\xe3\xa1\x1bL\x97\xc9\xf8'+ \
381 '\xe7-\xbe\xa9\x14\x1c\x8c\xe0\r\xf0\x0b\xbf7\x00\xb0n\\JQ\x1b?\x9dMN\x146\x9f\xd2J*M\xc0[R\x03\xde\xe4w|'+ \
382 '\xed\x99l~\xd6\xe7P\xf1\xf3\xd8\xed_\t\xfdZ\xcd\x13F\x04\x06\xb0\xa3U~H#;\x99\xbe\r\x00xB\xf8\xcf\xbdJ'+ \
383 '\xe3\x87\x00\x00\x0f\x1b6\x95T\x9a\xe0>G\xc9Z\xfe\xe5\'\xea\xac\x0b\x00v\xb3\xea\xe7_Z\xecqeW|0\xc2'+ \
384 '\xae\x94\xf1\x9f1\xfe\xc8\xfa\x9cY1\xde\xd5/\xb7\x87\xa4)\r\xf8\x83\xa2\x96\xa2X\x88\xcbQ`\xbb7\xb6\xf0]'+ \
385 '\xbf\xf0\x94\'h|isz\xcc\x82F\xbfaWI\xa1\tU\xac\x9ffr\x06\xa4\xb3=\xc6=\x00\x90y\xd6\xba{\xca\xf8\x9c\x8b'+ \
386 '\xd2\x86\xab4\xe0@\xd8\x055\xf2\xde\xdd\xa0j\xeb\xe1\x95\x14\x9a\xf0\xb5\xe9\xf0\x03\x008B\xe4W\x00\x80eW'+ \
387 '\xac\xaf\x94\xf1\xe5\xbe)\x19\xf0@\xd8\x05n\xe1\x83\xcbN~z\x96\xf0-b\x8f]%\x85\xa6tv\xf6\xd9ju\xd2\x19\x9b'+ \
388 '\r\x00(\xe9\xb3^?e\xfc\x9d\xe6w\xa3\xbb2\x02\x03b:\xc3j\xad\x18\x0f\x11\xfb]\xc2\xf4u\xdc\x19\xe0P\x9cm%'+ \
389 '\xb9\xa6\xa7\x8d\xe0\xa7\x10\x1e\xf9\x8c}\x8a\xed\xda\xb7\x0fY~\xfb\x93\xf1i\x8d\xc3\xdc\x0e\xfd\xf0:g$'+ \
390 '\xfb\x00\xe4v\x84\xd5\xaa\x08nx}/8L\xd3\xef\x9b\xfc\xc6\xd1\x1c\x17A%\x89&\x07\xba\x16\xd3\xaa^$.f\xdd'+ \
391 '\x85\x00\x80\xae\xe0\x01*~\xed\x9a\x92\xdc4\xf7\xd8\xe0\xc9\x9e\xf6\x1f\xb9\xaf\xbf2>\xf9\xde\xb2E7'+ \
392 '\xa6\xb8\xae\x9e\xfe\xf5h\xcb\x80yawX\x87pWW/\x99\xe3q\xf2djmUa\xcah\xef\xf7M\x03a\xd3S7W\x15&\x8f'+ \
393 '\xfeu\xb4\xf9\x97\x88*\xc94ihhhhhhhhhhhhhhhD\x19\xfe\x03\xec\xf7\xad7pH,\x9f\x00\x00\x00\x00IEND\xaeB`\x82'
395 # determine the next power of two
396 def npot(x):
397 res = 1
398 while res < x: res <<= 1
399 return res
401 # convert boolean value to string
402 def b2s(b):
403 if b: return "Y"
404 return "N"
406 # extract a number at the beginning of a string
407 def num(s):
408 s = s.strip()
409 r = ""
410 while s[0] in "0123456789":
411 r += s[0]
412 s = s[1:]
413 try:
414 return int(r)
415 except ValueError:
416 return -1
418 # get a representative subset of file statistics
419 def my_stat(filename):
420 try:
421 s = os.stat(filename)
422 except OSError:
423 return None
424 return (s.st_size, s.st_mtime, s.st_ctime, s.st_mode)
426 # determine (pagecount,width,height) of a PDF file
427 def analyze_pdf(filename):
428 f = file(filename,"rb")
429 pdf = f.read()
430 f.close()
431 box = map(float, pdf.split("/MediaBox",1)[1].split("]",1)[0].split("[",1)[1].strip().split())
432 return (max(map(num, pdf.split("/Count")[1:])), box[2]-box[0], box[3]-box[1])
434 # unescape &#123; literals in PDF files
435 re_unescape = re.compile(r'&#[0-9]+;')
436 def decode_literal(m):
437 try:
438 return chr(int(m.group(0)[2:-1]))
439 except ValueError:
440 return '?'
441 def unescape_pdf(s):
442 return re_unescape.sub(decode_literal, s)
444 # parse pdftk output
445 def pdftkParse(filename, page_offset=0):
446 f = file(filename, "r")
447 InfoKey = None
448 BookmarkTitle = None
449 Title = None
450 Pages = 0
451 for line in f.xreadlines():
452 try:
453 key, value = [item.strip() for item in line.split(':', 1)]
454 except IndexError:
455 continue
456 key = key.lower()
457 if key == "numberofpages":
458 Pages = int(value)
459 elif key == "infokey":
460 InfoKey = value.lower()
461 elif (key == "infovalue") and (InfoKey == "title"):
462 Title = unescape_pdf(value)
463 InfoKey = None
464 elif key == "bookmarktitle":
465 BookmarkTitle = unescape_pdf(value)
466 elif key == "bookmarkpagenumber" and BookmarkTitle:
467 try:
468 page = int(value)
469 if not GetPageProp(page + page_offset, '_title'):
470 SetPageProp(page + page_offset, '_title', BookmarkTitle)
471 except ValueError:
472 pass
473 BookmarkTitle = None
474 f.close()
475 if AutoOverview:
476 SetPageProp(page_offset + 1, '_overview', True)
477 for page in xrange(page_offset + 2, page_offset + Pages):
478 SetPageProp(page, '_overview', \
479 not(not(GetPageProp(page + AutoOverview - 1, '_title'))))
480 SetPageProp(page_offset + Pages, '_overview', True)
481 return (Title, Pages)
483 # translate pixel coordinates to normalized screen coordinates
484 def MouseToScreen(mousepos):
485 return (ZoomX0 + mousepos[0] * ZoomArea / ScreenWidth,
486 ZoomY0 + mousepos[1] * ZoomArea / ScreenHeight)
488 # normalize rectangle coordinates so that the upper-left point comes first
489 def NormalizeRect(X0, Y0, X1, Y1):
490 return (min(X0, X1), min(Y0, Y1), max(X0, X1), max(Y0, Y1))
492 # check if a point is inside a box (or a list of boxes)
493 def InsideBox(x, y, box):
494 return (x >= box[0]) and (y >= box[1]) and (x < box[2]) and (y < box[3])
495 def FindBox(x, y, boxes):
496 for i in xrange(len(boxes)):
497 if InsideBox(x, y, boxes[i]):
498 return i
499 raise ValueError
501 # zoom an image size to a destination size, preserving the aspect ratio
502 def ZoomToFit(size, dest=None):
503 if not dest:
504 dest = (ScreenWidth, ScreenHeight)
505 newx = dest[0]
506 newy = size[1] * newx / size[0]
507 if newy > dest[1]:
508 newy = dest[1]
509 newx = size[0] * newy / size[1]
510 return (newx, newy)
512 # get the overlay grid screen coordinates for a specific page
513 def OverviewPos(page):
514 return ( \
515 int(page % OverviewGridSize) * OverviewCellX + OverviewOfsX, \
516 int(page / OverviewGridSize) * OverviewCellY + OverviewOfsY \
519 def StopMPlayer():
520 global MPlayerPID, VideoPlaying
521 if not MPlayerPID: return
522 try:
523 if os.name == 'nt':
524 win32api.TerminateProcess(MPlayerPID, 0)
525 else:
526 os.kill(MPlayerPID, 2)
527 MPlayerPID = 0
528 except:
529 pass
530 VideoPlaying = False
532 def FormatTime(t, minutes=False):
533 if minutes and (t < 3600):
534 return "%d min" % (t / 60)
535 elif minutes:
536 return "%d:%02d" % (t / 3600, (t / 60) % 60)
537 elif t < 3600:
538 return "%d:%02d" % (t / 60, t % 60)
539 else:
540 ms = t % 3600
541 return "%d:%02d:%02d" % (t / 3600, ms / 60, ms % 60)
543 def SafeCall(func, args=[], kwargs={}):
544 if not func: return None
545 try:
546 return func(*args, **kwargs)
547 except:
548 print >>sys.stderr, "----- Exception in user function ----"
549 traceback.print_exc(file=sys.stderr)
550 print >>sys.stderr, "----- End of traceback -----"
552 def Quit(code=0):
553 print >>sys.stderr, "Total presentation time: %s." % \
554 FormatTime((pygame.time.get_ticks() - StartTime) / 1000)
555 sys.exit(code)
558 ##### RENDERING TOOL CODE ######################################################
560 # draw a fullscreen quad
561 def DrawFullQuad():
562 glBegin(GL_QUADS)
563 glTexCoord2d( 0.0, 0.0); glVertex2i(0, 0)
564 glTexCoord2d(TexMaxS, 0.0); glVertex2i(1, 0)
565 glTexCoord2d(TexMaxS, TexMaxT); glVertex2i(1, 1)
566 glTexCoord2d( 0.0, TexMaxT); glVertex2i(0, 1)
567 glEnd()
569 # draw a generic 2D quad
570 def DrawQuad(x0=0.0, y0=0.0, x1=1.0, y1=1.0):
571 glBegin(GL_QUADS)
572 glTexCoord2d( 0.0, 0.0); glVertex2d(x0, y0)
573 glTexCoord2d(TexMaxS, 0.0); glVertex2d(x1, y0)
574 glTexCoord2d(TexMaxS, TexMaxT); glVertex2d(x1, y1)
575 glTexCoord2d( 0.0, TexMaxT); glVertex2d(x0, y1)
576 glEnd()
578 # helper function: draw a translated fullscreen quad
579 def DrawTranslatedFullQuad(dx, dy, i, a):
580 glColor4d(i, i, i, a)
581 glPushMatrix()
582 glTranslated(dx, dy, 0.0)
583 DrawFullQuad()
584 glPopMatrix()
586 # draw a vertex in normalized screen coordinates,
587 # setting texture coordinates appropriately
588 def DrawPoint(x, y):
589 glTexCoord2d(x *TexMaxS, y * TexMaxT)
590 glVertex2d(x, y)
591 def DrawPointEx(x, y, a):
592 glColor4d(1.0, 1.0, 1.0, a)
593 glTexCoord2d(x * TexMaxS, y * TexMaxT)
594 glVertex2d(x, y)
596 # a mesh transformation function: it gets the relative transition time (in the
597 # [0.0,0.1) interval) and the normalized 2D screen coordinates, and returns a
598 # 7-tuple containing the desired 3D screen coordinates, 2D texture coordinates,
599 # and intensity/alpha color values.
600 def meshtrans_null(t, u, v):
601 return (u, v, 0.0, u, v, 1.0, t)
602 # (x, y, z, s, t, i, a)
604 # draw a quad, applying a mesh transformation function
605 def DrawMeshQuad(time=0.0, f=meshtrans_null):
606 line0 = [f(time, u * MeshStepX, 0.0) for u in xrange(MeshResX + 1)]
607 for v in xrange(1, MeshResY + 1):
608 line1 = [f(time, u * MeshStepX, v * MeshStepY) for u in xrange(MeshResX + 1)]
609 glBegin(GL_QUAD_STRIP)
610 for col in zip(line0, line1):
611 for x, y, z, s, t, i, a in col:
612 glColor4d(i, i, i, a)
613 glTexCoord2d(s * TexMaxS, t * TexMaxT)
614 glVertex3d(x, y, z)
615 glEnd()
616 line0 = line1
618 def GenerateSpotMesh():
619 global SpotMesh
620 rx0 = SpotRadius * PixelX
621 ry0 = SpotRadius * PixelY
622 rx1 = (SpotRadius + BoxEdgeSize) * PixelX
623 ry1 = (SpotRadius + BoxEdgeSize) * PixelY
624 steps = max(6, int(2.0 * pi * SpotRadius / SpotDetail / ZoomArea))
625 SpotMesh=[(rx0 * sin(a), ry0 * cos(a), rx1 * sin(a), ry1 * cos(a)) for a in \
626 [i * 2.0 * pi / steps for i in range(steps + 1)]]
629 ##### TRANSITIONS ##############################################################
631 # Each transition is represented by a class derived from Accentuate.Transition
632 # The interface consists of only two methods: the __init__ method may perform
633 # some transition-specific initialization, and render() finally renders a frame
634 # of the transition, using the global texture identifierst Tcurrent and Tnext.
636 # Transition itself is an abstract class
637 class AbstractError(StandardError):
638 pass
639 class Transition:
640 def __init__(self):
641 pass
642 def render(self, t):
643 raise AbstractError
645 # an array containing all possible transition classes
646 AllTransitions=[]
648 # a helper function doing the common task of directly blitting a background page
649 def DrawPageDirect(tex):
650 glDisable(GL_BLEND)
651 glBindTexture(TextureTarget, tex)
652 glColor3d(1, 1, 1)
653 DrawFullQuad()
655 # a helper function that enables alpha blending
656 def EnableAlphaBlend():
657 glEnable(GL_BLEND)
658 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
661 # Crossfade: one of the simplest transition you can think of :)
662 class Crossfade(Transition):
663 """simple crossfade"""
664 def render(self,t):
665 DrawPageDirect(Tcurrent)
666 EnableAlphaBlend()
667 glBindTexture(TextureTarget, Tnext)
668 glColor4d(1, 1, 1, t)
669 DrawFullQuad()
670 AllTransitions.append(Crossfade)
673 # Slide: a class of transitions that simply slide the new page in from one side
674 # after an idea from Joachim B Haga
675 class Slide(Transition):
676 def origin(self, t):
677 raise AbstractError
678 def render(self, t):
679 cx, cy, nx, ny = self.origin(t)
680 glBindTexture(TextureTarget, Tcurrent)
681 DrawQuad(cx, cy, cx+1.0, cy+1.0)
682 glBindTexture(TextureTarget, Tnext)
683 DrawQuad(nx, ny, nx+1.0, ny+1.0)
685 class SlideLeft(Slide):
686 """Slide to the left"""
687 def origin(self, t): return (-t, 0.0, 1.0-t, 0.0)
688 class SlideRight(Slide):
689 """Slide to the right"""
690 def origin(self, t): return (t, 0.0, t-1.0, 0.0)
691 class SlideUp(Slide):
692 """Slide upwards"""
693 def origin(self, t): return (0.0, -t, 0.0, 1.0-t)
694 class SlideDown(Slide):
695 """Slide downwards"""
696 def origin(self, t): return (0.0, t, 0.0, t-1.0)
697 AllTransitions.extend([SlideLeft, SlideRight, SlideUp, SlideDown])
700 # Squeeze: a class of transitions that squeeze the new page in from one size
701 class Squeeze(Transition):
702 def params(self, t):
703 raise AbstractError
704 def inv(self): return 0
705 def render(self, t):
706 cx1, cy1, nx0, ny0 = self.params(t)
707 if self.inv():
708 t1, t2 = (Tnext, Tcurrent)
709 else:
710 t1, t2 = (Tcurrent, Tnext)
711 glBindTexture(TextureTarget, t1)
712 DrawQuad(0.0, 0.0, cx1, cy1)
713 glBindTexture(TextureTarget, t2)
714 DrawQuad(nx0, ny0, 1.0, 1.0)
715 class SqueezeHorizontal(Squeeze):
716 def split(self, t): raise AbstractError
717 def params(self, t):
718 t = self.split(t)
719 return (t, 1.0, t, 0.0)
720 class SqueezeVertical(Squeeze):
721 def split(self, t): raise AbstractError
722 def params(self, t):
723 t = self.split(t)
724 return (1.0, t, 0.0, t)
726 class SqueezeLeft(SqueezeHorizontal):
727 """Squeeze to the left"""
728 def split(self, t): return 1.0 - t
729 class SqueezeRight(SqueezeHorizontal):
730 """Squeeze to the right"""
731 def split(self, t): return t
732 def inv(self): return 1
733 class SqueezeUp(SqueezeVertical):
734 """Squeeze upwards"""
735 def split(self, t): return 1.0 - t
736 class SqueezeDown(SqueezeVertical):
737 """Squeeze downwards"""
738 def split(self, t): return t
739 def inv(self): return 1
740 AllTransitions.extend([SqueezeLeft, SqueezeRight, SqueezeUp, SqueezeDown])
743 # Wipe: a class of transitions that softly "wipe" the new image over the old
744 # one along a path specified by a gradient function that maps normalized screen
745 # coordinates to a number in the range [0.0,1.0]
746 WipeWidth = 0.25
747 class Wipe(Transition):
748 def grad(self, u, v):
749 raise AbstractError
750 def afunc(self, g):
751 pos = (g - self.Wipe_start) / WipeWidth
752 return max(min(pos, 1.0), 0.0)
753 def render(self, t):
754 DrawPageDirect(Tnext)
755 EnableAlphaBlend()
756 glBindTexture(TextureTarget, Tcurrent)
757 self.Wipe_start = t * (1.0 + WipeWidth) - WipeWidth
758 DrawMeshQuad(t, lambda t, u, v: \
759 (u, v, 0.0, u,v, 1.0, self.afunc(self.grad(u, v))))
761 class WipeDown(Wipe):
762 """wipe downwards"""
763 def grad(self, u, v): return v
764 class WipeUp(Wipe):
765 """wipe upwards"""
766 def grad(self, u, v): return 1.0 - v
767 class WipeRight(Wipe):
768 """wipe from left to right"""
769 def grad(self, u, v): return u
770 class WipeLeft(Wipe):
771 """wipe from right to left"""
772 def grad(self, u, v): return 1.0 - u
773 class WipeDownRight(Wipe):
774 """wipe from the upper-left to the lower-right corner"""
775 def grad(self, u, v): return 0.5 * (u + v)
776 class WipeUpLeft(Wipe):
777 """wipe from the lower-right to the upper-left corner"""
778 def grad(self, u, v): return 1.0 - 0.5 * (u + v)
779 class WipeCenterOut(Wipe):
780 """wipe from the center outwards"""
781 def grad(self, u, v):
782 u -= 0.5
783 v -= 0.5
784 return sqrt(u * u * 1.777 + v * v) / 0.833
785 class WipeCenterIn(Wipe):
786 """wipe from the edges inwards"""
787 def grad(self, u, v):
788 u -= 0.5
789 v -= 0.5
790 return 1.0 - sqrt(u * u * 1.777 + v * v) / 0.833
791 AllTransitions.extend([WipeDown, WipeUp, WipeRight, WipeLeft, \
792 WipeDownRight, WipeUpLeft, WipeCenterOut, WipeCenterIn])
794 class WipeBlobs(Wipe):
795 """wipe using nice \"blob\"-like patterns"""
796 def __init__(self):
797 self.uscale = (5.0 + random.random() * 15.0) * 1.333
798 self.vscale = 5.0 + random.random() * 15.0
799 self.uofs = random.random() * 6.2
800 self.vofs = random.random() * 6.2
801 def grad(self,u,v):
802 return 0.5 + 0.25 * (cos(self.uofs + u * self.uscale) \
803 + cos(self.vofs + v * self.vscale))
804 AllTransitions.append(WipeBlobs)
806 class PagePeel(Transition):
807 """an unrealistic, but nice page peel effect"""
808 def render(self,t):
809 glDisable(GL_BLEND)
810 glBindTexture(TextureTarget, Tnext)
811 DrawMeshQuad(t, lambda t, u, v: \
812 (u, v, 0.0, u, v, 1.0 - 0.5 * (1.0 - u) * (1.0 - t), 1.0))
813 EnableAlphaBlend()
814 glBindTexture(TextureTarget, Tcurrent)
815 DrawMeshQuad(t, lambda t, u, v: \
816 (u * (1.0 - t), 0.5 + (v - 0.5) * (1.0 + u * t) * (1.0 + u * t), 0.0,
817 u, v, 1.0 - u * t * t, 1.0))
818 AllTransitions.append(PagePeel)
820 ### additional transition by Ronan Le Hy <rlehy@free.fr> ###
822 class PageTurn(Transition):
823 """another page peel effect, slower but more realistic than PagePeel"""
824 alpha = 2.
825 alpha_square = alpha * alpha
826 sqrt_two = sqrt(2.)
827 inv_sqrt_two = 1. / sqrt(2.)
828 def warp(self, t, u, v):
829 # distance from the 2d origin to the folding line
830 dpt = PageTurn.sqrt_two * (1.0 - t)
831 # distance from the 2d origin to the projection of (u,v) on the folding line
832 d = PageTurn.inv_sqrt_two * (u + v)
833 dmdpt = d - dpt
834 # the smaller rho is, the closer to asymptotes are the x(u) and y(v) curves
835 # ie, smaller rho => neater fold
836 rho = 0.001
837 common_sq = sqrt(4. - 8 * t - 4.*(u+v) + 4.*t*(t + v + u) + (u+v)*(u+v) + 4 * rho) / 2.
838 x = 1. - t + 0.5 * (u - v) - common_sq
839 y = 1. - t + 0.5 * (v - u) - common_sq
840 z = - 0.5 * (PageTurn.alpha * dmdpt + sqrt(PageTurn.alpha_square * dmdpt*dmdpt + 4))
841 if dmdpt < 0:
842 # part of the sheet still flat on the screen: lit and opaque
843 i = 1.0
844 alpha = 1.0
845 else:
846 # part of the sheet in the air, after the fold: shadowed and transparent
847 # z goes from -0.8 to -2 approximately
848 i = -0.5 * z
849 alpha = 0.5 * z + 1.5
850 # the corner of the page that you hold between your fingers
851 dthumb = 0.6 * u + 1.4 * v - 2 * 0.95
852 if dthumb > 0:
853 z -= dthumb
854 x += dthumb
855 y += dthumb
856 i = 1.0
857 alpha = 1.0
858 return (x,y,z, u,v, i, alpha)
859 def render(self, t):
860 glDisable(GL_BLEND)
861 glBindTexture(TextureTarget, Tnext)
862 DrawMeshQuad(t,lambda t, u, v: \
863 (u, v, 0.0, u, v, 1.0 - 0.5 * (1.0 - u) * (1.0 - t), 1.0))
864 EnableAlphaBlend()
865 glBindTexture(TextureTarget, Tcurrent)
866 DrawMeshQuad(t, self.warp)
867 AllTransitions.append(PageTurn)
869 ##### some additional transitions by Rob Reid <rreid@drao.nrc.ca> #####
871 class Dissipate(Transition):
872 """Sort of, but not quite, like blowing a bubble."""
873 def render(self, t):
874 glBindTexture(TextureTarget,Tcurrent)
875 scalfact = 1.0 - t
876 DrawMeshQuad(t,lambda t,u,v: (u, v, t, 0.5 + scalfact * (u - 0.5),\
877 0.5 + scalfact * (v - 0.5), 1.0, \
878 scalfact * scalfact))
879 EnableAlphaBlend()
880 glBindTexture(TextureTarget,Tnext)
881 glColor4d(1,1,1,t)
882 DrawFullQuad()
883 AllTransitions.append(Dissipate)
885 class OldOutNewIn(Transition):
886 """Parent class for transitions that do something with the current slide for
887 the first half, and then something with the next slide for the second half."""
888 def ffdmq(self, t, u, v):
889 """Function for use by DrawMeshQuad()."""
890 raise AbstractError
891 def render(self, t):
892 glColor3d(0, 0, 0)
893 DrawFullQuad()
894 if t < 0.5:
895 glBindTexture(TextureTarget, Tcurrent)
896 else:
897 glBindTexture(TextureTarget, Tnext)
898 DrawMeshQuad(t, self.ffdmq)
900 class ZoomOutIn(OldOutNewIn):
901 """Zooms the current page out, and the next one in."""
902 def ffdmq(self, t, u, v):
903 scalfact = abs(1.0 - 2.0 * t)
904 return (0.5 + scalfact * (u - 0.5), 0.5 + scalfact * (v - 0.5), 0.0, \
905 u, v, 1.0, 1.0)
906 AllTransitions.append(ZoomOutIn)
908 class SpinOutIn(OldOutNewIn):
909 """Spins the current page out, and the next one in. Inspired by a classic
910 1960's TV show."""
911 def ffdmq(self, t, u, v):
912 scalfact = abs(1.0 - 2.0 * t)
913 sa = scalfact * sin(16.0 * t)
914 ca = scalfact * cos(16.0 * t)
915 return (0.5 + ca * (u - 0.5) - 0.75 * sa * (v - 0.5),\
916 0.5 + 1.333 * sa * (u - 0.5) + ca * (v - 0.5),\
917 0.0, u, v, 1.0, 1.0)
918 AllTransitions.append(SpinOutIn)
920 class SpiralOutIn(OldOutNewIn):
921 """Flushes the current page away, only to have the next one overflow!"""
922 def ffdmq(self, t, u, v):
923 scalfact = abs(1.0 - 2.0 * t)
924 sa = scalfact * sin(16.0 * t)
925 ca = scalfact * cos(16.0 * t)
926 return (0.5 + sa + ca * (u - 0.5) - 0.75 * sa * (v - 0.5),\
927 0.5 + ca + 1.333 * sa * (u - 0.5) + ca * (v - 0.5),\
928 0.0, u, v, 1.0, 1.0)
929 AllTransitions.append(SpiralOutIn)
931 class SlideCarousel(OldOutNewIn):
932 """Old school transition accidentally made while working on SpinOutIn."""
933 def ffdmq(self, t, u, v):
934 sa = sin(6.0 * t)
935 ca = cos(6.0 * t)
936 scalfact = 0.5 + 0.5 * abs(1.0 - 2.0 * t)
937 const = 0.5 - 0.5 * scalfact
938 return (ca * (const + scalfact * u) - sa * (const + scalfact * v), \
939 sa * (const + scalfact * u) + ca * (const + scalfact * v), \
940 0.0, u, v, 1.0, 1.0)
941 AllTransitions.append(SlideCarousel)
943 class FlipBoardVert(OldOutNewIn):
944 """Should look something like the spinning bookcase to the secret room..."""
945 def ffdmq(self, t, u, v):
946 pit = 3.1415626 * t
947 c = cos(pit)
948 s = sin(pit)
949 if t < 0.5:
950 x = 0.5 + (u - 0.5) * c
951 z = (u - 0.5) * s
952 else:
953 x = 0.5 - (u - 0.5) * c
954 z = (0.5 - u) * s
956 return (x, v, z, u, v, 1.0, 1.0)
957 AllTransitions.append(FlipBoardVert)
959 class FlipBoardHori(OldOutNewIn):
960 """Some blackboards and whiteboards do this..."""
961 def ffdmq(self, t, u, v):
962 pit = 3.1415626 * t
963 c = cos(pit)
964 s = sin(pit)
965 if t < 0.5:
966 y = 0.5 + (v - 0.5) * c
967 z = (v - 0.5) * s
968 else:
969 y = 0.5 - (v - 0.5) * c
970 z = (0.5 - v) * s
972 return (u, y, z, u, v, 1.0, 1.0)
973 AllTransitions.append(FlipBoardHori)
975 def verticalblindold(t, u, v):
976 pit = 3.1415626 * t
977 umuaxis = (u % 0.2) - 0.1
978 uaxis = u - umuaxis
979 return (uaxis + umuaxis * cos(pit), v, umuaxis * sin(pit), u, v, 1.0, 1.0)
981 def verticalblindnew(t, u, v):
982 vmvaxis = (v % 0.2) - 0.1
983 vaxis = v - vmvaxis
984 return (u, vaxis - vmvaxis * cos(3.1415626 * t),\
985 -vmvaxis * sin(3.1415626 * t), u, v, 1.0, 1.0)
987 class VerticalBlinds(Transition):
988 """Vertical Venetian Blinds"""
989 def render(self, t):
990 glColor3d(0,0,0)
991 DrawFullQuad()
992 if t < 0.5:
993 glBindTexture(TextureTarget,Tcurrent)
994 DrawMeshQuad(t, verticalblindold)
995 else:
996 glBindTexture(TextureTarget,Tnext)
997 DrawMeshQuad(1.0 - t, verticalblindold)
998 AllTransitions.append(VerticalBlinds)
1000 # the AvailableTransitions array contains a list of all transition classes that
1001 # can be randomly assigned to pages
1002 AvailableTransitions=[ # from coolest to lamest
1003 # PagePeel, # deactivated: too intrusive
1004 WipeBlobs,
1005 WipeCenterOut,WipeCenterIn,
1006 WipeDownRight,WipeUpLeft,WipeDown,WipeUp,WipeRight,WipeLeft,
1007 Crossfade
1011 ##### OSD FONT RENDERER ########################################################
1013 # force a string or sequence of ordinals into a unicode string
1014 def ForceUnicode(s, charset='iso8859-15'):
1015 if type(s) == types.UnicodeType:
1016 return s
1017 if type(s) == types.StringType:
1018 return unicode(s, charset, 'ignore')
1019 if type(s) in (types.TupleType, types.ListType):
1020 return u''.join(map(unichr, s))
1021 raise TypeError, "string argument not convertible to Unicode"
1023 # search a system font path for a font file
1024 def SearchFont(root, name):
1025 if not os.path.isdir(root):
1026 return None
1027 infix = ""
1028 fontfile = []
1029 while (len(infix) < 10) and (len(fontfile) != 1):
1030 fontfile = filter(os.path.isfile, glob.glob(root + infix + name))
1031 infix += "*/"
1032 if len(fontfile) != 1:
1033 return None
1034 else:
1035 return fontfile[0]
1037 # load a system font
1038 def LoadFont(dirs, name, size):
1039 # first try to load the font directly
1040 try:
1041 return ImageFont.truetype(name, size, encoding='unic')
1042 except:
1043 pass
1044 # no need to search further on Windows
1045 if os.name == 'nt':
1046 return None
1047 # start search for the font
1048 for dir in dirs:
1049 fontfile = SearchFont(dir + "/", name)
1050 if fontfile:
1051 try:
1052 return ImageFont.truetype(fontfile, size, encoding='unic')
1053 except:
1054 pass
1055 return None
1057 # alignment constants
1058 Left = 0
1059 Right = 1
1060 Center = 2
1061 Down = 0
1062 Up = 1
1063 Auto = -1
1065 # font renderer class
1066 class GLFont:
1067 def __init__(self, width, height, name, size, search_path=[], default_charset='iso8859-15', extend=1, blur=1):
1068 self.width = width
1069 self.height = height
1070 self._i_extend = range(extend)
1071 self._i_blur = range(blur)
1072 self.feather = extend + blur + 1
1073 self.current_x = 0
1074 self.current_y = 0
1075 self.max_height = 0
1076 self.boxes = {}
1077 self.widths = {}
1078 self.line_height = 0
1079 self.default_charset = default_charset
1080 if type(name) == types.StringType:
1081 self.font = LoadFont(search_path, name, size)
1082 else:
1083 for check_name in name:
1084 self.font = LoadFont(search_path, check_name, size)
1085 if self.font: break
1086 if not self.font:
1087 raise IOError, "font file not found"
1088 self.img = Image.new('LA', (width, height))
1089 self.alpha = Image.new('L', (width, height))
1090 self.extend = ImageFilter.MaxFilter()
1091 self.blur = ImageFilter.Kernel((3, 3), [1,2,1,2,4,2,1,2,1])
1092 self.tex = glGenTextures(1)
1093 glBindTexture(GL_TEXTURE_2D, self.tex)
1094 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
1095 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
1096 self.AddString(range(32, 128))
1098 def AddCharacter(self, c):
1099 w, h = self.font.getsize(c)
1100 self.line_height = max(self.line_height, h)
1101 size = (w + 2 * self.feather, h + 2 * self.feather)
1102 glyph = Image.new('L', size)
1103 draw = ImageDraw.Draw(glyph)
1104 draw.text((self.feather, self.feather), c, font=self.font, fill=255)
1105 del draw
1107 box = self.AllocateGlyphBox(*size)
1108 self.img.paste(glyph, (box.orig_x, box.orig_y))
1110 for i in self._i_extend: glyph = glyph.filter(self.extend)
1111 for i in self._i_blur: glyph = glyph.filter(self.blur)
1112 self.alpha.paste(glyph, (box.orig_x, box.orig_y))
1114 self.boxes[c] = box
1115 self.widths[c] = w
1116 del glyph
1118 def AddString(self, s, charset=None, fail_silently=False):
1119 update_count = 0
1120 try:
1121 for c in ForceUnicode(s, self.GetCharset(charset)):
1122 if c in self.widths:
1123 continue
1124 self.AddCharacter(c)
1125 update_count += 1
1126 except ValueError:
1127 if fail_silently:
1128 pass
1129 else:
1130 raise
1131 if not update_count: return
1132 self.img.putalpha(self.alpha)
1133 glBindTexture(GL_TEXTURE_2D, self.tex)
1134 glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, \
1135 self.width, self.height, 0, \
1136 GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, self.img.tostring())
1138 def AllocateGlyphBox(self, w, h):
1139 if self.current_x + w > self.width:
1140 self.current_x = 0
1141 self.current_y += self.max_height
1142 self.max_height = 0
1143 if self.current_y + h > self.height:
1144 raise ValueError, "bitmap too small for all the glyphs"
1145 box = self.GlyphBox()
1146 box.orig_x = self.current_x
1147 box.orig_y = self.current_y
1148 box.size_x = w
1149 box.size_y = h
1150 box.x0 = self.current_x / float(self.width)
1151 box.y0 = self.current_y / float(self.height)
1152 box.x1 = (self.current_x + w) / float(self.width)
1153 box.y1 = (self.current_y + h) / float(self.height)
1154 box.dsx = w * PixelX
1155 box.dsy = h * PixelY
1156 self.current_x += w
1157 self.max_height = max(self.max_height, h)
1158 return box
1160 def GetCharset(self, charset=None):
1161 if charset: return charset
1162 return self.default_charset
1164 def SplitText(self, s, charset=None):
1165 return ForceUnicode(s, self.GetCharset(charset)).split(u'\n')
1167 def GetLineHeight(self):
1168 return self.line_height
1170 def GetTextWidth(self, s, charset=None):
1171 return max([self.GetTextWidthEx(line) for line in self.SplitText(s, charset)])
1173 def GetTextHeight(self, s, charset=None):
1174 return len(self.SplitText(s, charset)) * self.line_height
1176 def GetTextSize(self, s, charset=None):
1177 lines = self.SplitText(s, charset)
1178 return (max([self.GetTextWidthEx(line) for line in lines]), len(lines) * self.line_height)
1180 def GetTextWidthEx(self, u):
1181 if u: return sum([self.widths.get(c, 0) for c in u])
1182 else: return 0
1184 def GetTextHeightEx(self, u=[]):
1185 return self.line_height
1187 def AlignTextEx(self, x, u, align=Left):
1188 if not align: return x
1189 return x - (self.GetTextWidthEx(u) / align)
1191 def Draw(self, origin, text, charset=None, align=Left, color=(1.0, 1.0, 1.0), alpha=1.0, beveled=True):
1192 lines = self.SplitText(text, charset)
1193 x0, y0 = origin
1194 x0 -= self.feather
1195 y0 -= self.feather
1196 glEnable(GL_TEXTURE_2D)
1197 glEnable(GL_BLEND)
1198 glBindTexture(GL_TEXTURE_2D, self.tex)
1199 if beveled:
1200 glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_ALPHA)
1201 glColor4d(0.0, 0.0, 0.0, alpha)
1202 self.DrawLinesEx(x0, y0, lines, align)
1203 glBlendFunc(GL_ONE, GL_ONE)
1204 glColor3d(color[0] * alpha, color[1] * alpha, color[2] * alpha)
1205 self.DrawLinesEx(x0, y0, lines, align)
1206 glDisable(GL_BLEND)
1207 glDisable(GL_TEXTURE_2D)
1209 def DrawLinesEx(self, x0, y, lines, align=Left):
1210 global PixelX, PixelY
1211 glBegin(GL_QUADS)
1212 for line in lines:
1213 sy = y * PixelY
1214 x = self.AlignTextEx(x0, line, align)
1215 for c in line:
1216 if not c in self.widths: continue
1217 self.boxes[c].render(x * PixelX, sy)
1218 x += self.widths[c]
1219 y += self.line_height
1220 glEnd()
1222 class GlyphBox:
1223 def render(self, sx=0.0, sy=0.0):
1224 glTexCoord2d(self.x0, self.y0); glVertex2d(sx, sy)
1225 glTexCoord2d(self.x0, self.y1); glVertex2d(sx, sy+self.dsy)
1226 glTexCoord2d(self.x1, self.y1); glVertex2d(sx+self.dsx, sy+self.dsy)
1227 glTexCoord2d(self.x1, self.y0); glVertex2d(sx+self.dsx, sy)
1229 # high-level draw function
1230 def DrawOSD(x, y, text, halign=Auto, valign=Auto, alpha=1.0):
1231 if not(OSDFont) or not(text) or (alpha <= 0.004): return
1232 if alpha > 1.0: alpha = 1.0
1233 if halign == Auto:
1234 if x < 0:
1235 x += ScreenWidth
1236 halign = Right
1237 else:
1238 halign = Left
1239 if valign == Auto:
1240 if y < 0:
1241 y += ScreenHeight
1242 valign = Up
1243 else:
1244 valign = Down
1245 if valign != Down:
1246 y -= OSDFont.GetLineHeight() / valign
1247 if TextureTarget != GL_TEXTURE_2D:
1248 glDisable(TextureTarget)
1249 OSDFont.Draw((x, y), text, align=halign, alpha=alpha)
1251 # very high-level draw function
1252 def DrawOSDEx(position, text, alpha_factor=1.0):
1253 xpos = position >> 1
1254 y = (1 - 2 * (position & 1)) * OSDMargin
1255 if xpos < 2:
1256 x = (1 - 2 * xpos) * OSDMargin
1257 halign = Auto
1258 else:
1259 x = ScreenWidth / 2
1260 halign = Center
1261 DrawOSD(x, y, text, halign, alpha = OSDAlpha * alpha_factor)
1264 ##### PDF PARSER ###############################################################
1266 class PDFError(Exception):
1267 pass
1269 class PDFref:
1270 def __init__(self, ref):
1271 self.ref = ref
1272 def __repr__(self):
1273 return "PDFref(%d)" % self.ref
1275 re_pdfstring = re.compile(r'\(\)|\(.*?[^\\]\)')
1276 pdfstringrepl = [("\\"+x[0], x[1:]) for x in "(( )) n\n r\r t\t".split(" ")]
1277 def pdf_maskstring(s):
1278 s = s[1:-1]
1279 for a, b in pdfstringrepl:
1280 s = s.replace(a, b)
1281 return " <" + "".join(["%02X"%ord(c) for c in s]) + "> "
1282 def pdf_mask_all_strings(s):
1283 return re_pdfstring.sub(lambda x: pdf_maskstring(x.group(0)), s)
1284 def pdf_unmaskstring(s):
1285 return "".join([chr(int(s[i:i+2], 16)) for i in xrange(1, len(s)-1, 2)])
1287 class PDFParser:
1288 def __init__(self, filename):
1289 self.f = file(filename, "rb")
1291 # find the first cross-reference table
1292 self.f.seek(0, 2)
1293 filesize = self.f.tell()
1294 self.f.seek(filesize - 128)
1295 trailer = self.f.read()
1296 i = trailer.rfind("startxref")
1297 if i < 0:
1298 raise PDFError, "cross-reference table offset missing"
1299 try:
1300 offset = int(trailer[i:].split("\n")[1].strip())
1301 except (IndexError, ValueError):
1302 raise PDFError, "malformed cross-reference table offset"
1304 # follow the trailer chain
1305 self.xref = {}
1306 while offset:
1307 newxref = self.xref
1308 self.xref, rootref, offset = self.parse_trailer(offset)
1309 self.xref.update(newxref)
1311 # scan the page tree
1312 self.obj2page = {}
1313 self.page2obj = {}
1314 self.annots = {}
1315 self.page_count = 0
1316 self.box = {}
1317 root = self.getobj(rootref, 'Catalog')
1318 try:
1319 self.scan_page_tree(root['Pages'].ref)
1320 except KeyError:
1321 raise PDFError, "root page tree node missing"
1323 def getline(self):
1324 while True:
1325 line = self.f.readline().strip()
1326 if line: return line
1328 def find_length(self, tokens, begin, end):
1329 level = 1
1330 for i in xrange(1, len(tokens)):
1331 if tokens[i] == begin: level += 1
1332 if tokens[i] == end: level -= 1
1333 if not level: break
1334 return i + 1
1336 def parse_tokens(self, tokens, want_list=False):
1337 res = []
1338 while tokens:
1339 t = tokens[0]
1340 v = t
1341 tlen = 1
1342 if (len(tokens) >= 3) and (tokens[2] == 'R'):
1343 v = PDFref(int(t))
1344 tlen = 3
1345 elif t == "<<":
1346 tlen = self.find_length(tokens, "<<", ">>")
1347 v = self.parse_tokens(tokens[1 : tlen - 1], True)
1348 v = dict(zip(v[::2], v[1::2]))
1349 elif t == "[":
1350 tlen = self.find_length(tokens, "[", "]")
1351 v = self.parse_tokens(tokens[1 : tlen - 1], True)
1352 elif not(t) or (t[0] == "null"):
1353 v = None
1354 elif (t[0] == '<') and (t[-1] == '>'):
1355 v = pdf_unmaskstring(t)
1356 elif t[0] == '/':
1357 v = t[1:]
1358 elif t == 'null':
1359 v = None
1360 else:
1361 try:
1362 v = float(t)
1363 v = int(t)
1364 except ValueError:
1365 pass
1366 res.append(v)
1367 del tokens[:tlen]
1368 if want_list:
1369 return res
1370 if not res:
1371 return None
1372 if len(res) == 1:
1373 return res[0]
1374 return res
1376 def parse(self, data):
1377 data = pdf_mask_all_strings(data)
1378 data = data.replace("<<", " << ").replace("[", " [ ").replace("(", " (")
1379 data = data.replace(">>", " >> ").replace("]", " ] ").replace(")", ") ")
1380 data = data.replace("/", " /")
1381 return self.parse_tokens(filter(None, data.split()))
1383 def getobj(self, obj, force_type=None):
1384 offset = self.xref.get(obj, 0)
1385 if not offset:
1386 raise PDFError, "referenced non-existing PDF object"
1387 self.f.seek(offset)
1388 header = self.getline().split(None, 2)
1389 if (header[-1] != "obj") or (header[0] != str(obj)):
1390 raise PDFError, "object does not start where it's supposed to"
1391 data = []
1392 while True:
1393 line = self.getline()
1394 if line in ("endobj", "stream"): break
1395 data.append(line)
1396 data = self.parse(" ".join(data))
1397 if force_type:
1398 try:
1399 t = data['Type']
1400 except (KeyError, IndexError, ValueError):
1401 t = None
1402 if t != force_type:
1403 raise PDFError, "object does not match the intended type"
1404 return data
1406 def parse_xref_section(self, start, count):
1407 xref = {}
1408 for obj in xrange(start, start + count):
1409 line = self.getline()
1410 if line[-1] == 'f':
1411 xref[obj] = 0
1412 else:
1413 xref[obj] = int(line[:10], 10)
1414 return xref
1416 def parse_trailer(self, offset):
1417 self.f.seek(offset)
1418 xref = {}
1419 rootref = 0
1420 offset = 0
1421 if self.getline() != "xref":
1422 raise PDFError, "cross-reference table does not start where it's supposed to"
1423 return (xref, rootref, offset) # no xref table found, abort
1424 # parse xref sections
1425 while True:
1426 line = self.getline()
1427 if line == "trailer": break
1428 start, count = map(int, line.split())
1429 xref.update(self.parse_xref_section(start, count))
1430 # parse trailer
1431 while True:
1432 line = self.getline()
1433 if line in ("startxref", "%%EOF"): break
1434 if line[0] != '/': continue
1435 parts = line[1:].split()
1436 if parts[0] == 'Prev':
1437 offset = int(parts[1])
1438 if parts[0] == 'Root':
1439 if (len(parts) != 4) or (parts[3] != 'R'):
1440 raise PDFError, "root catalog entry is not a reference"
1441 rootref = int(parts[1])
1442 return (xref, rootref, offset)
1444 def scan_page_tree(self, obj, mbox=None, cbox=None):
1445 node = self.getobj(obj)
1446 if node['Type'] == 'Pages':
1447 for kid in node['Kids']:
1448 self.scan_page_tree(kid.ref, node.get('MediaBox', mbox), node.get('CropBox', cbox))
1449 else:
1450 page = self.page_count + 1
1451 self.page_count = page
1452 self.obj2page[obj] = page
1453 self.page2obj[page] = obj
1454 self.annots[page] = [a.ref for a in node.get('Annots', [])]
1455 self.box[page] = node.get('CropBox', cbox) or node.get('MediaBox', mbox)
1457 def dest2page(self, dest):
1458 if type(dest) != types.ListType:
1459 return dest
1460 elif dest[0].__class__ == PDFref:
1461 return self.obj2page.get(dest[0].ref, None)
1462 else:
1463 return dest[0]
1465 def get_href(self, obj):
1466 node = self.getobj(obj, 'Annot')
1467 if node['Subtype'] != 'Link': return None
1468 dest = None
1469 if 'Dest' in node:
1470 dest = self.dest2page(node['Dest'])
1471 elif 'A' in node:
1472 action = node['A']['S']
1473 if action == 'URI':
1474 dest = node['A'].get('URI', None)
1475 elif action == 'GoTo':
1476 dest = self.dest2page(node['A'].get('D', None))
1477 if dest:
1478 return tuple(node['Rect'] + [dest])
1480 def GetHyperlinks(self):
1481 res = {}
1482 for page in self.annots:
1483 a = filter(None, map(self.get_href, self.annots[page]))
1484 if a: res[page] = a
1485 return res
1488 def AddHyperlink(page_offset, page, target, linkbox, pagebox):
1489 page += page_offset
1490 if type(target) == types.IntType:
1491 target += page_offset
1492 w = 1.0 / (pagebox[2] - pagebox[0])
1493 h = 1.0 / (pagebox[3] - pagebox[1])
1495 # Hack to fix problems with landscape/powerdot files with hyperlinks
1496 if(PowerdotFix):
1497 y1 = linkbox[0] * w
1498 x1 = linkbox[3] * h
1499 y0 = linkbox[2] * w
1500 x0 = linkbox[1] * h
1501 else:
1502 x0 = (linkbox[0] - pagebox[0]) * w
1503 y0 = (pagebox[3] - linkbox[3]) * h
1504 x1 = (linkbox[2] - pagebox[0]) * w
1505 y1 = (pagebox[3] - linkbox[1]) * h
1507 href = (0, target, x0, y0, x1, y1)
1508 if GetPageProp(page, '_href'):
1509 PageProps[page]['_href'].append(href)
1510 else:
1511 SetPageProp(page, '_href', [href])
1514 def FixHyperlinks(page):
1515 if not(GetPageProp(page, '_box')) or not(GetPageProp(page, '_href')):
1516 return # no hyperlinks or unknown page size
1517 bx0, by0, bx1, by1 = GetPageProp(page, '_box')
1518 bdx = bx1 - bx0
1519 bdy = by1 - by0
1520 href = []
1521 for fixed, target, x0, y0, x1, y1 in GetPageProp(page, '_href'):
1522 if fixed:
1523 href.append((1, target, x0, y0, x1, y1))
1524 else:
1525 href.append((1, target, \
1526 int(bx0 + bdx * x0), int(by0 + bdy * y0), \
1527 int(bx0 + bdx * x1), int(by0 + bdy * y1)))
1528 SetPageProp(page, '_href', href)
1531 def ParsePDF(filename):
1532 try:
1533 assert 0 == spawn(os.P_WAIT, pdftkPath, \
1534 ["pdftk", FileNameEscape + filename + FileNameEscape, \
1535 "output", FileNameEscape + TempFileName + ".pdf" + FileNameEscape,
1536 "uncompress"])
1537 except OSError:
1538 print >>sys.stderr, "Note: pdftk not found, hyperlinks disabled."
1539 return
1540 except AssertionError:
1541 print >>sys.stderr, "Note: pdftk failed, hyperlinks disabled."
1542 return
1544 count = 0
1545 try:
1546 try:
1547 pdf = PDFParser(TempFileName + ".pdf")
1548 for page, annots in pdf.GetHyperlinks().iteritems():
1549 for page_offset in FileProps[filename]['offsets']:
1550 for a in annots:
1551 AddHyperlink(page_offset, page, a[4], a[:4], pdf.box[page])
1552 count += len(annots)
1553 FixHyperlinks(page)
1554 del pdf
1555 return count
1556 except IOError:
1557 print >>sys.stderr, "Note: file produced by pdftk not readable, hyperlinks disabled."
1558 except PDFError, e:
1559 print >>sys.stderr, "Note: error in file produced by pdftk, hyperlinks disabled."
1560 print >>sys.stderr, " PDF parser error message:", e
1561 finally:
1562 try:
1563 os.remove(TempFileName + ".pdf")
1564 except OSError:
1565 pass
1568 ##### PAGE CACHE MANAGEMENT ####################################################
1570 # helper class that allows PIL to write and read image files with an offset
1571 class IOWrapper:
1572 def __init__(self, f, offset=0):
1573 self.f = f
1574 self.offset = offset
1575 self.f.seek(offset)
1576 def read(self, count=None):
1577 if count is None:
1578 return self.f.read()
1579 else:
1580 return self.f.read(count)
1581 def write(self, data):
1582 self.f.write(data)
1583 def seek(self, pos, whence=0):
1584 assert(whence in (0, 1))
1585 if whence:
1586 self.f.seek(pos, 1)
1587 else:
1588 self.f.seek(pos + self.offset)
1589 def tell(self):
1590 return self.f.tell() - self.offset
1592 # generate a "magic number" that is used to identify persistent cache files
1593 def UpdateCacheMagic():
1594 global CacheMagic
1595 pool = [PageCount, ScreenWidth, ScreenHeight, b2s(Scaling), b2s(Supersample), b2s(Rotation)]
1596 flist = list(FileProps.keys())
1597 flist.sort(lambda a,b: cmp(a.lower(), b.lower()))
1598 for f in flist:
1599 pool.append(f)
1600 pool.extend(list(GetFileProp(f, 'stat', [])))
1601 CacheMagic = md5.new("\0".join(map(str, pool))).hexdigest()
1603 # set the persistent cache file position to the current end of the file
1604 def UpdatePCachePos():
1605 global CacheFilePos
1606 CacheFile.seek(0, 2)
1607 CacheFilePos = CacheFile.tell()
1609 # rewrite the header of the persistent cache
1610 def WritePCacheHeader(reset=False):
1611 pages = ["%08x" % PageCache.get(page, 0) for page in range(1, PageCount+1)]
1612 CacheFile.seek(0)
1613 CacheFile.write(CacheMagic + "".join(pages))
1614 if reset:
1615 CacheFile.truncate()
1616 UpdatePCachePos()
1618 # return an image from the persistent cache or None if none is available
1619 def GetPCacheImage(page):
1620 if CacheMode != PersistentCache:
1621 return # not applicable if persistent cache isn't used
1622 Lcache.acquire()
1623 try:
1624 if page in PageCache:
1625 img = Image.open(IOWrapper(CacheFile, PageCache[page]))
1626 img.load()
1627 return img
1628 finally:
1629 Lcache.release()
1631 # returns an image from the non-persistent cache or None if none is available
1632 def GetCacheImage(page):
1633 if CacheMode in (NoCache, PersistentCache):
1634 return # not applicable in uncached or persistent-cache mode
1635 Lcache.acquire()
1636 try:
1637 if page in PageCache:
1638 if CacheMode == FileCache:
1639 CacheFile.seek(PageCache[page])
1640 return CacheFile.read(TexSize)
1641 else:
1642 return PageCache[page]
1643 finally:
1644 Lcache.release()
1646 # adds an image to the persistent cache
1647 def AddToPCache(page, img):
1648 if CacheMode != PersistentCache:
1649 return # not applicable if persistent cache isn't used
1650 Lcache.acquire()
1651 try:
1652 if page in PageCache:
1653 return # page is already cached and we can't update it safely
1654 # -> stop here (the new image will be identical to the old
1655 # one anyway)
1656 img.save(IOWrapper(CacheFile, CacheFilePos), "ppm")
1657 PageCache[page] = CacheFilePos
1658 WritePCacheHeader()
1659 finally:
1660 Lcache.release()
1662 # adds an image to the non-persistent cache
1663 def AddToCache(page, data):
1664 global CacheFilePos
1665 if CacheMode in (NoCache, PersistentCache):
1666 return # not applicable in uncached or persistent-cache mode
1667 Lcache.acquire()
1668 try:
1669 if CacheMode == FileCache:
1670 if not(page in PageCache):
1671 PageCache[page] = CacheFilePos
1672 CacheFilePos += len(data)
1673 CacheFile.seek(PageCache[page])
1674 CacheFile.write(data)
1675 else:
1676 PageCache[page] = data
1677 finally:
1678 Lcache.release()
1680 # invalidates the whole cache
1681 def InvalidateCache():
1682 global PageCache, CacheFilePos
1683 Lcache.acquire()
1684 try:
1685 PageCache = {}
1686 if CacheMode == PersistentCache:
1687 UpdateCacheMagic()
1688 WritePCacheHeader(True)
1689 else:
1690 CacheFilePos = 0
1691 finally:
1692 Lcache.release()
1694 # initialize the persistent cache
1695 def InitPCache():
1696 global CacheFile, CacheMode
1698 # try to open the pre-existing cache file
1699 try:
1700 CacheFile = file(CacheFileName, "rb+")
1701 except IOError:
1702 CacheFile = None
1704 # check the cache magic
1705 UpdateCacheMagic()
1706 if CacheFile and (CacheFile.read(32) != CacheMagic):
1707 print >>sys.stderr, "Cache file mismatch, recreating cache."
1708 CacheFile.close()
1709 CacheFile = None
1711 if CacheFile:
1712 # if the magic was valid, import cache data
1713 print >>sys.stderr, "Using already existing persistent cache file."
1714 for page in range(1, PageCount+1):
1715 offset = int(CacheFile.read(8), 16)
1716 if offset:
1717 PageCache[page] = offset
1718 UpdatePCachePos()
1719 else:
1720 # if the magic was invalid or the file didn't exist, (re-)create it
1721 try:
1722 CacheFile = file(CacheFileName, "wb+")
1723 except IOError:
1724 print >>sys.stderr, "Error: cannot write the persistent cache file (`%s')" % CacheFileName
1725 print >>sys.stderr, "Falling back to temporary file cache."
1726 CacheMode = FileCache
1727 WritePCacheHeader()
1730 ##### PAGE RENDERING ###########################################################
1732 # generate a dummy image
1733 def DummyPage():
1734 img = Image.new('RGB', (ScreenWidth, ScreenHeight))
1735 img.paste(LogoImage, ((ScreenWidth - LogoImage.size[0]) / 2,
1736 (ScreenHeight - LogoImage.size[1]) / 2))
1737 return img
1739 # load a page from a PDF file
1740 def RenderPDF(page, MayAdjustResolution, ZoomMode):
1741 global UseGhostScript
1742 UseGhostScriptOnce = False
1744 SourceFile = GetPageProp(page, '_file')
1745 Resolution = GetFileProp(SourceFile, 'res', 96)
1746 RealPage = GetPageProp(page, '_page')
1748 if Supersample and not(ZoomMode):
1749 UseRes = int(0.5 + Resolution) * Supersample
1750 AlphaBits = 1
1751 else:
1752 UseRes = int(0.5 + Resolution)
1753 AlphaBits = 4
1754 if ZoomMode:
1755 UseRes = 2 * UseRes
1757 # call pdftoppm to generate the page image
1758 if not UseGhostScript:
1759 renderer = "pdftoppm"
1760 try:
1761 assert 0 == spawn(os.P_WAIT, \
1762 pdftoppmPath, ["pdftoppm", "-q"] + [ \
1763 "-f", str(RealPage), "-l", str(RealPage),
1764 "-r", str(int(UseRes)),
1765 FileNameEscape + SourceFile + FileNameEscape,
1766 TempFileName])
1767 # determine output filename
1768 digits = GetFileProp(SourceFile, 'digits', 6)
1769 imgfile = TempFileName + ("-%%0%dd.ppm" % digits) % RealPage
1770 if not os.path.exists(imgfile):
1771 for digits in xrange(6, 0, -1):
1772 imgfile = TempFileName + ("-%%0%dd.ppm" % digits) % RealPage
1773 if os.path.exists(imgfile): break
1774 SetFileProp(SourceFile, 'digits', digits)
1775 except OSError, (errcode, errmsg):
1776 print >>sys.stderr, "Warning: Cannot start pdftoppm -", errmsg
1777 print >>sys.stderr, "Falling back to GhostScript (permanently)."
1778 UseGhostScript = True
1779 except AssertionError:
1780 print >>sys.stderr, "There was an error while rendering page %d" % page
1781 print >>sys.stderr, "Falling back to GhostScript for this page."
1782 UseGhostScriptOnce = True
1784 # fallback to GhostScript
1785 if UseGhostScript or UseGhostScriptOnce:
1786 imgfile = TempFileName + ".tif"
1787 renderer = "GhostScript"
1788 try:
1789 assert 0 == spawn(os.P_WAIT, \
1790 GhostScriptPath, ["gs", "-q"] + GhostScriptPlatformOptions + [ \
1791 "-dBATCH", "-dNOPAUSE", "-sDEVICE=tiff24nc", "-dUseCropBox",
1792 "-sOutputFile=" + imgfile, \
1793 "-dFirstPage=%d" % RealPage, "-dLastPage=%d" % RealPage,
1794 "-r%dx%d" % (UseRes, int(UseRes * PAR)), \
1795 "-dTextAlphaBits=%d" % AlphaBits, \
1796 "-dGraphicsAlphaBits=%s" % AlphaBits, \
1797 FileNameEscape + SourceFile + FileNameEscape])
1798 except OSError, (errcode, errmsg):
1799 print >>sys.stderr, "Error: Cannot start GhostScript -", errmsg
1800 return DummyPage()
1801 except AssertionError:
1802 print >>sys.stderr, "There was an error while rendering page %d" % page
1803 return DummyPage()
1805 # open the page image file with PIL
1806 try:
1807 img = Image.open(imgfile)
1808 except:
1809 print >>sys.stderr, "Error: %s produced an unreadable file (page %d)" % (renderer, page)
1810 return DummyPage()
1812 # try to delete the file again (this constantly fails on Win32 ...)
1813 try:
1814 os.remove(imgfile)
1815 except OSError:
1816 pass
1818 # apply rotation
1819 rot = GetPageProp(page, 'rotate')
1820 if rot is None:
1821 rot = Rotation
1822 if rot:
1823 img = img.rotate(90 * (4 - rot))
1825 # determine real display size (don't care for ZoomMode, DisplayWidth and
1826 # DisplayHeight are only used for Supersample and AdjustResolution anyway)
1827 if Supersample:
1828 DisplayWidth = img.size[0] / Supersample
1829 DisplayHeight = img.size[1] / Supersample
1830 else:
1831 DisplayWidth = img.size[0]
1832 DisplayHeight = img.size[1]
1834 # if the image size is strange, re-adjust the rendering resolution
1835 if MayAdjustResolution \
1836 and ((abs(ScreenWidth - DisplayWidth) > 4) \
1837 or (abs(ScreenHeight - DisplayHeight) > 4)):
1838 newsize = ZoomToFit((DisplayWidth,DisplayHeight))
1839 NewResolution = newsize[0] * Resolution/DisplayWidth
1840 if abs(1.0 - NewResolution / Resolution) > 0.05:
1841 # only modify anything if the resolution deviation is large enough
1842 SetFileProp(SourceFile, 'res', NewResolution)
1843 return RenderPDF(page, False, ZoomMode)
1845 # downsample a supersampled image
1846 if Supersample and not(ZoomMode):
1847 return img.resize((DisplayWidth, DisplayHeight), Image.ANTIALIAS)
1849 return img
1852 # load a page from an image file
1853 def LoadImage(page, ZoomMode):
1854 # open the image file with PIL
1855 try:
1856 img = Image.open(GetPageProp(page, '_file'))
1857 except:
1858 print >>sys.stderr, "Image file `%s' is broken." % (FileList[page - 1])
1859 return DummyPage()
1861 # apply rotation
1862 rot = GetPageProp(page, 'rotate')
1863 if rot is None:
1864 rot = Rotation
1865 if rot:
1866 img = img.rotate(90 * (4 - rot))
1868 # determine destination size
1869 newsize = ZoomToFit(img.size)
1870 # don't scale if the source size is too close to the destination size
1871 if abs(newsize[0] - img.size[0]) < 2: newsize = img.size
1872 # don't scale if the source is smaller than the destination
1873 if not(Scaling) and (newsize > img.size): newsize = img.size
1874 # zoom up (if wanted)
1875 if ZoomMode: newsize=(2 * newsize[0], 2 * newsize[1])
1876 # skip processing if there was no change
1877 if newsize == img.size: return img
1879 # select a nice filter and resize the image
1880 if newsize > img.size:
1881 filter = Image.BICUBIC
1882 else:
1883 filter = Image.ANTIALIAS
1884 return img.resize(newsize, filter)
1887 # render a page to an OpenGL texture
1888 def PageImage(page, ZoomMode=False, RenderMode=False):
1889 global OverviewNeedUpdate
1890 EnableCacheRead = not(ZoomMode or RenderMode)
1891 EnableCacheWrite = EnableCacheRead and \
1892 (page >= PageRangeStart) and (page <= PageRangeEnd)
1894 # check for the image in the cache
1895 if EnableCacheRead:
1896 data = GetCacheImage(page)
1897 if data: return data
1899 # if it's not in the temporary cache, render it
1900 Lrender.acquire()
1901 try:
1902 # retrieve the image from the persistent cache or fully re-render it
1903 if EnableCacheRead:
1904 img = GetPCacheImage(page)
1905 else:
1906 img = None
1907 if not img:
1908 if GetPageProp(page, '_page'):
1909 img = RenderPDF(page, not(ZoomMode), ZoomMode)
1910 else:
1911 img = LoadImage(page, ZoomMode)
1912 if EnableCacheWrite:
1913 AddToPCache(page, img)
1915 # create black background image to paste real image onto
1916 if ZoomMode:
1917 TextureImage = Image.new('RGB', (2 * TexWidth, 2 * TexHeight))
1918 TextureImage.paste(img, ((2 * ScreenWidth - img.size[0]) / 2, \
1919 (2 * ScreenHeight - img.size[1]) / 2))
1920 else:
1921 TextureImage = Image.new('RGB', (TexWidth, TexHeight))
1922 x0 = (ScreenWidth - img.size[0]) / 2
1923 y0 = (ScreenHeight - img.size[1]) / 2
1924 TextureImage.paste(img, (x0, y0))
1925 SetPageProp(page, '_box', (x0, y0, x0 + img.size[0], y0 + img.size[1]))
1926 FixHyperlinks(page)
1928 # paste thumbnail into overview image
1929 if GetPageProp(page, ('overview', '_overview'), True) \
1930 and (page >= PageRangeStart) and (page <= PageRangeEnd) \
1931 and not(GetPageProp(page, '_overview_rendered')) \
1932 and not(RenderMode):
1933 pos = OverviewPos(OverviewPageMapInv[page])
1934 Loverview.acquire()
1935 try:
1936 # first, fill the underlying area with black (i.e. remove the dummy logo)
1937 blackness = Image.new('RGB', (OverviewCellX - OverviewBorder, \
1938 OverviewCellY - OverviewBorder))
1939 OverviewImage.paste(blackness, (pos[0] + OverviewBorder / 2, \
1940 pos[1] + OverviewBorder))
1941 del blackness
1942 # then, scale down the original image and paste it
1943 img.thumbnail((OverviewCellX - 2 * OverviewBorder, \
1944 OverviewCellY - 2 * OverviewBorder), \
1945 Image.ANTIALIAS)
1946 OverviewImage.paste(img, \
1947 (pos[0] + (OverviewCellX - img.size[0]) / 2, \
1948 pos[1] + (OverviewCellY - img.size[1]) / 2))
1949 finally:
1950 Loverview.release()
1951 SetPageProp(page, '_overview_rendered', True)
1952 OverviewNeedUpdate = True
1953 del img
1955 # return texture data
1956 if RenderMode:
1957 return TextureImage
1958 data=TextureImage.tostring()
1959 del TextureImage
1960 finally:
1961 Lrender.release()
1963 # finally add it back into the cache and return it
1964 if EnableCacheWrite:
1965 AddToCache(page, data)
1966 return data
1968 # render a page to an OpenGL texture
1969 def RenderPage(page, target):
1970 glBindTexture(TextureTarget ,target)
1971 try:
1972 glTexImage2D(TextureTarget, 0, 3, TexWidth, TexHeight, 0,\
1973 GL_RGB, GL_UNSIGNED_BYTE, PageImage(page))
1974 except GLerror:
1975 print >>sys.stderr, "I'm sorry, but your graphics card is not capable of rendering presentations"
1976 print >>sys.stderr, "in this resolution. Either the texture memory is exhausted, or there is no"
1977 print >>sys.stderr, "support for large textures (%dx%d). Please try to run Accentuate in a" % (TexWidth, TexHeight)
1978 print >>sys.stderr, "smaller resolution using the -g command-line option."
1979 sys.exit(1)
1981 # background rendering thread
1982 def RenderThread(p1, p2):
1983 global RTrunning, RTrestart
1984 RTrunning = True
1985 RTrestart = True
1986 while RTrestart:
1987 RTrestart = False
1988 for pdf in FileProps:
1989 if not pdf.lower().endswith(".pdf"): continue
1990 if RTrestart: break
1991 ParsePDF(pdf)
1992 if RTrestart: continue
1993 for page in xrange(1, PageCount + 1):
1994 if RTrestart: break
1995 if (page != p1) and (page != p2) \
1996 and (page >= PageRangeStart) and (page <= PageRangeEnd):
1997 PageImage(page)
1998 RTrunning = False
1999 if CacheMode >= FileCache:
2000 print >>sys.stderr, "Background rendering finished, used %.1f MiB of disk space." %\
2001 (CacheFilePos / 1048576.0)
2004 ##### RENDER MODE ##############################################################
2006 def DoRender():
2007 global TexWidth, TexHeight
2008 TexWidth = ScreenWidth
2009 TexHeight = ScreenHeight
2010 if os.path.exists(RenderToDirectory):
2011 print >>sys.stderr, "Destination directory `%s' already exists," % RenderToDirectory
2012 print >>sys.stderr, "refusing to overwrite anything."
2013 return 1
2014 try:
2015 os.mkdir(RenderToDirectory)
2016 except OSError, e:
2017 print >>sys.stderr, "Cannot create destination directory `%s':" % RenderToDirectory
2018 print >>sys.stderr, e.strerror
2019 return 1
2020 print >>sys.stderr, "Rendering presentation into `%s'" % RenderToDirectory
2021 for page in xrange(1, PageCount + 1):
2022 PageImage(page, RenderMode=True).save("%s/page%04d.png" % (RenderToDirectory, page))
2023 sys.stdout.write("[%d] " % page)
2024 sys.stdout.flush()
2025 print >>sys.stderr
2026 print >>sys.stderr, "Done."
2027 return 0
2030 ##### INFO SCRIPT I/O ##########################################################
2032 # info script reader
2033 def LoadInfoScript():
2034 global PageProps
2035 try:
2036 OldPageProps = PageProps
2037 execfile(InfoScriptPath, globals())
2038 NewPageProps = PageProps
2039 PageProps = OldPageProps
2040 del OldPageProps
2041 for page in NewPageProps:
2042 for prop in NewPageProps[page]:
2043 SetPageProp(page, prop, NewPageProps[page][prop])
2044 del NewPageProps
2045 except IOError:
2046 pass
2047 except:
2048 print >>sys.stderr, "----- Exception in info script ----"
2049 traceback.print_exc(file=sys.stderr)
2050 print >>sys.stderr, "----- End of traceback -----"
2052 # we can't save lamba expressions, so we need to warn the user
2053 # in every possible way
2054 ScriptTainted = False
2055 LambdaWarning = False
2056 def here_was_a_lambda_expression_that_could_not_be_saved():
2057 global LambdaWarning
2058 if not LambdaWarning:
2059 print >>sys.stderr, "WARNING: The info script for the current file contained lambda expressions that"
2060 print >>sys.stderr, " were removed during the a save operation."
2061 LambdaWarning = True
2063 # "clean" a PageProps entry so that only 'public' properties are left
2064 def GetPublicProps(props):
2065 props = props.copy()
2066 # delete private (underscore) props
2067 for prop in list(props.keys()):
2068 if str(prop)[0] == '_':
2069 del props[prop]
2070 # clean props to default values
2071 if props.get('overview', False):
2072 del props['overview']
2073 if not props.get('skip', True):
2074 del props['skip']
2075 if ('boxes' in props) and not(props['boxes']):
2076 del props['boxes']
2077 return props
2079 # Generate a string representation of a property value. Mainly this converts
2080 # classes or instances to the name of the class.
2081 def PropValueRepr(value):
2082 global ScriptTainted
2083 if type(value) == types.FunctionType:
2084 if value.__name__ != "<lambda>":
2085 return value.__name__
2086 if not ScriptTainted:
2087 print >>sys.stderr, "WARNING: The info script contains lambda expressions, which cannot be saved"
2088 print >>sys.stderr, " back. The modifed script will be written into a separate file to"
2089 print >>sys.stderr, " minimize data loss."
2090 ScriptTainted = True
2091 return "here_was_a_lambda_expression_that_could_not_be_saved"
2092 elif type(value) == types.ClassType:
2093 return value.__name__
2094 elif type(value) == types.InstanceType:
2095 return value.__class__.__name__
2096 elif type(value) == types.DictType:
2097 return "{ " + ", ".join([PropValueRepr(k) + ": " + PropValueRepr(value[k]) for k in value]) + " }"
2098 else:
2099 return repr(value)
2101 # generate a nicely formatted string representation of a page's properties
2102 def SinglePagePropRepr(page):
2103 props = GetPublicProps(PageProps[page])
2104 if not props: return None
2105 return "\n%3d: {%s\n }" % (page, \
2106 ",".join(["\n " + repr(prop) + ": " + PropValueRepr(props[prop]) for prop in props]))
2108 # generate a nicely formatted string representation of all page properties
2109 def PagePropRepr():
2110 pages = PageProps.keys()
2111 pages.sort()
2112 return "PageProps = {%s\n}" % (",".join(filter(None, map(SinglePagePropRepr, pages))))
2114 # count the characters of a python dictionary source code, correctly handling
2115 # embedded strings and comments, and nested dictionaries
2116 def CountDictChars(s, start=0):
2117 context = None
2118 level = 0
2119 for i in xrange(start, len(s)):
2120 c = s[i]
2121 if context is None:
2122 if c == '{': level += 1
2123 if c == '}': level -= 1
2124 if c == '#': context = '#'
2125 if c == '"': context = '"'
2126 if c == "'": context = "'"
2127 elif context[0] == "\\":
2128 context=context[1]
2129 elif context == '#':
2130 if c in "\r\n": context = None
2131 elif context == '"':
2132 if c == "\\": context = "\\\""
2133 if c == '"': context = None
2134 elif context == "'":
2135 if c == "\\": context = "\\'"
2136 if c == "'": context = None
2137 if level < 0: return i
2138 raise ValueError, "the dictionary never ends"
2140 # modify and save a file's info script
2141 def SaveInfoScript(filename):
2142 # read the old info script
2143 try:
2144 f = file(filename, "r")
2145 script = f.read()
2146 f.close()
2147 except IOError:
2148 script = ""
2149 if not script:
2150 script = "# -*- coding: iso-8859-1 -*-\n"
2152 # replace the PageProps of the old info script with the current ones
2153 try:
2154 m = re.search("^.*(PageProps)\s*=\s*(\{).*$", script,re.MULTILINE)
2155 if m:
2156 script = script[:m.start(1)] + PagePropRepr() + \
2157 script[CountDictChars(script, m.end(2)) + 1 :]
2158 else:
2159 script += "\n" + PagePropRepr() + "\n"
2160 except (AttributeError, ValueError):
2161 pass
2163 if ScriptTainted:
2164 filename += ".modified"
2166 # write the script back
2167 try:
2168 f = file(filename, "w")
2169 f.write(script)
2170 f.close()
2171 except:
2172 print >>sys.stderr, "Oops! Could not write info script!"
2175 ##### OPENGL RENDERING #########################################################
2177 # draw OSD overlays
2178 def DrawOverlays():
2179 reltime = pygame.time.get_ticks() - StartTime
2180 if EstimatedDuration and (OverviewMode or GetPageProp(Pcurrent, 'progress', True)):
2181 rel = (0.001 * reltime) / EstimatedDuration
2182 x = int(ScreenWidth * rel)
2183 y = 1.0 - ProgressBarSize * PixelX
2184 a = min(255, max(0, x - ScreenWidth))
2185 b = min(255, max(0, x - ScreenWidth - 256))
2186 r = a
2187 g = 255 - b
2188 b = 0
2189 glDisable(TextureTarget)
2190 glDisable(GL_TEXTURE_2D)
2191 glEnable(GL_BLEND)
2192 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
2193 glBegin(GL_QUADS)
2194 glColor4ub(r, g, b, 0)
2195 glVertex2d(0, y)
2196 glVertex2d(rel, y)
2197 glColor4ub(r, g, b, ProgressBarAlpha)
2198 glVertex2d(rel, 1.0)
2199 glVertex2d(0, 1.0)
2200 glEnd()
2201 glDisable(GL_BLEND)
2202 if WantStatus:
2203 DrawOSDEx(OSDStatusPos, CurrentOSDStatus)
2204 if TimeDisplay:
2205 t = reltime / 1000
2206 DrawOSDEx(OSDTimePos, FormatTime(t, MinutesOnly))
2207 if CurrentOSDComment and (OverviewMode or not(TransitionRunning)):
2208 DrawOSD(ScreenWidth/2, \
2209 ScreenHeight - 3*OSDMargin - FontSize, \
2210 CurrentOSDComment, Center, Up)
2211 if CursorImage and CursorVisible:
2212 x, y = pygame.mouse.get_pos()
2213 x -= CursorHotspot[0]
2214 y -= CursorHotspot[1]
2215 X0 = x * PixelX
2216 Y0 = y * PixelY
2217 X1 = X0 + CursorSX
2218 Y1 = Y0 + CursorSY
2219 glDisable(TextureTarget)
2220 glEnable(GL_TEXTURE_2D)
2221 glBindTexture(GL_TEXTURE_2D, CursorTexture)
2222 glEnable(GL_BLEND)
2223 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
2224 glColor4ub(255, 255, 255, 255)
2225 glBegin(GL_QUADS)
2226 glTexCoord2d(0.0, 0.0); glVertex2d(X0, Y0)
2227 glTexCoord2d(CursorTX, 0.0); glVertex2d(X1, Y0)
2228 glTexCoord2d(CursorTX, CursorTY); glVertex2d(X1, Y1)
2229 glTexCoord2d(0.0, CursorTY); glVertex2d(X0, Y1)
2230 glEnd()
2231 glDisable(GL_BLEND)
2232 glDisable(GL_TEXTURE_2D)
2234 # draw the complete image of the current page
2235 def DrawCurrentPage(dark=1.0, do_flip=True):
2236 if VideoPlaying: return
2237 boxes = GetPageProp(Pcurrent, 'boxes')
2238 glClear(GL_COLOR_BUFFER_BIT)
2240 # pre-transform for zoom
2241 glLoadIdentity()
2242 glOrtho(ZoomX0, ZoomX0 + ZoomArea, ZoomY0 + ZoomArea, ZoomY0, -10.0, 10.0)
2244 # background layer -- the page's image, darkened if it has boxes
2245 glDisable(GL_BLEND)
2246 glEnable(TextureTarget)
2247 glBindTexture(TextureTarget, Tcurrent)
2248 if boxes or Tracing:
2249 light = 1.0 - 0.25 * dark
2250 else:
2251 light = 1.0
2252 glColor3d(light, light, light)
2253 DrawFullQuad()
2255 if boxes or Tracing:
2256 # alpha-blend the same image some times to blur it
2257 EnableAlphaBlend()
2258 DrawTranslatedFullQuad(+PixelX * ZoomArea, 0.0, light, dark / 2)
2259 DrawTranslatedFullQuad(-PixelX * ZoomArea, 0.0, light, dark / 3)
2260 DrawTranslatedFullQuad(0.0, +PixelY * ZoomArea, light, dark / 4)
2261 DrawTranslatedFullQuad(0.0, -PixelY * ZoomArea, light, dark / 5)
2263 if boxes:
2264 # draw outer box fade
2265 EnableAlphaBlend()
2266 for X0, Y0, X1, Y1 in boxes:
2267 glBegin(GL_QUAD_STRIP)
2268 DrawPointEx(X0, Y0, 1); DrawPointEx(X0 - EdgeX, Y0 - EdgeY, 0)
2269 DrawPointEx(X1, Y0, 1); DrawPointEx(X1 + EdgeX, Y0 - EdgeY, 0)
2270 DrawPointEx(X1, Y1, 1); DrawPointEx(X1 + EdgeX, Y1 + EdgeY, 0)
2271 DrawPointEx(X0, Y1, 1); DrawPointEx(X0 - EdgeX, Y1 + EdgeY, 0)
2272 DrawPointEx(X0, Y0, 1); DrawPointEx(X0 - EdgeX, Y0 - EdgeY, 0)
2273 glEnd()
2275 # draw boxes
2276 glDisable(GL_BLEND)
2277 glBegin(GL_QUADS)
2278 for X0, Y0, X1, Y1 in boxes:
2279 DrawPoint(X0, Y0)
2280 DrawPoint(X1, Y0)
2281 DrawPoint(X1, Y1)
2282 DrawPoint(X0, Y1)
2283 glEnd()
2285 if Tracing:
2286 x, y = MouseToScreen(pygame.mouse.get_pos())
2287 # outer spot fade
2288 EnableAlphaBlend()
2289 glBegin(GL_TRIANGLE_STRIP)
2290 for x0, y0, x1, y1 in SpotMesh:
2291 DrawPointEx(x + x0, y + y0, 1)
2292 DrawPointEx(x + x1, y + y1, 0)
2293 glEnd()
2294 # inner spot
2295 glDisable(GL_BLEND)
2296 glBegin(GL_TRIANGLE_FAN)
2297 DrawPoint(x, y)
2298 for x0, y0, x1, y1 in SpotMesh:
2299 DrawPoint(x + x0, y + y0)
2300 glEnd()
2302 if Marking:
2303 # soft alpha-blended rectangle
2304 glDisable(TextureTarget)
2305 glColor4d(*MarkColor)
2306 EnableAlphaBlend()
2307 glBegin(GL_QUADS)
2308 glVertex2d(MarkUL[0], MarkUL[1])
2309 glVertex2d(MarkLR[0], MarkUL[1])
2310 glVertex2d(MarkLR[0], MarkLR[1])
2311 glVertex2d(MarkUL[0], MarkLR[1])
2312 glEnd()
2313 # bright red frame
2314 glDisable(GL_BLEND)
2315 glBegin(GL_LINE_STRIP)
2316 glVertex2d(MarkUL[0], MarkUL[1])
2317 glVertex2d(MarkLR[0], MarkUL[1])
2318 glVertex2d(MarkLR[0], MarkLR[1])
2319 glVertex2d(MarkUL[0], MarkLR[1])
2320 glVertex2d(MarkUL[0], MarkUL[1])
2321 glEnd()
2322 glEnable(TextureTarget)
2324 # unapply the zoom transform
2325 glLoadIdentity()
2326 glOrtho(0.0, 1.0, 1.0, 0.0, -10.0, 10.0)
2328 # Done.
2329 DrawOverlays()
2330 if do_flip:
2331 pygame.display.flip()
2333 # draw a black screen with the Accentuate logo at the center
2334 def DrawLogo():
2335 glClear(GL_COLOR_BUFFER_BIT)
2336 glColor3ub(255, 255, 255)
2337 if TextureTarget != GL_TEXTURE_2D:
2338 glDisable(TextureTarget)
2339 glEnable(GL_TEXTURE_2D)
2340 glBindTexture(GL_TEXTURE_2D, LogoTexture)
2341 glBegin(GL_QUADS)
2342 glTexCoord2d(0, 0); glVertex2d(0.5 - 128.0 / ScreenWidth, 0.5 - 32.0 / ScreenHeight)
2343 glTexCoord2d(1, 0); glVertex2d(0.5 + 128.0 / ScreenWidth, 0.5 - 32.0 / ScreenHeight)
2344 glTexCoord2d(1, 1); glVertex2d(0.5 + 128.0 / ScreenWidth, 0.5 + 32.0 / ScreenHeight)
2345 glTexCoord2d(0, 1); glVertex2d(0.5 - 128.0 / ScreenWidth, 0.5 + 32.0 / ScreenHeight)
2346 glEnd()
2347 if OSDFont:
2348 OSDFont.Draw((ScreenWidth / 2, ScreenHeight / 2 + 48), \
2349 __version__, align=Center, alpha=0.25)
2350 glDisable(GL_TEXTURE_2D)
2352 # draw the prerender progress bar
2353 def DrawProgress(position):
2354 glDisable(TextureTarget)
2355 x0 = 0.1
2356 x2 = 1.0 - x0
2357 x1 = position * x2 + (1.0 - position) * x0
2358 y1 = 0.9
2359 y0 = y1 - 16.0 / ScreenHeight
2360 glBegin(GL_QUADS)
2361 glColor3ub( 64, 64, 64); glVertex2d(x0, y0); glVertex2d(x2, y0)
2362 glColor3ub(128, 128, 128); glVertex2d(x2, y1); glVertex2d(x0, y1)
2363 glColor3ub( 64, 128, 255); glVertex2d(x0, y0); glVertex2d(x1, y0)
2364 glColor3ub( 8, 32, 128); glVertex2d(x1, y1); glVertex2d(x0, y1)
2365 glEnd()
2366 glEnable(TextureTarget)
2368 # fade mode
2369 def DrawFadeMode(intensity, alpha):
2370 if VideoPlaying: return
2371 DrawCurrentPage(do_flip=False)
2372 glDisable(TextureTarget)
2373 EnableAlphaBlend()
2374 glColor4d(intensity, intensity, intensity, alpha)
2375 DrawFullQuad()
2376 glEnable(TextureTarget)
2377 pygame.display.flip()
2379 def FadeMode(intensity):
2380 t0 = pygame.time.get_ticks()
2381 while True:
2382 if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]): break
2383 t = (pygame.time.get_ticks() - t0) * 1.0 / BlankFadeDuration
2384 if t >= 1.0: break
2385 DrawFadeMode(intensity, t)
2386 DrawFadeMode(intensity, 1.0)
2388 while True:
2389 event = pygame.event.wait()
2390 if event.type == QUIT:
2391 PageLeft()
2392 Quit()
2393 elif event.type == VIDEOEXPOSE:
2394 DrawFadeMode(intensity, 1.0)
2395 elif event.type == MOUSEBUTTONUP:
2396 break
2397 elif event.type == KEYDOWN:
2398 if event.unicode == u'q':
2399 pygame.event.post(pygame.event.Event(QUIT))
2400 else:
2401 break
2403 t0 = pygame.time.get_ticks()
2404 while True:
2405 if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]): break
2406 t = (pygame.time.get_ticks() - t0) * 1.0 / BlankFadeDuration
2407 if t >= 1.0: break
2408 DrawFadeMode(intensity, 1.0 - t)
2409 DrawCurrentPage()
2411 # gamma control
2412 def SetGamma(new_gamma=None, new_black=None, force=False):
2413 global Gamma, BlackLevel
2414 if new_gamma is None: new_gamma = Gamma
2415 if new_gamma < 0.1: new_gamma = 0.1
2416 if new_gamma > 10.0: new_gamma = 10.0
2417 if new_black is None: new_black = BlackLevel
2418 if new_black < 0: new_black = 0
2419 if new_black > 254: new_black = 254
2420 if not(force) and (abs(Gamma - new_gamma) < 0.01) and (new_black == BlackLevel):
2421 return
2422 Gamma = new_gamma
2423 BlackLevel = new_black
2424 scale = 1.0 / (255 - BlackLevel)
2425 power = 1.0 / Gamma
2426 ramp = [int(65535.0 * ((max(0, x - BlackLevel) * scale) ** power)) for x in range(256)]
2427 return pygame.display.set_gamma_ramp(ramp, ramp, ramp)
2429 # cursor image
2430 def PrepareCustomCursor(cimg):
2431 global CursorTexture, CursorSX, CursorSY, CursorTX, CursorTY
2432 w, h = cimg.size
2433 tw, th = map(npot, cimg.size)
2434 if (tw > 256) or (th > 256):
2435 print >>sys.stderr, "Custom cursor is rediculously large, reverting to normal one."
2436 return False
2437 img = Image.new('RGBA', (tw, th))
2438 img.paste(cimg, (0, 0))
2439 CursorTexture = glGenTextures(1)
2440 glBindTexture(GL_TEXTURE_2D, CursorTexture)
2441 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tw, th, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.tostring())
2442 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
2443 CursorSX = w * PixelX
2444 CursorSY = h * PixelY
2445 CursorTX = w / float(tw)
2446 CursorTY = h / float(th)
2447 return True
2450 ##### CONTROL AND NAVIGATION ###################################################
2452 # update the applications' title bar
2453 def UpdateCaption(page=0, force=False):
2454 global CurrentCaption, CurrentOSDCaption, CurrentOSDPage, CurrentOSDStatus
2455 global CurrentOSDComment
2456 if (page == CurrentCaption) and not(force):
2457 return
2458 CurrentCaption = page
2459 caption = __title__
2460 if DocumentTitle:
2461 caption += " - " + DocumentTitle
2462 if page < 1:
2463 CurrentOSDCaption = ""
2464 CurrentOSDPage = ""
2465 CurrentOSDStatus = ""
2466 CurrentOSDComment = ""
2467 pygame.display.set_caption(caption, __title__)
2468 return
2469 CurrentOSDPage = "%d/%d" % (page, PageCount)
2470 caption = "%s (%s)" % (caption, CurrentOSDPage)
2471 title = GetPageProp(page, 'title') or GetPageProp(page, '_title')
2472 if title:
2473 caption += ": %s" % title
2474 CurrentOSDCaption = title
2475 else:
2476 CurrentOSDCaption = ""
2477 status = []
2478 if GetPageProp(page, 'skip', False):
2479 status.append("skipped: yes")
2480 if not GetPageProp(page, ('overview', '_overview'), True):
2481 status.append("on overview page: no")
2482 CurrentOSDStatus = ", ".join(status)
2483 CurrentOSDComment = GetPageProp(page, 'comment')
2484 pygame.display.set_caption(caption, __title__)
2486 # get next/previous page
2487 def GetNextPage(page, direction, keypressed=0):
2488 try_page = page
2489 while True:
2490 try_page += direction
2491 if try_page == page:
2492 return 0 # tried all pages, but none found
2493 if Wrap:
2494 if try_page < 1: try_page = PageCount
2495 if try_page > PageCount: try_page = 1
2496 elif (FadeToBlackAtEnd and keypressed):
2497 if try_page > PageCount:
2498 FadeMode(0.0)
2499 else:
2500 if try_page < 1 or try_page > PageCount:
2501 return 0 # start or end of presentation
2502 if not GetPageProp(try_page, 'skip', False):
2503 return try_page
2505 # pre-load the following page into Pnext/Tnext
2506 def PreloadNextPage(page):
2507 global Pnext, Tnext
2508 if (page < 1) or (page > PageCount):
2509 Pnext = 0
2510 return 0
2511 if page == Pnext:
2512 return 1
2513 RenderPage(page, Tnext)
2514 Pnext = page
2515 return 1
2517 # perform box fading; the fade animation time is mapped through func()
2518 def BoxFade(func):
2519 t0 = pygame.time.get_ticks()
2520 while 1:
2521 if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]): break
2522 t = (pygame.time.get_ticks() - t0) * 1.0 / BoxFadeDuration
2523 if t >= 1.0: break
2524 DrawCurrentPage(func(t))
2525 DrawCurrentPage(func(1.0))
2526 return 0
2528 # reset the timer
2529 def ResetTimer():
2530 global StartTime, PageEnterTime
2531 if TimeTracking and not(FirstPage):
2532 print "--- timer was reset here ---"
2533 StartTime = pygame.time.get_ticks()
2534 PageEnterTime = 0
2536 # start video playback
2537 def PlayVideo(video):
2538 global MPlayerPID, VideoPlaying
2539 if not video: return
2540 StopMPlayer()
2541 try:
2542 MPlayerPID = spawn(os.P_NOWAIT, \
2543 MPlayerPath, [MPlayerPath, "-quiet", \
2544 "-monitorpixelaspect", "1:1", "-autosync", "100"] + \
2545 MPlayerPlatformOptions + [ "-slave", \
2546 "-wid", str(pygame.display.get_wm_info()['window']), \
2547 FileNameEscape + video + FileNameEscape])
2548 if MPlayerColorKey:
2549 glClear(GL_COLOR_BUFFER_BIT)
2550 pygame.display.flip()
2551 VideoPlaying = True
2552 except OSError:
2553 MPlayerPID = 0
2555 # called each time a page is entered
2556 def PageEntered(update_time=True):
2557 global PageEnterTime, MPlayerPID, IsZoomed, WantStatus
2558 if update_time:
2559 PageEnterTime = pygame.time.get_ticks() - StartTime
2560 IsZoomed = False # no, we don't have a pre-zoomed image right now
2561 WantStatus = False # don't show status unless it's changed interactively
2562 timeout = AutoAdvance
2563 shown = GetPageProp(Pcurrent, '_shown', 0)
2564 if not shown:
2565 timeout = GetPageProp(Pcurrent, 'timeout', timeout)
2566 video = GetPageProp(Pcurrent, 'video')
2567 sound = GetPageProp(Pcurrent, 'sound')
2568 PlayVideo(video)
2569 if sound and not(video):
2570 StopMPlayer()
2571 try:
2572 MPlayerPID = spawn(os.P_NOWAIT, \
2573 MPlayerPath, [MPlayerPath, "-quiet", "-really-quiet", \
2574 FileNameEscape + sound + FileNameEscape])
2575 except OSError:
2576 MPlayerPID = 0
2577 SafeCall(GetPageProp(Pcurrent, 'OnEnterOnce'))
2578 SafeCall(GetPageProp(Pcurrent, 'OnEnter'))
2579 if timeout: pygame.time.set_timer(USEREVENT_PAGE_TIMEOUT, timeout)
2580 SetPageProp(Pcurrent, '_shown', shown + 1)
2582 # called each time a page is left
2583 def PageLeft(overview=False):
2584 global FirstPage, LastPage, WantStatus
2585 WantStatus = False
2586 if not overview:
2587 if GetTristatePageProp(Pcurrent, 'reset'):
2588 ResetTimer()
2589 FirstPage = False
2590 LastPage = Pcurrent
2591 if GetPageProp(Pcurrent, '_shown', 0) == 1:
2592 SafeCall(GetPageProp(Pcurrent, 'OnLeaveOnce'))
2593 SafeCall(GetPageProp(Pcurrent, 'OnLeave'))
2594 if TimeTracking:
2595 t1 = pygame.time.get_ticks() - StartTime
2596 dt = (t1 - PageEnterTime + 500) / 1000
2597 if overview:
2598 p = "over"
2599 else:
2600 p = "%4d" % Pcurrent
2601 print "%s%9s%9s%9s" % (p, FormatTime(dt), \
2602 FormatTime(PageEnterTime / 1000), \
2603 FormatTime(t1 / 1000))
2605 # perform a transition to a specified page
2606 def TransitionTo(page):
2607 global Pcurrent, Pnext, Tcurrent, Tnext
2608 global PageCount, Marking, Tracing, Panning, TransitionRunning
2610 # first, stop the auto-timer
2611 pygame.time.set_timer(USEREVENT_PAGE_TIMEOUT, 0)
2613 # invalid page? go away
2614 if not PreloadNextPage(page):
2615 return 0
2617 # notify that the page has been left
2618 PageLeft()
2620 # box fade-out
2621 if GetPageProp(Pcurrent, 'boxes') or Tracing:
2622 skip = BoxFade(lambda t: 1.0 - t)
2623 else:
2624 skip = 0
2626 # some housekeeping
2627 Marking = False
2628 Tracing = False
2629 UpdateCaption(page)
2631 # check if the transition is valid
2632 tpage = min(Pcurrent, Pnext)
2633 if 'transition' in PageProps[tpage]:
2634 tkey = 'transition'
2635 else:
2636 tkey = '_transition'
2637 trans = PageProps[tpage][tkey]
2638 if trans is None:
2639 transtime = 0
2640 else:
2641 transtime = GetPageProp(tpage, 'transtime', TransitionDuration)
2642 try:
2643 dummy = trans.__class__
2644 except AttributeError:
2645 # ah, gotcha! the transition is not yet intantiated!
2646 trans = trans()
2647 PageProps[tpage][tkey] = trans
2649 # backward motion? then swap page buffers now
2650 backward = (Pnext < Pcurrent)
2651 if backward:
2652 Pcurrent, Pnext = (Pnext, Pcurrent)
2653 Tcurrent, Tnext = (Tnext, Tcurrent)
2655 # transition animation
2656 if not(skip) and transtime:
2657 transtime = 1.0 / transtime
2658 TransitionRunning = True
2659 t0 = pygame.time.get_ticks()
2660 while not(VideoPlaying):
2661 if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]):
2662 skip = 1
2663 break
2664 t = (pygame.time.get_ticks() - t0) * transtime
2665 if t >= 1.0: break
2666 if backward: t = 1.0 - t
2667 glEnable(TextureTarget)
2668 trans.render(t)
2669 DrawOverlays()
2670 pygame.display.flip()
2671 TransitionRunning = False
2673 # forward motion => swap page buffers now
2674 if not backward:
2675 Pcurrent, Pnext = (Pnext, Pcurrent)
2676 Tcurrent, Tnext = (Tnext, Tcurrent)
2678 # box fade-in
2679 if not(skip) and GetPageProp(Pcurrent, 'boxes'): BoxFade(lambda t: t)
2681 # finally update the screen and preload the next page
2682 DrawCurrentPage() # I do that twice because for some strange reason, the
2683 PageEntered()
2684 if not PreloadNextPage(GetNextPage(Pcurrent, 1)):
2685 PreloadNextPage(GetNextPage(Pcurrent, -1))
2686 return 1
2688 # zoom mode animation
2689 def ZoomAnimation(targetx, targety, func):
2690 global ZoomX0, ZoomY0, ZoomArea
2691 t0 = pygame.time.get_ticks()
2692 while True:
2693 if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]): break
2694 t = (pygame.time.get_ticks() - t0)* 1.0 / ZoomDuration
2695 if t >= 1.0: break
2696 t = func(t)
2697 t = (2.0 - t) * t
2698 ZoomX0 = targetx * t
2699 ZoomY0 = targety * t
2700 ZoomArea = 1.0 - 0.5 * t
2701 DrawCurrentPage()
2702 t = func(1.0)
2703 ZoomX0 = targetx * t
2704 ZoomY0 = targety * t
2705 ZoomArea = 1.0 - 0.5 * t
2706 GenerateSpotMesh()
2707 DrawCurrentPage()
2709 # enter zoom mode
2710 def EnterZoomMode(targetx, targety):
2711 global ZoomMode, IsZoomed, ZoomWarningIssued
2712 ZoomAnimation(targetx, targety, lambda t: t)
2713 ZoomMode = True
2714 if TextureTarget != GL_TEXTURE_2D:
2715 if not ZoomWarningIssued:
2716 print >>sys.stderr, "Sorry, but I can't increase the detail level in zoom mode any further when"
2717 print >>sys.stderr, "GL_ARB_texture_rectangle is used. Please try running Accentuate with the"
2718 print >>sys.stderr, "'-e' parameter. If a modern nVidia or ATI graphics card is used, a driver"
2719 print >>sys.stderr, "update may also fix the problem."
2720 ZoomWarningIssued = True
2721 return
2722 if not IsZoomed:
2723 glBindTexture(TextureTarget, Tcurrent)
2724 try:
2725 glTexImage2D(TextureTarget, 0, 3, TexWidth * 2, TexHeight * 2, 0, \
2726 GL_RGB, GL_UNSIGNED_BYTE, PageImage(Pcurrent, True))
2727 except GLerror:
2728 if not ZoomWarningIssued:
2729 print >>sys.stderr, "Sorry, but I can't increase the detail level in zoom mode any further, because"
2730 print >>sys.stderr, "your OpenGL implementation does not support that. Either the texture memory is"
2731 print >>sys.stderr, "exhausted, or there is no support for large textures (%dx%d). If you really" % (TexWidth * 2, TexHeight * 2)
2732 print >>sys.stderr, "need high-res zooming, please try to run Accentuate in a smaller resolution"
2733 print >>sys.stderr, "using the -g command-line option."
2734 ZoomWarningIssued = True
2735 return
2736 DrawCurrentPage()
2737 IsZoomed = True
2739 # leave zoom mode (if enabled)
2740 def LeaveZoomMode():
2741 global ZoomMode
2742 if not ZoomMode: return
2743 ZoomAnimation(ZoomX0, ZoomY0, lambda t: 1.0 - t)
2744 ZoomMode = False
2745 Panning = False
2747 # increment/decrement spot radius
2748 def IncrementSpotSize(delta):
2749 global SpotRadius
2750 if not Tracing:
2751 return
2752 SpotRadius = max(SpotRadius + delta, 8)
2753 GenerateSpotMesh()
2754 DrawCurrentPage()
2756 # post-initialize the page transitions
2757 def PrepareTransitions():
2758 Unspecified = 0xAFFED00F
2759 # STEP 1: randomly assign transitions where the user didn't specify them
2760 cnt = sum([1 for page in xrange(1, PageCount + 1) \
2761 if GetPageProp(page, 'transition', Unspecified) == Unspecified])
2762 newtrans = ((cnt / len(AvailableTransitions) + 1) * AvailableTransitions)[:cnt]
2763 random.shuffle(newtrans)
2764 for page in xrange(1, PageCount + 1):
2765 if GetPageProp(page, 'transition', Unspecified) == Unspecified:
2766 SetPageProp(page, '_transition', newtrans.pop())
2767 # STEP 2: instantiate transitions
2768 for page in PageProps:
2769 for key in ('transition', '_transition'):
2770 if not key in PageProps[page]:
2771 continue
2772 trans = PageProps[page][key]
2773 if trans is not None:
2774 PageProps[page][key] = trans()
2776 # update timer values and screen timer
2777 def TimerTick():
2778 global CurrentTime, ProgressBarPos
2779 redraw = False
2780 newtime = (pygame.time.get_ticks() - StartTime) * 0.001
2781 if EstimatedDuration:
2782 newpos = int(ScreenWidth * newtime / EstimatedDuration)
2783 if newpos != ProgressBarPos:
2784 redraw = True
2785 ProgressBarPos = newpos
2786 newtime = int(newtime)
2787 if TimeDisplay and (CurrentTime != newtime):
2788 redraw = True
2789 CurrentTime = newtime
2790 return redraw
2792 # set cursor visibility
2793 def SetCursor(visible):
2794 global CursorVisible
2795 CursorVisible = visible
2796 if not CursorImage:
2797 pygame.mouse.set_visible(visible)
2799 # shortcut handling
2800 def IsValidShortcutKey(key):
2801 return ((key >= K_a) and (key <= K_z)) \
2802 or ((key >= K_0) and (key <= K_9)) \
2803 or ((key >= K_F1) and (key <= K_F12))
2804 def FindShortcut(shortcut):
2805 for page, props in PageProps.iteritems():
2806 try:
2807 check = props['shortcut']
2808 if type(check) != types.StringType:
2809 check = int(check)
2810 elif (len(check) > 1) and (check[0] in "Ff"):
2811 check = K_F1 - 1 + int(check[1:])
2812 else:
2813 check = ord(check.lower())
2814 except (KeyError, TypeError, ValueError):
2815 continue
2816 if check == shortcut:
2817 return page
2818 return None
2819 def AssignShortcut(page, key):
2820 old_page = FindShortcut(key)
2821 if old_page:
2822 del PageProps[old_page]['shortcut']
2823 if key < 127:
2824 shortcut = chr(key)
2825 elif (key >= K_F1) and (key <= K_F15):
2826 shortcut = "F%d" % (key - K_F1 + 1)
2827 else:
2828 shortcut = int(key)
2829 SetPageProp(page, 'shortcut', shortcut)
2832 ##### OVERVIEW MODE ############################################################
2834 def UpdateOverviewTexture():
2835 global OverviewNeedUpdate
2836 glBindTexture(TextureTarget, Tnext)
2837 Loverview.acquire()
2838 try:
2839 glTexImage2D(TextureTarget, 0, 3, TexWidth, TexHeight, 0, \
2840 GL_RGB, GL_UNSIGNED_BYTE, OverviewImage.tostring())
2841 finally:
2842 Loverview.release()
2843 OverviewNeedUpdate = False
2845 # draw the overview page
2846 def DrawOverview():
2847 if VideoPlaying: return
2848 glClear(GL_COLOR_BUFFER_BIT)
2849 glDisable(GL_BLEND)
2850 glEnable(TextureTarget)
2851 glBindTexture(TextureTarget, Tnext)
2852 glColor3ub(192, 192, 192)
2853 DrawFullQuad()
2855 pos = OverviewPos(OverviewSelection)
2856 X0 = PixelX * pos[0]
2857 Y0 = PixelY * pos[1]
2858 X1 = PixelX * (pos[0] + OverviewCellX)
2859 Y1 = PixelY * (pos[1] + OverviewCellY)
2860 glColor3d(1.0, 1.0, 1.0)
2861 glBegin(GL_QUADS)
2862 DrawPoint(X0, Y0)
2863 DrawPoint(X1, Y0)
2864 DrawPoint(X1, Y1)
2865 DrawPoint(X0, Y1)
2866 glEnd()
2868 DrawOSDEx(OSDTitlePos, CurrentOSDCaption)
2869 DrawOSDEx(OSDPagePos, CurrentOSDPage)
2870 DrawOSDEx(OSDStatusPos, CurrentOSDStatus)
2871 DrawOverlays()
2872 pygame.display.flip()
2874 # overview zoom effect, time mapped through func
2875 def OverviewZoom(func):
2876 global TransitionRunning
2877 pos = OverviewPos(OverviewSelection)
2878 X0 = PixelX * (pos[0] + OverviewBorder)
2879 Y0 = PixelY * (pos[1] + OverviewBorder)
2880 X1 = PixelX * (pos[0] - OverviewBorder + OverviewCellX)
2881 Y1 = PixelY * (pos[1] - OverviewBorder + OverviewCellY)
2883 TransitionRunning = True
2884 t0 = pygame.time.get_ticks()
2885 while not(VideoPlaying):
2886 t = (pygame.time.get_ticks() - t0) * 1.0 / ZoomDuration
2887 if t >= 1.0: break
2888 t = func(t)
2889 t1 = t*t
2890 t = 1.0 - t1
2892 zoom = (t * (X1 - X0) + t1) / (X1 - X0)
2893 OX = zoom * (t * X0 - X0) - (zoom - 1.0) * t * X0
2894 OY = zoom * (t * Y0 - Y0) - (zoom - 1.0) * t * Y0
2895 OX = t * X0 - zoom * X0
2896 OY = t * Y0 - zoom * Y0
2898 glDisable(GL_BLEND)
2899 glEnable(TextureTarget)
2900 glBindTexture(TextureTarget, Tnext)
2901 glBegin(GL_QUADS)
2902 glColor3ub(192, 192, 192)
2903 glTexCoord2d( 0.0, 0.0); glVertex2d(OX, OY)
2904 glTexCoord2d(TexMaxS, 0.0); glVertex2d(OX + zoom, OY)
2905 glTexCoord2d(TexMaxS, TexMaxT); glVertex2d(OX + zoom, OY + zoom)
2906 glTexCoord2d( 0.0, TexMaxT); glVertex2d(OX, OY + zoom)
2907 glColor3ub(255, 255, 255)
2908 glTexCoord2d(X0 * TexMaxS, Y0 * TexMaxT); glVertex2d(OX + X0*zoom, OY + Y0 * zoom)
2909 glTexCoord2d(X1 * TexMaxS, Y0 * TexMaxT); glVertex2d(OX + X1*zoom, OY + Y0 * zoom)
2910 glTexCoord2d(X1 * TexMaxS, Y1 * TexMaxT); glVertex2d(OX + X1*zoom, OY + Y1 * zoom)
2911 glTexCoord2d(X0 * TexMaxS, Y1 * TexMaxT); glVertex2d(OX + X0*zoom, OY + Y1 * zoom)
2912 glEnd()
2914 EnableAlphaBlend()
2915 glBindTexture(TextureTarget, Tcurrent)
2916 glColor4d(1.0, 1.0, 1.0, 1.0 - t * t * t)
2917 glBegin(GL_QUADS)
2918 glTexCoord2d( 0.0, 0.0); glVertex2d(t * X0, t * Y0)
2919 glTexCoord2d(TexMaxS, 0.0); glVertex2d(t * X1 + t1, t * Y0)
2920 glTexCoord2d(TexMaxS, TexMaxT); glVertex2d(t * X1 + t1, t * Y1 + t1)
2921 glTexCoord2d( 0.0, TexMaxT); glVertex2d(t * X0, t * Y1 + t1)
2922 glEnd()
2924 DrawOSDEx(OSDTitlePos, CurrentOSDCaption, alpha_factor=t)
2925 DrawOSDEx(OSDPagePos, CurrentOSDPage, alpha_factor=t)
2926 DrawOSDEx(OSDStatusPos, CurrentOSDStatus, alpha_factor=t)
2927 DrawOverlays()
2928 pygame.display.flip()
2929 TransitionRunning = False
2931 # overview keyboard navigation
2932 def OverviewKeyboardNav(delta):
2933 global OverviewSelection
2934 dest = OverviewSelection + delta
2935 if (dest >= OverviewPageCount) or (dest < 0):
2936 return
2937 OverviewSelection = dest
2938 x, y = OverviewPos(OverviewSelection)
2939 pygame.mouse.set_pos((x + (OverviewCellX / 2), y + (OverviewCellY / 2)))
2941 # overview mode PageProp toggle
2942 def OverviewTogglePageProp(prop, default):
2943 if (OverviewSelection < 0) or (OverviewSelection >= len(OverviewPageMap)):
2944 return
2945 page = OverviewPageMap[OverviewSelection]
2946 SetPageProp(page, prop, not(GetPageProp(page, prop, default)))
2947 UpdateCaption(page, force=True)
2948 DrawOverview()
2950 # overview event handler
2951 def HandleOverviewEvent(event):
2952 global OverviewSelection, TimeDisplay
2954 if event.type == QUIT:
2955 PageLeft(overview=True)
2956 Quit()
2957 elif event.type == VIDEOEXPOSE:
2958 DrawOverview()
2960 elif event.type == KEYDOWN:
2961 if (event.key == K_ESCAPE) or (event.unicode == u'q'):
2962 pygame.event.post(pygame.event.Event(QUIT))
2963 elif event.unicode == u'f':
2964 SetFullscreen(not Fullscreen)
2965 elif event.unicode == u't':
2966 TimeDisplay = not(TimeDisplay)
2967 DrawOverview()
2968 elif event.unicode == u'r':
2969 ResetTimer()
2970 if TimeDisplay: DrawOverview()
2971 elif event.unicode == u's':
2972 SaveInfoScript(InfoScriptPath)
2973 elif event.unicode == u'o':
2974 OverviewTogglePageProp('overview', GetPageProp(Pcurrent, '_overview', True))
2975 elif event.unicode == u'i':
2976 OverviewTogglePageProp('skip', False)
2977 elif event.key == K_UP: OverviewKeyboardNav(-OverviewGridSize)
2978 elif event.key == K_LEFT: OverviewKeyboardNav(-1)
2979 elif event.key == K_RIGHT: OverviewKeyboardNav(+1)
2980 elif event.key == K_DOWN: OverviewKeyboardNav(+OverviewGridSize)
2981 elif event.key == K_TAB:
2982 OverviewSelection = -1
2983 return 0
2984 elif event.key in (K_RETURN, K_KP_ENTER):
2985 return 0
2986 elif IsValidShortcutKey(event.key):
2987 if event.mod & KMOD_SHIFT:
2988 try:
2989 AssignShortcut(OverviewPageMap[OverviewSelection], event.key)
2990 except IndexError:
2991 pass # no valid page selected
2992 else:
2993 # load shortcut
2994 page = FindShortcut(event.key)
2995 if page:
2996 OverviewSelection = OverviewPageMapInv[page]
2997 x, y = OverviewPos(OverviewSelection)
2998 pygame.mouse.set_pos((x + (OverviewCellX / 2), \
2999 y + (OverviewCellY / 2)))
3000 DrawOverview()
3002 elif event.type == MOUSEBUTTONUP:
3003 if event.button == 1:
3004 return 0
3005 elif event.button in (2, 3):
3006 OverviewSelection = -1
3007 return 0
3009 elif event.type == MOUSEMOTION:
3010 pygame.event.clear(MOUSEMOTION)
3011 # mouse move in fullscreen mode -> show mouse cursor and reset mouse timer
3012 if Fullscreen:
3013 pygame.time.set_timer(USEREVENT_HIDE_MOUSE, MouseHideDelay)
3014 SetCursor(True)
3015 # determine highlighted page
3016 OverviewSelection = \
3017 int((event.pos[0] - OverviewOfsX) / OverviewCellX) + \
3018 int((event.pos[1] - OverviewOfsY) / OverviewCellY) * OverviewGridSize
3019 if (OverviewSelection < 0) or (OverviewSelection >= len(OverviewPageMap)):
3020 UpdateCaption(0)
3021 else:
3022 UpdateCaption(OverviewPageMap[OverviewSelection])
3023 DrawOverview()
3025 elif event.type == USEREVENT_HIDE_MOUSE:
3026 # mouse timer event -> hide fullscreen cursor
3027 pygame.time.set_timer(USEREVENT_HIDE_MOUSE, 0)
3028 SetCursor(False)
3029 DrawOverview()
3031 return 1
3033 # overview mode entry/loop/exit function
3034 def DoOverview():
3035 global Pcurrent, Pnext, Tcurrent, Tnext, Tracing, OverviewSelection
3036 global PageEnterTime, OverviewMode
3038 pygame.time.set_timer(USEREVENT_PAGE_TIMEOUT, 0)
3039 PageLeft()
3040 UpdateOverviewTexture()
3042 if GetPageProp(Pcurrent, 'boxes') or Tracing:
3043 BoxFade(lambda t: 1.0 - t)
3044 Tracing = False
3045 OverviewSelection = OverviewPageMapInv[Pcurrent]
3047 OverviewMode = True
3048 OverviewZoom(lambda t: 1.0 - t)
3049 DrawOverview()
3050 PageEnterTime = pygame.time.get_ticks() - StartTime
3051 while True:
3052 event = pygame.event.poll()
3053 if event.type == NOEVENT:
3054 force_update = OverviewNeedUpdate
3055 if OverviewNeedUpdate:
3056 UpdateOverviewTexture()
3057 if TimerTick() or force_update:
3058 DrawOverview()
3059 pygame.time.wait(20)
3060 elif not HandleOverviewEvent(event):
3061 break
3062 PageLeft(overview=True)
3064 if (OverviewSelection < 0) or (OverviewSelection >= OverviewPageCount):
3065 OverviewSelection = OverviewPageMapInv[Pcurrent]
3066 Pnext = Pcurrent
3067 else:
3068 Pnext = OverviewPageMap[OverviewSelection]
3069 if Pnext != Pcurrent:
3070 Pcurrent = Pnext
3071 RenderPage(Pcurrent, Tcurrent)
3072 UpdateCaption(Pcurrent)
3073 OverviewZoom(lambda t: t)
3074 OverviewMode = False
3075 DrawCurrentPage()
3077 if GetPageProp(Pcurrent, 'boxes'):
3078 BoxFade(lambda t: t)
3079 PageEntered()
3080 if not PreloadNextPage(GetNextPage(Pcurrent, 1)):
3081 PreloadNextPage(GetNextPage(Pcurrent, -1))
3084 ##### EVENT HANDLING ###########################################################
3086 # set fullscreen mode
3087 def SetFullscreen(fs, do_init=True):
3088 global Fullscreen
3090 # let pygame do the real work
3091 if do_init:
3092 if fs == Fullscreen: return
3093 if not pygame.display.toggle_fullscreen(): return
3094 Fullscreen=fs
3096 # redraw the current page (pygame is too lazy to send an expose event ...)
3097 DrawCurrentPage()
3099 # show cursor and set auto-hide timer
3100 if fs:
3101 pygame.time.set_timer(USEREVENT_HIDE_MOUSE, MouseHideDelay)
3102 else:
3103 pygame.time.set_timer(USEREVENT_HIDE_MOUSE, 0)
3104 SetCursor(True)
3106 # PageProp toggle
3107 def TogglePageProp(prop, default):
3108 global WantStatus
3109 SetPageProp(Pcurrent, prop, not(GetPageProp(Pcurrent, prop, default)))
3110 UpdateCaption(Pcurrent, force=True)
3111 WantStatus = True
3112 DrawCurrentPage()
3114 # main event handling function
3115 def HandleEvent(event):
3116 global HaveMark, ZoomMode, Marking, Tracing, Panning, SpotRadius, FileStats
3117 global MarkUL, MarkLR, MouseDownX, MouseDownY, PanAnchorX, PanAnchorY
3118 global ZoomX0, ZoomY0, RTrunning, RTrestart, StartTime, PageEnterTime
3119 global CurrentTime, TimeDisplay, TimeTracking, ProgressBarPos
3121 if event.type == QUIT:
3122 PageLeft()
3123 Quit()
3124 elif event.type == VIDEOEXPOSE:
3125 DrawCurrentPage()
3127 elif event.type == KEYDOWN:
3128 if VideoPlaying:
3129 StopMPlayer()
3130 DrawCurrentPage()
3131 elif (event.key == K_ESCAPE) or (event.unicode == u'q'):
3132 pygame.event.post(pygame.event.Event(QUIT))
3133 elif event.unicode == u'f':
3134 SetFullscreen(not Fullscreen)
3135 elif (event.key == K_TAB) and (event.mod & KMOD_ALT) and Fullscreen:
3136 SetFullscreen(False)
3137 elif event.unicode == u's':
3138 SaveInfoScript(InfoScriptPath)
3139 elif event.unicode == u'z': # handle QWERTY and QWERTZ keyboards
3140 if ZoomMode:
3141 LeaveZoomMode()
3142 else:
3143 tx, ty = MouseToScreen(pygame.mouse.get_pos())
3144 EnterZoomMode(0.5 * tx, 0.5 * ty)
3145 elif event.unicode == u'b':
3146 FadeMode(0.0)
3147 elif event.unicode == u'w':
3148 FadeMode(1.0)
3149 elif event.unicode == u't':
3150 TimeDisplay = not(TimeDisplay)
3151 DrawCurrentPage()
3152 if TimeDisplay and not(TimeTracking) and FirstPage:
3153 print >>sys.stderr, "Time tracking mode enabled."
3154 TimeTracking = True
3155 print "page duration enter leave"
3156 print "---- -------- -------- --------"
3157 elif event.unicode == u'r':
3158 ResetTimer()
3159 if TimeDisplay: DrawCurrentPage()
3160 elif event.unicode == u'l':
3161 TransitionTo(LastPage)
3162 elif event.unicode == u'o':
3163 TogglePageProp('overview', GetPageProp(Pcurrent, '_overview', True))
3164 elif event.unicode == u'i':
3165 TogglePageProp('skip', False)
3166 elif event.key == K_TAB:
3167 LeaveZoomMode()
3168 DoOverview()
3169 elif event.key in (32, K_DOWN, K_RIGHT, K_PAGEDOWN):
3170 LeaveZoomMode()
3171 TransitionTo(GetNextPage(Pcurrent, 1, 1))
3172 elif event.key in (K_BACKSPACE, K_UP, K_LEFT, K_PAGEUP):
3173 LeaveZoomMode()
3174 TransitionTo(GetNextPage(Pcurrent, -1, 1))
3175 elif event.key == K_HOME:
3176 if Pcurrent != 1:
3177 TransitionTo(1)
3178 elif event.key == K_END:
3179 if Pcurrent != PageCount:
3180 TransitionTo(PageCount)
3181 elif event.key in (K_RETURN, K_KP_ENTER):
3182 if not(GetPageProp(Pcurrent, 'boxes')) and Tracing:
3183 BoxFade(lambda t: 1.0 - t)
3184 Tracing = not(Tracing)
3185 if not(GetPageProp(Pcurrent, 'boxes')) and Tracing:
3186 BoxFade(lambda t: t)
3187 elif event.unicode == u'+':
3188 IncrementSpotSize(+8)
3189 elif event.unicode == u'-':
3190 IncrementSpotSize(-8)
3191 elif event.unicode == u'[':
3192 SetGamma(new_gamma=Gamma / GammaStep)
3193 elif event.unicode == u']':
3194 SetGamma(new_gamma=Gamma * GammaStep)
3195 elif event.unicode == u'{':
3196 SetGamma(new_black=BlackLevel - BlackLevelStep)
3197 elif event.unicode == u'}':
3198 SetGamma(new_black=BlackLevel + BlackLevelStep)
3199 elif event.unicode == u'\\':
3200 SetGamma(1.0, 0)
3201 else:
3202 keyfunc = GetPageProp(Pcurrent, 'keys', {}).get(event.unicode, None)
3203 if keyfunc:
3204 SafeCall(keyfunc)
3205 elif IsValidShortcutKey(event.key):
3206 if event.mod & KMOD_SHIFT:
3207 AssignShortcut(Pcurrent, event.key)
3208 else:
3209 # load keyboard shortcut
3210 page = FindShortcut(event.key)
3211 if page and (page != Pcurrent):
3212 TransitionTo(page)
3214 elif event.type == MOUSEBUTTONDOWN:
3215 if VideoPlaying:
3216 Marking = False
3217 Panning = False
3218 return
3219 MouseDownX, MouseDownY = event.pos
3220 if event.button == 1:
3221 MarkUL = MarkLR = MouseToScreen(event.pos)
3222 elif (event.button == 3) and ZoomMode:
3223 PanAnchorX = ZoomX0
3224 PanAnchorY = ZoomY0
3225 elif event.button == 4:
3226 IncrementSpotSize(+8)
3227 elif event.button == 5:
3228 IncrementSpotSize(-8)
3230 elif event.type == MOUSEBUTTONUP:
3231 if VideoPlaying:
3232 StopMPlayer()
3233 DrawCurrentPage()
3234 Marking = False
3235 Panning = False
3236 return
3237 if event.button == 2:
3238 LeaveZoomMode()
3239 DoOverview()
3240 return
3241 if event.button == 1:
3242 if Marking:
3243 # left mouse button released in marking mode -> stop box marking
3244 Marking = False
3245 # reject too small boxes
3246 if (abs(MarkUL[0] - MarkLR[0]) > 0.04) \
3247 and (abs(MarkUL[1] - MarkLR[1]) > 0.03):
3248 boxes = GetPageProp(Pcurrent, 'boxes', [])
3249 oldboxcount = len(boxes)
3250 boxes.append(NormalizeRect(MarkUL[0], MarkUL[1], MarkLR[0], MarkLR[1]))
3251 SetPageProp(Pcurrent, 'boxes', boxes)
3252 if not(oldboxcount) and not(Tracing):
3253 BoxFade(lambda t: t)
3254 DrawCurrentPage()
3255 else:
3256 # left mouse button released, but no marking
3257 LeaveZoomMode()
3258 dest = GetNextPage(Pcurrent, 1)
3259 x, y = event.pos
3260 for valid, target, x0, y0, x1, y1 in GetPageProp(Pcurrent, '_href', []):
3261 if valid and (x >= x0) and (x < x1) and (y >= y0) and (y < y1):
3262 dest = target
3263 break
3264 if type(dest) == types.IntType:
3265 TransitionTo(dest)
3266 else:
3267 RunURL(dest)
3268 if (event.button == 3) and not(Panning):
3269 # right mouse button -> check if a box has to be killed
3270 boxes = GetPageProp(Pcurrent, 'boxes', [])
3271 x, y = MouseToScreen(event.pos)
3272 try:
3273 # if a box is already present around the clicked position, kill it
3274 idx = FindBox(x, y, boxes)
3275 if (len(boxes) == 1) and not(Tracing):
3276 BoxFade(lambda t: 1.0 - t)
3277 del boxes[idx]
3278 SetPageProp(Pcurrent, 'boxes', boxes)
3279 DrawCurrentPage()
3280 except ValueError:
3281 # no box present -> go to previous page
3282 LeaveZoomMode()
3283 TransitionTo(GetNextPage(Pcurrent, -1))
3284 Panning = False
3286 elif event.type == MOUSEMOTION:
3287 pygame.event.clear(MOUSEMOTION)
3288 # mouse move in fullscreen mode -> show mouse cursor and reset mouse timer
3289 if Fullscreen:
3290 pygame.time.set_timer(USEREVENT_HIDE_MOUSE, MouseHideDelay)
3291 SetCursor(True)
3292 # don't react on mouse input during video playback
3293 if VideoPlaying: return
3294 # activate marking if mouse is moved away far enough
3295 if event.buttons[0] and not(Marking):
3296 x, y = event.pos
3297 if (abs(x - MouseDownX) > 4) and (abs(y - MouseDownY) > 4):
3298 Marking = True
3299 # mouse move while marking -> update marking box
3300 if Marking:
3301 MarkLR = MouseToScreen(event.pos)
3302 # mouse move while RMB is pressed -> panning
3303 if event.buttons[2] and ZoomMode:
3304 x, y = event.pos
3305 if not(Panning) and (abs(x - MouseDownX) > 4) and (abs(y - MouseDownY) > 4):
3306 Panning = True
3307 ZoomX0 = PanAnchorX + (MouseDownX - x) * ZoomArea / ScreenWidth
3308 ZoomY0 = PanAnchorY + (MouseDownY - y) * ZoomArea / ScreenHeight
3309 ZoomX0 = min(max(ZoomX0, 0.0), 1.0 - ZoomArea)
3310 ZoomY0 = min(max(ZoomY0, 0.0), 1.0 - ZoomArea)
3311 # if anything changed, redraw the page
3312 if Marking or Tracing or event.buttons[2] or (CursorImage and CursorVisible):
3313 DrawCurrentPage()
3315 elif event.type == USEREVENT_HIDE_MOUSE:
3316 # mouse timer event -> hide fullscreen cursor
3317 pygame.time.set_timer(USEREVENT_HIDE_MOUSE, 0)
3318 SetCursor(False)
3319 DrawCurrentPage()
3321 elif event.type == USEREVENT_PAGE_TIMEOUT:
3322 TransitionTo(GetNextPage(Pcurrent, 1))
3324 elif event.type == USEREVENT_POLL_FILE:
3325 dirty = False
3326 for f in FileProps:
3327 if my_stat(f) != GetFileProp(f, 'stat'):
3328 dirty = True
3329 break
3330 if dirty:
3331 # first, check if the new file is valid
3332 if not os.path.isfile(GetPageProp(Pcurrent, '_file')):
3333 return
3334 # invalidate everything we used to know about the input files
3335 InvalidateCache()
3336 for props in PageProps.itervalues():
3337 for prop in ('_overview_rendered', '_box', '_href'):
3338 if prop in props: del props[prop]
3339 LoadInfoScript()
3340 # force a transition to the current page, reloading it
3341 Pnext=-1
3342 TransitionTo(Pcurrent)
3343 # restart the background renderer thread. this is not completely safe,
3344 # i.e. there's a small chance that we fail to restart the thread, but
3345 # this isn't critical
3346 if CacheMode and BackgroundRendering:
3347 if RTrunning:
3348 RTrestart = True
3349 else:
3350 RTrunning = True
3351 thread.start_new_thread(RenderThread, (Pcurrent, Pnext))
3353 elif event.type == USEREVENT_TIMER_UPDATE:
3354 if TimerTick():
3355 DrawCurrentPage()
3358 ##### FILE LIST GENERATION #####################################################
3360 def IsImageFileName(name):
3361 return os.path.splitext(name)[1].lower() in \
3362 (".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp", ".ppm", ".pgm")
3363 def IsPlayable(name):
3364 return IsImageFileName(name) or name.lower().endswith(".pdf") or os.path.isdir(name)
3366 def AddFile(name, title=None):
3367 global FileList, FileName
3369 if os.path.isfile(name):
3370 FileList.append(name)
3371 if title: SetFileProp(name, 'title', title)
3373 elif os.path.isdir(name):
3374 images = [os.path.join(name, f) for f in os.listdir(name) if IsImageFileName(f)]
3375 images.sort(lambda a, b: cmp(a.lower(), b.lower()))
3376 if not images:
3377 print >>sys.stderr, "Warning: no image files in directory `%s'" % name
3378 for img in images: AddFile(img)
3380 elif name.startswith('@') and os.path.isfile(name[1:]):
3381 name = name[1:]
3382 dirname = os.path.dirname(name)
3383 try:
3384 f = file(name, "r")
3385 next_title = None
3386 for line in f:
3387 line = [part.strip() for part in line.split('#', 1)]
3388 if len(line) == 1:
3389 subfile = line[0]
3390 title = None
3391 else:
3392 subfile, title = line
3393 if subfile:
3394 AddFile(os.path.normpath(os.path.join(dirname, subfile)), title)
3395 f.close()
3396 except IOError:
3397 print >>sys.stderr, "Error: cannot read list file `%s'" % name
3398 if not FileName:
3399 FileName = name
3400 else:
3401 FileName = ""
3403 else:
3404 files = list(filter(IsPlayable, glob.glob(name)))
3405 if files:
3406 for f in files: AddFile(f)
3407 else:
3408 print >>sys.stderr, "Error: input file `%s' not found" % name
3411 ##### INITIALIZATION ###########################################################
3413 def main():
3414 global ScreenWidth, ScreenHeight, TexWidth, TexHeight, TexSize, LogoImage
3415 global TexMaxS, TexMaxT, MeshStepX, MeshStepY, EdgeX, EdgeY, PixelX, PixelY
3416 global OverviewGridSize, OverviewCellX, OverviewCellY
3417 global OverviewOfsX, OverviewOfsY, OverviewImage, OverviewPageCount
3418 global OverviewPageMap, OverviewPageMapInv, FileName, FileList, PageCount
3419 global DocumentTitle, PageProps, LogoTexture, OSDFont
3420 global Pcurrent, Pnext, Tcurrent, Tnext, InitialPage
3421 global CacheFile, CacheFileName
3422 global Extensions, AllowExtensions, TextureTarget, PAR, DAR, TempFileName
3423 global BackgroundRendering, FileStats, RTrunning, RTrestart, StartTime
3424 global CursorImage, CursorVisible, InfoScriptPath
3426 # allocate temporary file
3427 TempFileName = tempfile.mktemp(prefix="Accentuate-", suffix="_tmp")
3429 # some input guesswork
3430 DocumentTitle = os.path.splitext(os.path.split(FileName)[1])[0]
3431 if FileName and not(FileList):
3432 AddFile(FileName)
3433 if not(FileName) and (len(FileList) == 1):
3434 FileName = FileList[0]
3436 # fill the page list
3437 PageCount = 0
3438 for name in FileList:
3439 ispdf = name.lower().endswith(".pdf")
3440 if ispdf:
3441 # PDF input -> try to pre-parse the PDF file
3442 pages = 0
3443 # phase 1: internal PDF parser
3444 try:
3445 pages, pdf_width, pdf_height = analyze_pdf(name)
3446 if Rotation & 1:
3447 pdf_width, pdf_height = (pdf_height, pdf_width)
3448 res = min(ScreenWidth * 72.0 / pdf_width, \
3449 ScreenHeight * 72.0 / pdf_height)
3450 except:
3451 res = 72.0
3453 # phase 2: use pdftk
3454 try:
3455 assert 0 == spawn(os.P_WAIT, pdftkPath, \
3456 ["pdftk", FileNameEscape + name + FileNameEscape, \
3457 "dump_data", "output", TempFileName + ".txt"])
3458 title, pages = pdftkParse(TempFileName + ".txt", PageCount)
3459 if DocumentTitle and title: DocumentTitle = title
3460 except:
3461 pass
3462 else:
3463 # Image File
3464 pages = 1
3465 SetPageProp(PageCount + 1, '_title', os.path.split(name)[-1])
3467 # validity check
3468 if not pages:
3469 print >>sys.stderr, "Warning: The input file `%s' could not be analyzed." % name
3470 continue
3472 # add pages and files into PageProps and FileProps
3473 pagerange = list(range(PageCount + 1, PageCount + pages + 1))
3474 for page in pagerange:
3475 SetPageProp(page, '_file', name)
3476 if ispdf: SetPageProp(page, '_page', page - PageCount)
3477 title = GetFileProp(name, 'title')
3478 if title: SetPageProp(page, '_title', title)
3479 SetFileProp(name, 'pages', GetFileProp(name, 'pages', []) + pagerange)
3480 SetFileProp(name, 'offsets', GetFileProp(name, 'offsets', []) + [PageCount])
3481 if not GetFileProp(name, 'stat'): SetFileProp(name, 'stat', my_stat(name))
3482 if ispdf: SetFileProp(name, 'res', res)
3483 PageCount += pages
3485 # no pages? strange ...
3486 if not PageCount:
3487 print >>sys.stderr, "The presentation doesn't have any pages, quitting."
3488 sys.exit(1)
3490 # if rendering is wanted, do it NOW
3491 if RenderToDirectory:
3492 sys.exit(DoRender())
3494 # load and execute info script
3495 if not InfoScriptPath:
3496 InfoScriptPath = FileName + ".info"
3497 LoadInfoScript()
3499 # initialize graphics
3500 pygame.init()
3501 if Fullscreen and UseAutoScreenSize:
3502 size = GetScreenSize()
3503 if size:
3504 ScreenWidth, ScreenHeight = size
3505 print >>sys.stderr, "Detected screen size: %dx%d pixels" % (ScreenWidth, ScreenHeight)
3506 flags = OPENGL|DOUBLEBUF
3507 if Fullscreen:
3508 flags |= FULLSCREEN
3509 try:
3510 pygame.display.set_mode((ScreenWidth, ScreenHeight), flags)
3511 except:
3512 print >>sys.stderr, "FATAL: cannot create rendering surface in the desired resolution (%dx%d)" % (ScreenWidth, ScreenHeight)
3513 sys.exit(1)
3514 pygame.display.set_caption(__title__)
3515 pygame.key.set_repeat(500, 30)
3516 if Fullscreen:
3517 pygame.mouse.set_visible(False)
3518 CursorVisible = False
3519 glOrtho(0.0, 1.0, 1.0, 0.0, -10.0, 10.0)
3520 if (Gamma <> 1.0) or (BlackLevel <> 0):
3521 SetGamma(force=True)
3523 # check if graphics are unaccelerated
3524 renderer = glGetString(GL_RENDERER)
3525 print >>sys.stderr, "OpenGL renderer:", renderer
3526 if renderer.lower() in ("mesa glx indirect", "gdi generic"):
3527 print >>sys.stderr, "WARNING: Using an OpenGL software renderer. Accentuate will work, but it will"
3528 print >>sys.stderr, " very likely be too slow to be usable."
3530 # setup the OpenGL texture mode
3531 Extensions = dict([(ext.split('_', 2)[-1], None) for ext in \
3532 glGetString(GL_EXTENSIONS).split()])
3533 if AllowExtensions and ("texture_non_power_of_two" in Extensions):
3534 print >>sys.stderr, "Using GL_ARB_texture_non_power_of_two."
3535 TextureTarget = GL_TEXTURE_2D
3536 TexWidth = ScreenWidth
3537 TexHeight = ScreenHeight
3538 TexMaxS = 1.0
3539 TexMaxT = 1.0
3540 elif AllowExtensions and ("texture_rectangle" in Extensions):
3541 print >>sys.stderr, "Using GL_ARB_texture_rectangle."
3542 TextureTarget = 0x84F5 # GL_TEXTURE_RECTANGLE_ARB
3543 TexWidth = ScreenWidth
3544 TexHeight = ScreenHeight
3545 TexMaxS = ScreenWidth
3546 TexMaxT = ScreenHeight
3547 else:
3548 print >>sys.stderr, "Using conventional power-of-two textures with padding."
3549 TextureTarget = GL_TEXTURE_2D
3550 TexWidth = npot(ScreenWidth)
3551 TexHeight = npot(ScreenHeight)
3552 TexMaxS = ScreenWidth * 1.0 / TexWidth
3553 TexMaxT = ScreenHeight * 1.0 / TexHeight
3554 TexSize = TexWidth * TexHeight * 3
3556 # set up some variables
3557 if DAR is not None:
3558 PAR = DAR / float(ScreenWidth) * float(ScreenHeight)
3559 MeshStepX = 1.0 / MeshResX
3560 MeshStepY = 1.0 / MeshResY
3561 PixelX = 1.0 / ScreenWidth
3562 PixelY = 1.0 / ScreenHeight
3563 EdgeX = BoxEdgeSize * 1.0 / ScreenWidth
3564 EdgeY = BoxEdgeSize * 1.0 / ScreenHeight
3565 if InitialPage is None:
3566 InitialPage = GetNextPage(0, 1)
3567 Pcurrent = InitialPage
3569 # prepare logo image
3570 LogoImage = Image.open(StringIO.StringIO(LOGO))
3571 LogoTexture = glGenTextures(1)
3572 glBindTexture(GL_TEXTURE_2D, LogoTexture)
3573 glTexImage2D(GL_TEXTURE_2D, 0, 1, 256, 64, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, LogoImage.tostring())
3574 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
3575 DrawLogo()
3576 pygame.display.flip()
3578 # initialize OSD font
3579 try:
3580 OSDFont = GLFont(FontTextureWidth, FontTextureHeight, FontList, FontSize, search_path=FontPath)
3581 DrawLogo()
3582 titles = []
3583 for key in ('title', '_title'):
3584 titles.extend([p[key] for p in PageProps.itervalues() if key in p])
3585 if titles:
3586 OSDFont.AddString("".join(titles))
3587 except ValueError:
3588 print >>sys.stderr, "The OSD font size is too large, the OSD will be rendered incompletely."
3589 except IOError:
3590 print >>sys.stderr, "Could not open OSD font file, disabling OSD."
3591 except (NameError, AttributeError, TypeError):
3592 print >>sys.stderr, "Your version of PIL is too old or incomplete, disabling OSD."
3594 # initialize mouse cursor
3595 if CursorImage:
3596 try:
3597 CursorImage = PrepareCustomCursor(Image.open(CursorImage))
3598 except:
3599 print >>sys.stderr, "Could not open the mouse cursor image, using standard cursor."
3600 CursorImage = False
3602 # set up page cache
3603 if CacheMode == PersistentCache:
3604 if not CacheFileName:
3605 CacheFileName = FileName + ".cache"
3606 InitPCache()
3607 if CacheMode == FileCache:
3608 CacheFile = tempfile.TemporaryFile(prefix="Accentuate-", suffix=".cache")
3610 # initialize overview metadata
3611 OverviewPageMap=[i for i in xrange(1, PageCount + 1) \
3612 if GetPageProp(i, ('overview', '_overview'), True) \
3613 and (i >= PageRangeStart) and (i <= PageRangeEnd)]
3614 OverviewPageCount = max(len(OverviewPageMap), 1)
3615 OverviewPageMapInv = {}
3616 for page in xrange(1, PageCount + 1):
3617 OverviewPageMapInv[page] = len(OverviewPageMap) - 1
3618 for i in xrange(len(OverviewPageMap)):
3619 if OverviewPageMap[i] >= page:
3620 OverviewPageMapInv[page] = i
3621 break
3623 # initialize overview page geometry
3624 OverviewGridSize = 1
3625 while OverviewPageCount > OverviewGridSize * OverviewGridSize:
3626 OverviewGridSize += 1
3627 OverviewCellX = int(ScreenWidth / OverviewGridSize)
3628 OverviewCellY = int(ScreenHeight / OverviewGridSize)
3629 OverviewOfsX = int((ScreenWidth - OverviewCellX * OverviewGridSize)/2)
3630 OverviewOfsY = int((ScreenHeight - OverviewCellY * \
3631 int((OverviewPageCount + OverviewGridSize - 1) / OverviewGridSize)) / 2)
3632 OverviewImage = Image.new('RGB', (TexWidth, TexHeight))
3634 # fill overlay "dummy" images
3635 dummy = LogoImage.copy()
3636 maxsize = (OverviewCellX - 2 * OverviewBorder, OverviewCellY - 2 * OverviewBorder)
3637 if (dummy.size[0] > maxsize[0]) or (dummy.size[1] > maxsize[1]):
3638 dummy.thumbnail(ZoomToFit(dummy.size, maxsize), Image.ANTIALIAS)
3639 margX = int((OverviewCellX - dummy.size[0]) / 2)
3640 margY = int((OverviewCellY - dummy.size[1]) / 2)
3641 dummy = dummy.convert(mode='RGB')
3642 for page in range(OverviewPageCount):
3643 pos = OverviewPos(page)
3644 OverviewImage.paste(dummy, (pos[0] + margX, pos[1] + margY))
3645 del dummy
3647 # set up background rendering
3648 if not EnableBackgroundRendering:
3649 print >>sys.stderr, "Background rendering isn't available on this platform."
3650 BackgroundRendering = False
3652 # if caching is enabled, pre-render all pages
3653 if CacheMode and not(BackgroundRendering):
3654 DrawLogo()
3655 DrawProgress(0.0)
3656 pygame.display.flip()
3657 for pdf in FileProps:
3658 if pdf.lower().endswith(".pdf"):
3659 ParsePDF(pdf)
3660 stop = False
3661 progress = 0.0
3662 for page in range(InitialPage, PageCount + 1) + range(1, InitialPage):
3663 event = pygame.event.poll()
3664 while event.type != NOEVENT:
3665 if event.type == KEYDOWN:
3666 if (event.key == K_ESCAPE) or (event.unicode == u'q'):
3667 Quit()
3668 stop = True
3669 elif event.type == MOUSEBUTTONUP:
3670 stop = True
3671 event = pygame.event.poll()
3672 if stop: break
3673 if (page >= PageRangeStart) and (page <= PageRangeEnd):
3674 PageImage(page)
3675 DrawLogo()
3676 progress += 1.0 / PageCount;
3677 DrawProgress(progress)
3678 pygame.display.flip()
3680 # create buffer textures
3681 DrawLogo()
3682 pygame.display.flip()
3683 glEnable(TextureTarget)
3684 Tcurrent = glGenTextures(1)
3685 Tnext = glGenTextures(1)
3686 for T in (Tcurrent, Tnext):
3687 glBindTexture(TextureTarget, T)
3688 glTexParameteri(TextureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
3689 glTexParameteri(TextureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
3690 glTexParameteri(TextureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP)
3691 glTexParameteri(TextureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP)
3693 # prebuffer current and next page
3694 Pnext = 0
3695 RenderPage(Pcurrent, Tcurrent)
3696 PageEntered(update_time=False)
3697 PreloadNextPage(GetNextPage(Pcurrent, 1))
3699 # some other preparations
3700 PrepareTransitions()
3701 GenerateSpotMesh()
3702 if PollInterval:
3703 pygame.time.set_timer(USEREVENT_POLL_FILE, PollInterval * 1000)
3705 # start the background rendering thread
3706 if CacheMode and BackgroundRendering:
3707 RTrunning = True
3708 thread.start_new_thread(RenderThread, (Pcurrent, Pnext))
3710 # start output and enter main loop
3711 StartTime = pygame.time.get_ticks()
3712 pygame.time.set_timer(USEREVENT_TIMER_UPDATE, 100)
3713 if not(Fullscreen) and CursorImage:
3714 pygame.mouse.set_visible(False)
3715 DrawCurrentPage()
3716 UpdateCaption(Pcurrent)
3718 # Kick off LIRC thread
3719 if(UseLIRC is True):
3720 irrec = IRRec()
3721 irrec.start()
3723 while True:
3724 HandleEvent(pygame.event.wait())
3727 # wrapper around main() that ensures proper uninitialization
3728 def run_main():
3729 global CacheFile
3730 try:
3731 main()
3732 finally:
3733 StopMPlayer()
3734 # ensure that background rendering is halted
3735 Lrender.acquire()
3736 Lcache.acquire()
3737 # remove all temp files
3738 if 'CacheFile' in globals():
3739 del CacheFile
3740 for tmp in glob.glob(TempFileName + "*"):
3741 try:
3742 os.remove(tmp)
3743 except OSError:
3744 pass
3745 pygame.quit()
3746 if(UseLIRC is True):
3747 pylirc.exit()
3750 ##### COMMAND-LINE PARSER AND HELP #############################################
3752 def if_op(cond, res_then, res_else):
3753 if cond: return res_then
3754 else: return res_else
3756 def HelpExit(code=0):
3757 print """A nice presentation tool.
3759 Usage: """+os.path.basename(sys.argv[0])+""" [OPTION...] <INPUT(S)...>
3761 You may either play a PDF file, a directory containing image files or
3762 individual image files.
3764 Input options:
3765 -r, --rotate <n> rotate pages clockwise in 90-degree steps
3766 --scale scale images to fit screen (not used in PDF mode)
3767 --supersample use supersampling (only used in PDF mode)
3768 -s --supersample for PDF files, --scale for image files
3769 -I, --script <path> set the path of the info script
3770 -u, --poll <seconds> check periodically if the source file has been
3771 updated and reload it if it did
3772 -o, --output <dir> don't display the presentation, only render to .png
3773 -h, --help show this help text and exit
3775 Output options:
3776 -f, --fullscreen """+if_op(Fullscreen,"do NOT ","")+"""start in fullscreen mode
3777 -g, --geometry <WxH> set window size or fullscreen resolution
3778 -A, --aspect <X:Y> adjust for a specific display aspect ratio (e.g. 5:4)
3779 -G, --gamma <G[:BL]> specify startup gamma and black level
3781 Page options:
3782 -i, --initialpage <n> start with page <n>
3783 -p, --pages <A-B> only cache pages in the specified range;
3784 implicitly sets -i <A>
3785 -w, --wrap go back to the first page after the last page
3786 -J, --fade fade to black after the last page
3787 -H, --hyperlink attempt to compensate for landscaped hyperlinks (hack)
3788 -a, --auto <seconds> automatically advance to next page after some seconds
3789 -O, --autooverview <x> automatically derive page visibility on overview page
3790 -O first = show pages with captions
3791 -O last = show pages before pages with captions
3793 Display options:
3794 -t, --transition <trans[,trans2...]>
3795 force a specific transitions or set of transitions
3796 -l, --listtrans print a list of available transitions and exit
3797 -F, --font <file> use a specific TrueType font file for the OSD
3798 -S, --fontsize <px> specify the OSD font size in pixels
3799 -C, --cursor <F[:X,Y]> use a .png image as the mouse cursor
3800 -L, --layout <spec> set the OSD layout (please read the documentation)
3802 Timing options:
3803 -M, --minutes display time in minutes, not seconds
3804 -d, --duration <time> set the desired duration of the presentation and show
3805 a progress bar at the bottom of the screen
3806 -T, --transtime <ms> set transition duration in milliseconds
3807 -D, --mousedelay <ms> set mouse hide delay for fullscreen mode (in ms)
3808 -B, --boxfade <ms> set highlight box fade duration in milliseconds
3809 -Z, --zoom <ms> set zoom duration in milliseconds
3811 Advanced options:
3812 -c, --cache <mode> set page cache mode:
3813 -c none = disable caching completely
3814 -c memory = store cache in RAM
3815 -c disk = store cache on disk temporarily
3816 -c persistent = store cache on disk persistently
3817 --cachefile <path> set the persistent cache file path (implies -cp)
3818 -b, --noback don't pre-render images in the background
3819 -P, --gspath <path> set path to GhostScript or pdftoppm executable
3820 -R, --meshres <XxY> set mesh resolution for effects (default: 48x36)
3821 -e, --noext don't use OpenGL texture size extensions
3823 For detailed information, visit""", __website__
3824 sys.exit(code)
3826 def ListTransitions():
3827 print "Available transitions:"
3828 standard = dict([(tc.__name__, None) for tc in AvailableTransitions])
3829 trans = [(tc.__name__, tc.__doc__) for tc in AllTransitions]
3830 trans.append(('None', "no transition"))
3831 trans.sort()
3832 maxlen = max([len(item[0]) for item in trans])
3833 for name, desc in trans:
3834 if name in standard:
3835 star = '*'
3836 else:
3837 star = ' '
3838 print star, name.ljust(maxlen), '-', desc
3839 print "(transitions with * are enabled by default)"
3840 sys.exit(0)
3842 def TryTime(s, regexp, func):
3843 m = re.match(regexp, s, re.I)
3844 if not m: return 0
3845 return func(map(int, m.groups()))
3846 def ParseTime(s):
3847 return TryTime(s, r'([0-9]+)s?$', lambda m: m[0]) \
3848 or TryTime(s, r'([0-9]+)m$', lambda m: m[0] * 60) \
3849 or TryTime(s, r'([0-9]+)[m:]([0-9]+)[ms]?$', lambda m: m[0] * 60 + m[1]) \
3850 or TryTime(s, r'([0-9]+)[h:]([0-9]+)[hm]?$', lambda m: m[0] * 3600 + m[1] * 60) \
3851 or TryTime(s, r'([0-9]+)[h:]([0-9]+)[m:]([0-9]+)s?$', lambda m: m[0] * 3600 + m[1] * 60 + m[2])
3853 def opterr(msg):
3854 print >>sys.stderr, "command line parse error:", msg
3855 print >>sys.stderr, "use `%s -h' to get help" % sys.argv[0]
3856 print >>sys.stderr, "or visit", __website__, "for full documentation"
3857 sys.exit(2)
3859 def SetTransitions(list):
3860 global AvailableTransitions
3861 index = dict([(tc.__name__.lower(), tc) for tc in AllTransitions])
3862 index['none'] = None
3863 AvailableTransitions=[]
3864 for trans in list.split(','):
3865 try:
3866 AvailableTransitions.append(index[trans.lower()])
3867 except KeyError:
3868 opterr("unknown transition `%s'" % trans)
3870 def ParseLayoutPosition(value):
3871 xpos = []
3872 ypos = []
3873 for c in value.strip().lower():
3874 if c == 't': ypos.append(0)
3875 elif c == 'b': ypos.append(1)
3876 elif c == 'l': xpos.append(0)
3877 elif c == 'r': xpos.append(1)
3878 elif c == 'c': xpos.append(2)
3879 else: opterr("invalid position specification `%s'" % value)
3880 if not xpos: opterr("position `%s' lacks X component" % value)
3881 if not ypos: opterr("position `%s' lacks Y component" % value)
3882 if len(xpos)>1: opterr("position `%s' has multiple X components" % value)
3883 if len(ypos)>1: opterr("position `%s' has multiple Y components" % value)
3884 return (xpos[0] << 1) | ypos[0]
3885 def SetLayoutSubSpec(key, value):
3886 global OSDTimePos, OSDTitlePos, OSDPagePos, OSDStatusPos
3887 global OSDAlpha, OSDMargin
3888 lkey = key.strip().lower()
3889 if lkey in ('a', 'alpha', 'opacity'):
3890 try:
3891 OSDAlpha = float(value)
3892 except ValueError:
3893 opterr("invalid alpha value `%s'" % value)
3894 if OSDAlpha > 1.0:
3895 OSDAlpha *= 0.01 # accept percentages, too
3896 if (OSDAlpha < 0.0) or (OSDAlpha > 1.0):
3897 opterr("alpha value %s out of range" % value)
3898 elif lkey in ('margin', 'dist', 'distance'):
3899 try:
3900 OSDMargin = float(value)
3901 except ValueError:
3902 opterr("invalid margin value `%s'" % value)
3903 if OSDMargin < 0:
3904 opterr("margin value %s out of range" % value)
3905 elif lkey in ('t', 'time'):
3906 OSDTimePos = ParseLayoutPosition(value)
3907 elif lkey in ('title', 'caption'):
3908 OSDTitlePos = ParseLayoutPosition(value)
3909 elif lkey in ('page', 'number'):
3910 OSDPagePos = ParseLayoutPosition(value)
3911 elif lkey in ('status', 'info'):
3912 OSDStatusPos = ParseLayoutPosition(value)
3913 else:
3914 opterr("unknown layout element `%s'" % key)
3915 def SetLayout(spec):
3916 for sub in spec.replace(':', '=').split(','):
3917 try:
3918 key, value = sub.split('=')
3919 except ValueError:
3920 opterr("invalid layout spec `%s'" % sub)
3921 SetLayoutSubSpec(key, value)
3923 def ParseCacheMode(arg):
3924 arg = arg.strip().lower()
3925 if "none".startswith(arg): return NoCache
3926 if "off".startswith(arg): return NoCache
3927 if "memory".startswith(arg): return MemCache
3928 if "disk".startswith(arg): return FileCache
3929 if "file".startswith(arg): return FileCache
3930 if "persistent".startswith(arg): return PersistentCache
3931 opterr("invalid cache mode `%s'" % arg)
3933 def ParseAutoOverview(arg):
3934 arg = arg.strip().lower()
3935 if "off".startswith(arg): return Off
3936 if "first".startswith(arg): return First
3937 if "last".startswith(arg): return Last
3938 try:
3939 i = int(arg)
3940 assert (i >= Off) and (i <= Last)
3941 except:
3942 opterr("invalid auto-overview mode `%s'" % arg)
3944 def ParseOptions(argv):
3945 global FileName, FileList, Fullscreen, Scaling, Supersample, CacheMode
3946 global TransitionDuration, MouseHideDelay, BoxFadeDuration, ZoomDuration
3947 global ScreenWidth, ScreenHeight, MeshResX, MeshResY, InitialPage, Wrap
3948 global AutoAdvance, RenderToDirectory, Rotation, AllowExtensions, DAR
3949 global BackgroundRendering, UseAutoScreenSize, PollInterval, CacheFileName
3950 global PageRangeStart, PageRangeEnd, FontList, FontSize, Gamma, BlackLevel
3951 global EstimatedDuration, CursorImage, CursorHotspot, MinutesOnly
3952 global GhostScriptPath, pdftoppmPath, UseGhostScript, InfoScriptPath
3953 global AutoOverview, FadeToBlackAtEnd
3955 try: # unused short options: jknqvxyzEKNQUVWXY
3956 opts, args = getopt.getopt(argv, \
3957 "hfg:sc:i:wa:t:lo:r:T:D:B:Z:P:R:eA:mbp:u:F:S:G:d:C:ML:I:O:J:H", \
3958 ["help", "fullscreen", "geometry=", "scale", "supersample", \
3959 "nocache", "initialpage=", "wrap", "auto", "listtrans", "output=", \
3960 "rotate=", "transition=", "transtime=", "mousedelay=", "boxfade=", \
3961 "zoom=", "gspath=", "meshres=", "noext", "aspect", "memcache", \
3962 "noback", "pages=", "poll=", "font=", "fontsize=", "gamma=",
3963 "duration=", "cursor=", "minutes", "layout=", "script=", "cache=",
3964 "cachefile=", "autooverview=", "fade", "hyperlink"])
3965 except getopt.GetoptError, message:
3966 opterr(message)
3968 for opt, arg in opts:
3969 if opt in ("-h", "--help"):
3970 HelpExit()
3971 if opt in ("-l", "--listtrans"):
3972 ListTransitions()
3973 if opt in ("-f", "--fullscreen"):
3974 Fullscreen = not(Fullscreen)
3975 if opt in ("-e", "--noext"):
3976 AllowExtensions = not(AllowExtensions)
3977 if opt in ("-s", "--scale"):
3978 Scaling = not(Scaling)
3979 if opt in ("-s", "--supersample"):
3980 Supersample = 2
3981 if opt in ("-w", "--wrap"):
3982 Wrap = not(Wrap)
3983 if opt in ("-J", "--fade"):
3984 FadeToBlackAtEnd = not(FadeToBlackAtEnd)
3985 if opt in ("-H", "--hyperlink"):
3986 PowerdotFix = not(PowerdotFix)
3987 if opt in ("-O", "--autooverview"):
3988 AutoOverview = ParseAutoOverview(arg)
3989 if opt in ("-c", "--cache"):
3990 CacheMode = ParseCacheMode(arg)
3991 if opt == "--nocache":
3992 print >>sys.stderr, "Note: The `--nocache' option is deprecated, use `--cache none' instead."
3993 CacheMode = NoCache
3994 if opt in ("-m", "--memcache"):
3995 print >>sys.stderr, "Note: The `--memcache' option is deprecated, use `--cache memory' instead."
3996 CacheMode = MemCache
3997 if opt == "--cachefile":
3998 CacheFileName = arg
3999 CacheMode = PersistentCache
4000 if opt in ("-M", "--minutes"):
4001 MinutesOnly = not(MinutesOnly)
4002 if opt in ("-b", "--noback"):
4003 BackgroundRendering = not(BackgroundRendering)
4004 if opt in ("-t", "--transition"):
4005 SetTransitions(arg)
4006 if opt in ("-L", "--layout"):
4007 SetLayout(arg)
4008 if opt in ("-o", "--output"):
4009 RenderToDirectory = arg
4010 if opt in ("-I", "--script"):
4011 InfoScriptPath = arg
4012 if opt in ("-F", "--font"):
4013 FontList = [arg]
4014 if opt in ("-P", "--gspath"):
4015 UseGhostScript = (arg.replace("\\", "/").split("/")[-1].lower().find("pdftoppm") < 0)
4016 if UseGhostScript:
4017 GhostScriptPath = arg
4018 else:
4019 pdftoppmPath = arg
4020 if opt in ("-S", "--fontsize"):
4021 try:
4022 FontSize = int(arg)
4023 assert FontSize > 0
4024 except:
4025 opterr("invalid parameter for --fontsize")
4026 if opt in ("-i", "--initialpage"):
4027 try:
4028 InitialPage = int(arg)
4029 assert InitialPage > 0
4030 except:
4031 opterr("invalid parameter for --initialpage")
4032 if opt in ("-d", "--duration"):
4033 try:
4034 EstimatedDuration = ParseTime(arg)
4035 assert EstimatedDuration > 0
4036 except:
4037 opterr("invalid parameter for --duration")
4038 if opt in ("-a", "--auto"):
4039 try:
4040 AutoAdvance = int(arg) * 1000
4041 assert (AutoAdvance > 0) and (AutoAdvance <= 86400000)
4042 except:
4043 opterr("invalid parameter for --auto")
4044 if opt in ("-T", "--transtime"):
4045 try:
4046 TransitionDuration = int(arg)
4047 assert (TransitionDuration >= 0) and (TransitionDuration < 32768)
4048 except:
4049 opterr("invalid parameter for --transtime")
4050 if opt in ("-D", "--mousedelay"):
4051 try:
4052 MouseHideDelay = int(arg)
4053 assert (MouseHideDelay >= 0) and (MouseHideDelay < 32768)
4054 except:
4055 opterr("invalid parameter for --mousedelay")
4056 if opt in ("-B", "--boxfade"):
4057 try:
4058 BoxFadeDuration = int(arg)
4059 assert (BoxFadeDuration >= 0) and (BoxFadeDuration < 32768)
4060 except:
4061 opterr("invalid parameter for --boxfade")
4062 if opt in ("-Z", "--zoom"):
4063 try:
4064 ZoomDuration = int(arg)
4065 assert (ZoomDuration >= 0) and (ZoomDuration < 32768)
4066 except:
4067 opterr("invalid parameter for --zoom")
4068 if opt in ("-r", "--rotate"):
4069 try:
4070 Rotation = int(arg)
4071 except:
4072 opterr("invalid parameter for --rotate")
4073 while Rotation < 0: Rotation += 4
4074 Rotation = Rotation & 3
4075 if opt in ("-u", "--poll"):
4076 try:
4077 PollInterval = int(arg)
4078 assert PollInterval >= 0
4079 except:
4080 opterr("invalid parameter for --poll")
4081 if opt in ("-g", "--geometry"):
4082 try:
4083 ScreenWidth, ScreenHeight = map(int, arg.split("x"))
4084 assert (ScreenWidth >= 320) and (ScreenWidth < 4096)
4085 assert (ScreenHeight >= 200) and (ScreenHeight < 4096)
4086 UseAutoScreenSize = False
4087 except:
4088 opterr("invalid parameter for --geometry")
4089 if opt in ("-R", "--meshres"):
4090 try:
4091 MeshResX, MeshResY = map(int, arg.split("x"))
4092 assert (MeshResX > 0) and (MeshResX <= ScreenWidth)
4093 assert (MeshResY > 0) and (MeshResY <= ScreenHeight)
4094 except:
4095 opterr("invalid parameter for --meshres")
4096 if opt in ("-p", "--pages"):
4097 try:
4098 PageRangeStart, PageRangeEnd = map(int, arg.split("-"))
4099 assert PageRangeStart > 0
4100 assert PageRangeStart <= PageRangeEnd
4101 except:
4102 opterr("invalid parameter for --pages")
4103 InitialPage=PageRangeStart
4104 if opt in ("-A", "--aspect"):
4105 try:
4106 if ':' in arg:
4107 fx, fy = map(float, arg.split(':'))
4108 DAR = fx / fy
4109 else:
4110 DAR = float(arg)
4111 assert DAR > 0.0
4112 except:
4113 opterr("invalid parameter for --aspect")
4114 if opt in ("-G", "--gamma"):
4115 try:
4116 if ':' in arg:
4117 arg, bl = arg.split(':', 1)
4118 BlackLevel = int(bl)
4119 Gamma = float(arg)
4120 assert Gamma > 0.0
4121 assert (BlackLevel >= 0) and (BlackLevel < 255)
4122 except:
4123 opterr("invalid parameter for --gamma")
4124 if opt in ("-C", "--cursor"):
4125 try:
4126 if ':' in arg:
4127 arg = arg.split(':')
4128 assert len(arg) > 1
4129 CursorImage = ':'.join(arg[:-1])
4130 CursorHotspot = map(int, arg[-1].split(','))
4131 else:
4132 CursorImage = arg
4133 assert (BlackLevel >= 0) and (BlackLevel < 255)
4134 except:
4135 opterr("invalid parameter for --cursor")
4137 for arg in args:
4138 AddFile(arg)
4139 if not FileList:
4140 opterr("no playable files specified")
4141 return
4143 # glob and filter argument list
4144 files = []
4145 for arg in args:
4146 files.extend(glob.glob(arg))
4147 files = list(filter(IsPlayable, files))
4149 # if only one argument is specified, use it as the informal file name
4150 if len(files) == 1:
4151 FileName = files[0]
4152 else:
4153 FileName = ""
4155 # construct final FileList by expanding directories to image file lists
4156 FileList = []
4157 for item in files:
4158 if os.path.isdir(item):
4159 images = [os.path.join(item, f) for f in os.listdir(item) if IsImageFileName(f)]
4160 images.sort(lambda a, b: cmp(a.lower(), b.lower()))
4161 FileList.extend(images)
4162 else:
4163 FileList.append(item)
4165 if not FileList:
4166 opterr("no playable files specified")
4169 # use this function if you intend to use Accentuate as a library
4170 def run():
4171 try:
4172 run_main()
4173 except SystemExit, e:
4174 return e.code
4176 if __name__=="__main__":
4177 ParseOptions(sys.argv[1:])
4178 run_main()