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