#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
#
# KeyJnote, a fancy presentation tool
# Copyright (C) 2005-2007 Martin J. Fiedler <martin.fiedler@gmx.net>
# portions Copyright (C) 2005 Rob Reid <rreid@drao.nrc.ca>
# portions Copyright (C) 2006 Ronan Le Hy <rlehy@free.fr>
# portions Copyright (C) 2007 Luke Campagnola <luke.campagnola@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

__title__   = "KeyJnote"
__version__ = "0.10.0"
__author__  = "Martin J. Fiedler"
__email__   = "martin.fiedler@gmx.net"
__website__ = "http://keyjnote.sourceforge.net/"
import sys
if __name__ == "__main__":
    print >>sys.stderr, "Welcome to", __title__, "version", __version__
TopLeft, BottomLeft, TopRight, BottomRight, TopCenter, BottomCenter = range(6)

# You may change the following lines to modify the default settings
Fullscreen = True
Scaling = False
Supersample = None
BackgroundRendering = True
UseGhostScript = False
UseAutoScreenSize = True
ScreenWidth = 1024
ScreenHeight = 768
TransitionDuration = 1000
MouseHideDelay = 3000
BoxFadeDuration = 100
ZoomDuration = 250
BlankFadeDuration = 250
MeshResX = 48
MeshResY = 36
MarkColor = (1.0, 0.0, 0.0, 0.1)
BoxEdgeSize = 4
SpotRadius = 64
SpotDetail = 16
UseCache = True
CacheFile = True
OverviewBorder = 3
InitialPage = None
Wrap = False
AutoAdvance = None
RenderToDirectory = None
Rotation = 0
AllowExtensions = True
DAR = None
PAR = 1.0
PollInterval = 0
PageRangeStart = 0
PageRangeEnd = 999999
FontSize = 14
FontTextureWidth = 512
FontTextureHeight = 256
Gamma = 1.0
BlackLevel = 0
GammaStep = 1.1
BlackLevelStep = 8
EstimatedDuration = None
ProgressBarSize = 16
ProgressBarAlpha = 128
CursorImage = None
CursorHotspot = (0, 0)
MinutesOnly = False
OSDMargin = 16
OSDAlpha = 1.0
OSDTimePos = TopRight
OSDTitlePos = BottomLeft
OSDPagePos = BottomRight
OSDStatusPos = TopLeft


# import basic modules
import random, getopt, os, types, re, codecs, tempfile, glob, StringIO
from math import *

# initialize some platform-specific settings
if os.name == "nt":
    root = os.path.split(sys.argv[0])[0] or "."
    pdftoppmPath = os.path.join(root, "pdftoppm.exe")
    GhostScriptPath = os.path.join(root, "gs\\gswin32c.exe")
    GhostScriptPlatformOptions = ["-I" + os.path.join(root, "gs")]
    try:
        import win32api
        SoundPlayerPath = os.path.join(root, "gplay.exe")
        def GetScreenSize():
            dm = win32api.EnumDisplaySettings(None, -1) #ENUM_CURRENT_SETTINGS
            return (int(dm.PelsWidth), int(dm.PelsHeight))
    except ImportError:
        SoundPlayerPath = ""
        def GetScreenSize(): return pygame.display.list_modes()[0]
    SoundPlayerOptions = []
    pdftkPath = os.path.join(root, "pdftk.exe")
    FileNameEscape = '"'
    spawn = os.spawnv
    if getattr(sys, "frozen", None):
        sys.path.append(root)
    FontPath = []
    FontList = ["Verdana.ttf", "Arial.ttf"]
else:
    def GetScreenSize(): return pygame.display.list_modes()[0]
    pdftoppmPath = "pdftoppm"
    GhostScriptPath = "gs"
    GhostScriptPlatformOptions = []
    SoundPlayerPath = "mplayer"
    SoundPlayerOptions = ["-quiet", "-really-quiet"]
    pdftkPath = "pdftk"
    spawn = os.spawnvp
    FileNameEscape = ""
    FontPath = ["/usr/share/fonts", "/usr/local/share/fonts", "/usr/X11R6/lib/X11/fonts/TTF"]
    FontList = ["DejaVuSans.ttf", "Vera.ttf", "Verdana.ttf"]

# import special modules
try:
    from OpenGL.GL  import *
    import pygame
    from pygame.locals import *
    import Image, ImageDraw, ImageFont, ImageFilter
    import TiffImagePlugin, BmpImagePlugin, JpegImagePlugin, PngImagePlugin, PpmImagePlugin
except (ValueError, ImportError), err:
    print >>sys.stderr, "Oops! Cannot load necessary modules:", err
    print >>sys.stderr, """To use KeyJnote, you need to install the following Python modules:
 - PyOpenGL [python-opengl]   http://pyopengl.sourceforge.net/
 - PyGame   [python-pygame]   http://www.pygame.org/
 - PIL      [python-imaging]  http://www.pythonware.com/products/pil/
 - PyWin32  (OPTIONAL, Win32) http://starship.python.net/crew/mhammond/win32/
Additionally, please be sure to have pdftoppm or GhostScript installed if you
intend to use PDF input."""
    sys.exit(1)

try:
    import thread
    EnableBackgroundRendering = True
    def create_lock(): return thread.allocate_lock()
except ImportError:
    EnableBackgroundRendering = False
    class pseudolock:
        def __init__(self): self.state = False
        def acquire(self, dummy=0): self.state = True
        def release(self): self.state = False
        def locked(self): return self.state
    def create_lock(): return pseudolock()

##### TOOL CODE ################################################################

# initialize private variables
InfoScriptPath = None
Marking = False
Tracing = False
Panning = False
PageProps = {}
PageCache = {}
SoundPlayerPID = 0
MouseDownX = 0
MouseDownY = 0
MarkUL = (0, 0)
MarkLR = (0, 0)
ZoomX0 = 0.0
ZoomY0 = 0.0
ZoomArea = 1.0
ZoomMode = False
IsZoomed = False
ZoomWarningIssued = False
CurrentCaption = 0
CacheFilePos = 0
OverviewNeedUpdate = False
FileStats = None
OSDFont = None
CurrentOSDCaption = ""
CurrentOSDPage = ""
CurrentOSDStatus = ""
Lrender = create_lock()
Lcache = create_lock()
Loverview = create_lock()
RTrunning = False
RTrestart = False
StartTime = 0
CurrentTime = 0
PageEnterTime = 0
TimeDisplay = False
TimeTracking = False
FirstPage = True
ProgressBarPos = 0
CursorVisible = True
OverviewMode = False
LastPage = 0
WantStatus = False

# tool constants (used in info scripts)
FirstTimeOnly = 2

# event constants
USEREVENT_HIDE_MOUSE = USEREVENT
USEREVENT_PAGE_TIMEOUT = USEREVENT + 1
USEREVENT_POLL_FILE = USEREVENT + 2
USEREVENT_TIMER_UPDATE = USEREVENT + 3

# read and write the PageProps meta-dictionary
def GetPageProp(page, prop, default=None):
    if not page in PageProps: return default
    return PageProps[page].get(prop, default)
def SetPageProp(page, prop, value):
    global PageProps
    if not page in PageProps:
        PageProps[page] = {prop: value}
    else:
        PageProps[page][prop] = value
def GetTristatePageProp(page, prop, default=0):
    res = GetPageProp(page, prop, default)
    if res != FirstTimeOnly: return res
    return (GetPageProp(page, 'shown', 0) == 1)

# the KeyJnote logo (256x64 pixels grayscale PNG)
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\x00\x052IDATx^\xed\x9a\xe1\x91\xe3(\x14\x84{\xf76\x0f\x98Hx\x8eD8\x12\xa1H\x8c"\xb8\x10\x8c"\x19\x88\xc47u\x9c\xab\x8b\xa1(\xd0\xd8W\xb7>'+\
'\xcd\xf7Oe\xcf{\xad\x9e\xa7\x06I\xc6\x81\xf9\xe6\x9bo\xbe\xf9\xe6\x07\xc8\r\x99\xb7\x08\x02?!\x93$\xe21\xea>\xff=?\xd1\xc1N\xc8\xc0F\xbc<\xfb\r\xd0\x17dp\x0e8\xa2\x01\x1e\x19\xac\x1eG4\xc0\x19d6\x8b\xdf\x1dm}\xd4\xd8\xcb/\x90\x1a\x99\x91I\x16'+\
'\xbf7"b@\x9ed\x80\xf6\xaf\x12\x80\xb7\x7f\'\x03\xbc*\x02\xf0p\x19\xe0\x0c\x03\xf0\x88\x06\xc8\xcc\x00<\xa2\x01\xda3\x00\xff\xc7\xfc\xea\x04@+\x00\xf5\x07\x88\x1f\xe0\xb9\xc8\xdfe\x03H\xd5\x95\x9f\x8fA\xad\xbb\x0cp\xa6\x1d\x80\xdaZ\x85\xcc\xe6=o'+\
'\x19\xaa\xbb\x0b\xc8\xb5\xbb\xef\xbf\xf1\xcf\x84\xa9\xe3\xeb\xaebE\x81mY\x00\xe4\x9d"\x1aZG\r\x90\xb9\x19\x80\xdaM<0\xc6\xd9P\x18`=\x88 \xb3Et\xd1\x9e+\xf94-\x0e\x05\xe2\x15\x881\x17~\x81\xa0\xafu0\x03\xb4o\x06\xa0\x84\t\x04PW\x0b\x00!\xf1\x9ck'+\
'\x03<\xba\xc8\xbb\x01\xc1\xec\x0b=\xe1\xaaP2GAE_\xeb\xa0\x01^\xb5\x02P*%\xb8X\x9e#0\x81h\x83L@\x0f\xb9\xa2dr`\x9d`P\xa1\xae\x82\x8a\xbe\xd6!\x03\xaca\x006u\x92\x8b\xd0\x00\xc0~\xe9\n\xa8\xeb\xce\xc2\xf3W \xa4r`Lk\xdf\x00}a\x006\xef\x0e\xcf\xa7'+\
'\xd3yC\xc6\x01\x88+2\xb2\xf7\n i]\xd6\x04\xb0j&\xa8\xa2\xebJ5\x1a\xc0\xf6\x012\xd8\xfe\xa1\xa3\xb5\x1d\x82\xa5\xde\xad\x12\xeeT\xf1t\xc8\xdbl\x95\xb1\x9e18\xd9/\x1a\x90rD\xb9\x19\x19\xa3c\xd1\x14\x9b\xcd]\xdd=.\x95\x97\xdc\xe4\xc6\x89\x1d\xd0\xda'+\
'\x99\x00^\x00FP\xa2\xe7\xa2&\xe0\xcf\x1c\xfb\x90>_\x03\x92%`\xc5\x08IB\x16\xbe\x80\x85\xd8\x14\xd8\xee]\xa3l\r\x85cZ{\x06\xcc@ctme\xb4\xcfR\x8c\xe6\xb7!_\x1a\x00\x16u\xe5\x82\xe2P\x07\xb2M\xf5@\x8fk\xed\x19@\x94\x03a\xd1-T\xa1 \xd5:@\x03R@\x1f'+\
'\x16e!]\x94s\x91\x05\xa2\x03\xaa\xb3\x19\xd6:n\x00\xe6\xa2\xbe\xa8z0\x02\x8b\xc6\xad<qm\xf6\x0c\x80\xafjB\x15r=\x08\x0f\x04\xa4\xabu\xdc\x80\x94\xea\xael\x16@"\xffYp4\x7f\x7f\x04\xc6\xa2&\xab\n\xea \xe1!\x04dXk\x7f+l\xef+\xa1q\xae.\xfa\x0eB\xa9'+\
'@H\x8a\xeb\x00 \xdc\x04\xec4\xa0\xe9:\t\x13\x1b\x931\xad\xfd\t8\x05\xbf\xd5\x17\x014\x9a(\xa0\x8a\xc1\x89\x03\xf0\x00\xbar\x85\x87\x95\xa61\xad]\x03\xce\x01\xb0\xa8\x07X\x8d^\xc7\xb6\x1c\xc2\x87P\r\x03:\x9a\xd4\x03\x0fDV\x0f .\xc8\x18\x8bA\x18'+\
'\x83\x13\r\xc0\x1a_\xee\x89\x90+W\xe3\x8bF\x9f\x84\xea\x1a\xb0\x8d\x01\xd0\xf8\x8fI\x83\x8f\xc5\xed\x15\x19gQ\xb25#\xcc\xbb<z6@\xab\xdc\xcd?*7\xd7\x81\x8e\xb5\x8dt\x9e\x8cj\xed\x1b\x10\xd6\xfb\xe6>xj\xe9\xbc%\xf0\xf3}\x02\xa4\x95\x00\x9aBF\x88w'+\
'\x03F\xb2\x91P\xeb\x03\x8f\xc5\x132N\x8f%/cP\tlk\r\x10Nb\x1f6\x95\xce:O:Z\x87\r\xe0~S\xb9\xb2\x99\xf4cP`Z\xdb`\xd93\x00l:\xa1`\xea\x18@\xad\x8f\xbc\x19\xda\x90\x99\xa4(j\xfb+\xa1\x95\xe6\x00\x18J\xdcc\x00\x1c\x08\x0fB#`\xa9\xf5\xeb\x06T\x9b\x81'+\
'\x908\x11-|\xfe\x8ej\x19\xa0=%\xeeZZau}\xaf\x8b\x95QH\x03zZ\xc7\r\x88\x0bX\x86\xa7\x83\xd9vG +\xdc>\xc7\x86\x04\x05\xde#\x0e\xe1\x90Q\xbej\xc2\x0f\xeb\xd9\xa4\xd6\x07\x0c`\x0e\xceR\x1c^<\xbd\x86\xb6R\x1aP\x1f\xc8{\xf4\xce9\x1f\xaf\x8a\x02\x07\t+'+\
'2&\xe8\xcf\xcfH\x97\xc8i\xe2\xae\r\xa3Z\xb9\x0c\xf67\x03\xb9\x8a\xbb 3M[\x88\x11\xd0\x10m\xb0\x04\x80#kj\x034\xd4TM\xca(N\xb2k0\xefk\x88\xd0r/\x85\xcd}\x0e\xd4\x8b\r\x80\xc8\xa0V\x1a\xd0\xdf\x0c(\xe7\x00x=\xf35C\x11\xec\xc4\xf3\x83\xb5\xbd\xfb;G'+\
'\x8c\x12\xed\x95/L@\x90\x98Q\x95\xac\xaeVf\xc0\xe8E\x90G`E\rL\x15\x83\x19\xd74`\xf5\x18\'\x9cP\xc3\xa7}|$MzZ\xc7\r\x88\xae\x9cY\xbb\xf4n>\x19\xef[l\x19\xb0Z\xec!\xbc%T\xac\x12\xab\xb0"\x1d\xad\xe3\x06\xc0oe\xc0\xb8\xb7\r\x9fI\x1a\x84\xf1\xe6\x1b'+\
'\xb6\xa7\xb3\xc5>\xa2^\x12\n\xb6\x93\x8d Q\x12\nzZ\xc7\r\x80Ey[\x18\xe5\xadP\xb3\xad\'\x1d\xaa\t(\xef\x83\xd6\x04J?k\x8f\xdd8}\xa6\xf1i=Ih[\xc4f]\xad\xf8\x81\xfd\xf0M=\xe2\x07\xcdWR\x8b\xab\xfe "\x86\xc7~=\xc0\x9e5Z\xf8qO+\rx>\xc1\xb0\xf6o\xcfO<'+\
'\x19\xfe\xb4b\xc1+\xf0\xfc\t\x90+\xc3\xe6x\x13\xc0\xf3\x87\xc3K\xf0\x07\x9e\x8b\xfd\x93\xbb\xb4\x03N\x80\x0e\x17.\x9e8\\\x06\x88\xe5N}q\xc73@\xae<\xd8\x04\x07\x9c\x80[y\xfe\xc7\xcb\x80\x15\xfcm\x1d\x8e8\x01\xf6\xc2[\xbd\x97\xe1\x17\x9eG\xe0O\x9d'+\
'\x0eJ\xbc\xdd\x82\xc5\x81\xb1V\xe3\xd5\xf8\x0b\xd7\x882\xce\xbf\x86\xdf\xbf\x00\x00\x00\x00IEND\xaeB`\x82'

# determine the next power of two
def npot(x):
    res = 1
    while res < x: res <<= 1
    return res

# extract a number at the beginning of a string
def num(s):
    s = s.strip()
    r = ""
    while s[0] in "0123456789":
        r += s[0]
        s = s[1:]
    try:
        return int(r)
    except ValueError:
        return -1

# get a representative subset of file statistics
def my_stat(filename):
    try:
        s = os.stat(filename)
    except OSError:
        return None
    return (s.st_size, s.st_mtime, s.st_ctime, s.st_mode, s.st_ino, s.st_dev, s.st_uid, s.st_gid)

# determine (pagecount,width,height) of a PDF file
def analyze_pdf(filename):
    f = file(filename,"rb")
    pdf = f.read()
    f.close()
    box = map(float, pdf.split("/MediaBox",1)[1].split("]",1)[0].split("[",1)[1].strip().split())
    return (max(map(num, pdf.split("/Count")[1:])), box[2]-box[0], box[3]-box[1])

# unescape &#123; literals in PDF files
re_unescape = re.compile(r'&#[0-9]+;')
def decode_literal(m):
    try:
        return chr(int(m.group(0)[2:-1]))
    except ValueError:
        return '?'
def unescape_pdf(s):
    return re_unescape.sub(decode_literal, s)

# parse pdftk output
def pdftkParse(filename):
    global DocumentTitle, PageCount
    f = file(filename,"r")
    InfoKey = None
    BookmarkTitle = None
    for line in f.xreadlines():
        try:
            key, value = [item.strip() for item in line.split(':', 1)]
        except IndexError:
            continue
        key = key.lower()
        if key == "numberofpages":
            PageCount = int(value)
        elif key == "infokey":
            InfoKey = value.lower()
        elif (key == "infovalue") and (InfoKey == "title"):
            DocumentTitle = unescape_pdf(value)
            InfoKey = None
        elif key == "bookmarktitle":
            BookmarkTitle = unescape_pdf(value)
        elif key == "bookmarkpagenumber" and BookmarkTitle:
            try:
                page = int(value)
                if not GetPageProp(page, 'private_title'):
                  SetPageProp(page, 'private_title', BookmarkTitle)
            except ValueError:
                pass
            BookmarkTitle = None
    f.close()

# translate pixel coordinates to normalized screen coordinates
def MouseToScreen(mousepos):
    return (ZoomX0 + mousepos[0] * ZoomArea / ScreenWidth,
            ZoomY0 + mousepos[1] * ZoomArea / ScreenHeight)

# normalize rectangle coordinates so that the upper-left point comes first
def NormalizeRect(X0, Y0, X1, Y1):
    return (min(X0, X1), min(Y0, Y1), max(X0, X1), max(Y0, Y1))

# check if a point is inside a box (or a list of boxes)
def InsideBox(x, y, box):
    return (x >= box[0]) and (y >= box[1]) and (x < box[2]) and (y < box[3])
def FindBox(x, y, boxes):
    for i in xrange(len(boxes)):
        if InsideBox(x, y, boxes[i]):
            return i
    raise ValueError

# zoom an image size to a destination size, preserving the aspect ratio
def ZoomToFit(size, dest=None):
    if not dest:
        dest = (ScreenWidth, ScreenHeight)
    newx = dest[0]
    newy = size[1] * newx / size[0]
    if newy > dest[1]:
        newy = dest[1]
        newx = size[0] * newy / size[1]
    return (newx, newy)

# get the overlay grid screen coordinates for a specific page
def OverviewPos(page):
    return ( \
        int(page % OverviewGridSize) * OverviewCellX + OverviewOfsX, \
        int(page / OverviewGridSize) * OverviewCellY + OverviewOfsY  \
    )

def StopSound():
    global SoundPlayerPID
    if not SoundPlayerPID: return
    try:
        if os.name == 'nt':
            win32api.TerminateProcess(SoundPlayerPID, 0)
        else:
            os.kill(SoundPlayerPID, 2)
        SoundPlayerPID = 0
    except:
        pass

def IsImageFileName(name):
    return os.path.splitext(name)[1].lower() in \
           (".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp")

def GetFileStats():
    global PDFinput, FileName, FileList
    if PDFinput:
        return my_stat(FileName)
    else:
        return tuple([my_stat(os.path.join(FileName, f)) for f in FileList])

def FormatTime(t, minutes=False):
    if minutes and (t < 3600):
        return "%d min" % (t / 60)
    elif minutes:
        return "%d:%02d" % (t / 3600, (t / 60) % 60)
    elif t < 3600:
        return "%d:%02d" % (t / 60, t % 60)
    else:
        ms = t % 3600
        return "%d:%02d:%02d" % (t / 3600, ms / 60, ms % 60)

def Quit(code=0):
    print >>sys.stderr, "Total presentation time: %s." % \
                        FormatTime((pygame.time.get_ticks() - StartTime) / 1000)
    sys.exit(code)


##### RENDERING TOOL CODE ######################################################

# draw a fullscreen quad
def DrawFullQuad():
    glBegin(GL_QUADS)
    glTexCoord2d(    0.0,     0.0);  glVertex2i(0, 0)
    glTexCoord2d(TexMaxS,     0.0);  glVertex2i(1, 0)
    glTexCoord2d(TexMaxS, TexMaxT);  glVertex2i(1, 1)
    glTexCoord2d(    0.0, TexMaxT);  glVertex2i(0, 1)
    glEnd()

# draw a generic 2D quad
def DrawQuad(x0=0.0, y0=0.0, x1=1.0, y1=1.0):
    glBegin(GL_QUADS)
    glTexCoord2d(    0.0,     0.0);  glVertex2d(x0, y0)
    glTexCoord2d(TexMaxS,     0.0);  glVertex2d(x1, y0)
    glTexCoord2d(TexMaxS, TexMaxT);  glVertex2d(x1, y1)
    glTexCoord2d(    0.0, TexMaxT);  glVertex2d(x0, y1)
    glEnd()

# a mesh transformation function: it gets the relative transition time (in the
# [0.0,0.1) interval) and the normalized 2D screen coordinates, and returns a
# 7-tuple containing the desired 3D screen coordinates, 2D texture coordinates,
# and intensity/alpha color values.
def meshtrans_null(t, u, v):
    return (u, v, 0.0, u, v, 1.0, t)
         # (x, y, z,   s, t, i,   a)

# draw a quad, applying a mesh transformation function
def DrawMeshQuad(time=0.0, f=meshtrans_null):
    line0 = [f(time, u * MeshStepX, 0.0) for u in xrange(MeshResX + 1)]
    for v in xrange(1, MeshResY + 1):
        line1 = [f(time, u * MeshStepX, v * MeshStepY) for u in xrange(MeshResX + 1)]
        glBegin(GL_QUAD_STRIP)
        for col in zip(line0, line1):
            for x, y, z, s, t, i, a in col:
                glColor4d(i, i, i, a)
                glTexCoord2d(s * TexMaxS, t * TexMaxT)
                glVertex3d(x, y, z)
        glEnd()
        line0 = line1

def GenerateSpotMesh():
    global SpotMesh
    rx0 = SpotRadius * PixelX
    ry0 = SpotRadius * PixelY
    rx1 = (SpotRadius + BoxEdgeSize) * PixelX
    ry1 = (SpotRadius + BoxEdgeSize) * PixelY
    steps = max(6, int(2.0 * pi * SpotRadius / SpotDetail / ZoomArea))
    SpotMesh=[(rx0 * sin(a), ry0 * cos(a), rx1 * sin(a), ry1 * cos(a)) for a in \
             [i * 2.0 * pi / steps for i in range(steps + 1)]]


##### TRANSITIONS ##############################################################

# Each transition is represented by a class derived from keyjnote.Transition
# The interface consists of only two methods: the __init__ method may perform
# some transition-specific initialization, and render() finally renders a frame
# of the transition, using the global texture identifierst Tcurrent and Tnext.

# Transition itself is an abstract class
class AbstractError(StandardError):
    pass
class Transition:
    def __init__(self):
        pass
    def render(self, t):
        raise AbstractError

# an array containing all possible transition classes
AllTransitions=[]

# a helper function doing the common task of directly blitting a background page
def DrawPageDirect(tex):
    glDisable(GL_BLEND)
    glBindTexture(TextureTarget, tex)
    glColor3d(1, 1, 1)
    DrawFullQuad()

# a helper function that enables alpha blending
def EnableAlphaBlend():
    glEnable(GL_BLEND)
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)


# Crossfade: one of the simplest transition you can think of :)
class Crossfade(Transition):
    """simple crossfade"""
    def render(self,t):
      DrawPageDirect(Tcurrent)
      EnableAlphaBlend()
      glBindTexture(TextureTarget, Tnext)
      glColor4d(1, 1, 1, t)
      DrawFullQuad()
AllTransitions.append(Crossfade)


# Slide: a class of transitions that simply slide the new page in from one side
# after an idea from Joachim B Haga
class Slide(Transition):
    def origin(self, t):
        raise AbstractError
    def render(self, t):
        cx, cy, nx, ny = self.origin(t)
    	glBindTexture(TextureTarget, Tcurrent)
    	DrawQuad(cx, cy, cx+1.0, cy+1.0)
    	glBindTexture(TextureTarget, Tnext)
    	DrawQuad(nx, ny, nx+1.0, ny+1.0)

class SlideLeft(Slide):
    """Slide to the left"""
    def origin(self, t): return (-t, 0.0, 1.0-t, 0.0)
class SlideRight(Slide):
    """Slide to the right"""
    def origin(self, t): return (t, 0.0, t-1.0, 0.0)
class SlideUp(Slide):
    """Slide upwards"""
    def origin(self, t): return (0.0, -t, 0.0, 1.0-t)
class SlideDown(Slide):
    """Slide downwards"""
    def origin(self, t): return (0.0, t, 0.0, t-1.0)
AllTransitions.extend([SlideLeft, SlideRight, SlideUp, SlideDown])


# Squeeze: a class of transitions that squeeze the new page in from one size
class Squeeze(Transition):
    def params(self, t):
        raise AbstractError
    def inv(self): return 0
    def render(self, t):
        cx1, cy1, nx0, ny0 = self.params(t)
        if self.inv():
            t1, t2 = (Tnext, Tcurrent)
        else:
            t1, t2 = (Tcurrent, Tnext)
    	glBindTexture(TextureTarget, t1)
    	DrawQuad(0.0, 0.0, cx1, cy1)
    	glBindTexture(TextureTarget, t2)
    	DrawQuad(nx0, ny0, 1.0, 1.0)
class SqueezeHorizontal(Squeeze):
    def split(self, t): raise AbstractError
    def params(self, t):
        t = self.split(t)
        return (t, 1.0, t, 0.0)
class SqueezeVertical(Squeeze):
    def split(self, t): raise AbstractError
    def params(self, t):
        t = self.split(t)
        return (1.0, t, 0.0, t)

class SqueezeLeft(SqueezeHorizontal):
    """Squeeze to the left"""
    def split(self, t): return 1.0 - t
class SqueezeRight(SqueezeHorizontal):
    """Squeeze to the right"""
    def split(self, t): return t
    def inv(self): return 1
class SqueezeUp(SqueezeVertical):
    """Squeeze upwards"""
    def split(self, t): return 1.0 - t
class SqueezeDown(SqueezeVertical):
    """Squeeze downwards"""
    def split(self, t): return t
    def inv(self): return 1
AllTransitions.extend([SqueezeLeft, SqueezeRight, SqueezeUp, SqueezeDown])


# Wipe: a class of transitions that softly "wipe" the new image over the old
# one along a path specified by a gradient function that maps normalized screen
# coordinates to a number in the range [0.0,1.0]
WipeWidth = 0.25
class Wipe(Transition):
    def grad(self, u, v):
        raise AbstractError
    def afunc(self, g):
        pos = (g - self.Wipe_start) / WipeWidth
        return max(min(pos, 1.0), 0.0)
    def render(self, t):
        DrawPageDirect(Tnext)
        EnableAlphaBlend()
        glBindTexture(TextureTarget, Tcurrent)
        self.Wipe_start = t * (1.0 + WipeWidth) - WipeWidth
        DrawMeshQuad(t, lambda t, u, v: \
                     (u, v, 0.0,  u,v,  1.0, self.afunc(self.grad(u, v))))

class WipeDown(Wipe):
    """wipe downwards"""
    def grad(self, u, v): return v
class WipeUp(Wipe):
    """wipe upwards"""
    def grad(self, u, v): return 1.0 - v
class WipeRight(Wipe):
    """wipe from left to right"""
    def grad(self, u, v): return u
class WipeLeft(Wipe):
    """wipe from right to left"""
    def grad(self, u, v): return 1.0 - u
class WipeDownRight(Wipe):
    """wipe from the upper-left to the lower-right corner"""
    def grad(self, u, v): return 0.5 * (u + v)
class WipeUpLeft(Wipe):
    """wipe from the lower-right to the upper-left corner"""
    def grad(self, u, v): return 1.0 - 0.5 * (u + v)
class WipeCenterOut(Wipe):
    """wipe from the center outwards"""
    def grad(self, u, v):
        u -= 0.5
        v -= 0.5
        return sqrt(u * u * 1.777 + v * v) / 0.833
class WipeCenterIn(Wipe):
    """wipe from the edges inwards"""
    def grad(self, u, v):
        u -= 0.5
        v -= 0.5
        return 1.0 - sqrt(u * u * 1.777 + v * v) / 0.833
AllTransitions.extend([WipeDown, WipeUp, WipeRight, WipeLeft, \
                       WipeDownRight, WipeUpLeft, WipeCenterOut, WipeCenterIn])

class WipeBlobs(Wipe):
    """wipe using nice \"blob\"-like patterns"""
    def __init__(self):
        self.uscale = (5.0 + random.random() * 15.0) * 1.333
        self.vscale =  5.0 + random.random() * 15.0
        self.uofs = random.random() * 6.2
        self.vofs = random.random() * 6.2
    def grad(self,u,v):
        return 0.5 + 0.25 * (cos(self.uofs + u * self.uscale) \
                          +  cos(self.vofs + v * self.vscale))
AllTransitions.append(WipeBlobs)

class PagePeel(Transition):
    """an unrealistic, but nice page peel effect"""
    def render(self,t):
        glDisable(GL_BLEND)
        glBindTexture(TextureTarget, Tnext)
        DrawMeshQuad(t, lambda t, u, v: \
                     (u, v, 0.0,  u, v,  1.0 - 0.5 * (1.0 - u) * (1.0 - t), 1.0))
        EnableAlphaBlend()
        glBindTexture(TextureTarget, Tcurrent)
        DrawMeshQuad(t, lambda t, u, v: \
                     (u * (1.0 - t), 0.5 + (v - 0.5) * (1.0 + u * t) * (1.0 + u * t), 0.0,
                      u, v,  1.0 - u * t * t, 1.0))
AllTransitions.append(PagePeel)

### additional transition by Ronan Le Hy <rlehy@free.fr> ###

class PageTurn(Transition):
    """another page peel effect, slower but more realistic than PagePeel"""
    alpha = 2.
    alpha_square = alpha * alpha
    sqrt_two = sqrt(2.)
    inv_sqrt_two = 1. / sqrt(2.)
    def warp(self, t, u, v):
        # distance from the 2d origin to the folding line
        dpt = PageTurn.sqrt_two * (1.0 - t)
        # distance from the 2d origin to the projection of (u,v) on the folding line
        d = PageTurn.inv_sqrt_two * (u + v)
        dmdpt = d - dpt
        # the smaller rho is, the closer to asymptotes are the x(u) and y(v) curves
        # ie, smaller rho => neater fold
        rho = 0.001
        common_sq = sqrt(4. - 8 * t - 4.*(u+v) + 4.*t*(t + v + u) + (u+v)*(u+v) + 4 * rho) / 2.
        x = 1. - t + 0.5 * (u - v) - common_sq
        y = 1. - t + 0.5 * (v - u) - common_sq
        z = - 0.5 * (PageTurn.alpha * dmdpt + sqrt(PageTurn.alpha_square * dmdpt*dmdpt + 4))
        if dmdpt < 0:
            # part of the sheet still flat on the screen: lit and opaque
            i = 1.0
            alpha = 1.0
        else:
            # part of the sheet in the air, after the fold: shadowed and transparent
            # z goes from -0.8 to -2 approximately
            i = -0.5 * z
            alpha = 0.5 * z + 1.5
            # the corner of the page that you hold between your fingers
            dthumb = 0.6 * u + 1.4 * v - 2 * 0.95
            if dthumb > 0:
                z -= dthumb
                x += dthumb
                y += dthumb
                i = 1.0
                alpha = 1.0
        return (x,y,z, u,v, i, alpha)
    def render(self, t):
        glDisable(GL_BLEND)
        glBindTexture(TextureTarget, Tnext)
        DrawMeshQuad(t,lambda t, u, v: \
                    (u, v, 0.0,  u, v,  1.0 - 0.5 * (1.0 - u) * (1.0 - t), 1.0))
        EnableAlphaBlend()
        glBindTexture(TextureTarget, Tcurrent)
        DrawMeshQuad(t, self.warp)
AllTransitions.append(PageTurn)

##### some additional transitions by Rob Reid <rreid@drao.nrc.ca> #####

class ZoomOutIn(Transition):
    """zooms the current page out, and the next one in."""
    def render(self, t):
        glColor3d(0.0, 0.0, 0.0)
        DrawFullQuad()
        if t < 0.5:
            glBindTexture(TextureTarget, Tcurrent)
            scalfact = 1.0 - 2.0 * t
            DrawMeshQuad(t, lambda t, u, v: (0.5 + scalfact * (u - 0.5), \
                                             0.5 + scalfact * (v - 0.5), 0.0, \
                                             u, v, 1.0, 1.0))
        else:
            glBindTexture(TextureTarget, Tnext)
            scalfact = 2.0 * t - 1.0
            EnableAlphaBlend()
            DrawMeshQuad(t, lambda t, u, v: (0.5 + scalfact * (u - 0.5), \
                                             0.5 + scalfact * (v - 0.5), 0.0, \
                                             u, v, 1.0, 1.0))
AllTransitions.append(ZoomOutIn)

class SpinOutIn(Transition):
    """spins the current page out, and the next one in."""
    def render(self, t):
        glColor3d(0.0, 0.0, 0.0)
        DrawFullQuad()
        if t < 0.5:
            glBindTexture(TextureTarget, Tcurrent)
            scalfact = 1.0 - 2.0 * t
        else:
            glBindTexture(TextureTarget, Tnext)
            scalfact = 2.0 * t - 1.0
        sa = scalfact * sin(16.0 * t)
        ca = scalfact * cos(16.0 * t)
        DrawMeshQuad(t,lambda t, u, v: (0.5 + ca * (u - 0.5) - 0.75 * sa * (v - 0.5),\
                                        0.5 + 1.333 * sa * (u - 0.5) + ca * (v - 0.5),\
                                        0.0, u, v, 1.0, 1.0))
AllTransitions.append(SpinOutIn)

class SpiralOutIn(Transition):
    """flushes the current page away to have the next one overflow"""
    def render(self, t):
        glColor3d(0.0, 0.0, 0.0)
        DrawFullQuad()
        if t < 0.5:
            glBindTexture(TextureTarget,Tcurrent)
            scalfact = 1.0 - 2.0 * t
        else:
          glBindTexture(TextureTarget,Tnext)
          scalfact = 2.0 * t - 1.0
        sa = scalfact * sin(16.0 * t)
        ca = scalfact * cos(16.0 * t)
        DrawMeshQuad(t, lambda t, u, v: (0.5 + sa + ca * (u - 0.5) - 0.75 * sa * (v - 0.5),\
                                         0.5 + ca + 1.333 * sa * (u - 0.5) + ca * (v - 0.5),\
                                         0.0, u, v, 1.0, 1.0))
AllTransitions.append(SpiralOutIn)

# the AvailableTransitions array contains a list of all transition classes that
# can be randomly assigned to pages
AvailableTransitions=[ # from coolest to lamest
    # PagePeel, # deactivated: too intrusive
    WipeBlobs,
    WipeCenterOut,WipeCenterIn,
    WipeDownRight,WipeUpLeft,WipeDown,WipeUp,WipeRight,WipeLeft,
    Crossfade
]


##### OSD FONT RENDERER ########################################################

# force a string or sequence of ordinals into a unicode string
def ForceUnicode(s, charset='iso8859-15'):
    if type(s) == types.UnicodeType:
        return s
    if type(s) == types.StringType:
        return unicode(s, charset, 'ignore')
    if type(s) in (types.TupleType, types.ListType):
        return u''.join(map(unichr, s))
    raise TypeError, "string argument not convertible to Unicode"

# search a system font path for a font file
def SearchFont(root, name):
    if not os.path.isdir(root):
        return None
    infix = ""
    fontfile = []
    while (len(infix) < 10) and (len(fontfile) != 1):
        fontfile = filter(os.path.isfile, glob.glob(root + infix + name))
        infix += "*/"
    if len(fontfile) != 1:
        return None
    else:
        return fontfile[0]

# load a system font
def LoadFont(dirs, name, size):
    # first try to load the font directly
    try:
        return ImageFont.truetype(name, size, encoding='unic')
    except:
        pass
    # no need to search further on Windows
    if os.name == 'nt':
        return None
    # start search for the font
    for dir in dirs:
        fontfile = SearchFont(dir + "/", name)
        if fontfile:
            try:
                return ImageFont.truetype(fontfile, size, encoding='unic')
            except:
                pass
    return None

# alignment constants
Left = 0
Right = 1
Center = 2
Down = 0
Up = 1
Auto = -1

# font renderer class
class GLFont:
    def __init__(self, width, height, name, size, search_path=[], default_charset='iso8859-15', extend=1, blur=1):
        self.width = width
        self.height = height
        self._i_extend = range(extend)
        self._i_blur = range(blur)
        self.feather = extend + blur + 1
        self.current_x = 0
        self.current_y = 0
        self.max_height = 0
        self.boxes = {}
        self.widths = {}
        self.line_height = 0
        self.default_charset = default_charset
        if type(name) == types.StringType:
            self.font = LoadFont(search_path, name, size)
        else:
            for check_name in name:
                self.font = LoadFont(search_path, check_name, size)
                if self.font: break
        if not self.font:
            raise IOError, "font file not found"
        self.img = Image.new('LA', (width, height))
        self.alpha = Image.new('L', (width, height))
        self.extend = ImageFilter.MaxFilter()
        self.blur = ImageFilter.Kernel((3, 3), [1,2,1,2,4,2,1,2,1])
        self.tex = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, self.tex)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
        self.AddString(range(32, 128))

    def AddCharacter(self, c):
        w, h = self.font.getsize(c)
        self.line_height = max(self.line_height, h)
        size = (w + 2 * self.feather, h + 2 * self.feather)
        glyph = Image.new('L', size)
        draw = ImageDraw.Draw(glyph)
        draw.text((self.feather, self.feather), c, font=self.font, fill=255)
        del draw

        box = self.AllocateGlyphBox(*size)
        self.img.paste(glyph, (box.orig_x, box.orig_y))

        for i in self._i_extend: glyph = glyph.filter(self.extend)
        for i in self._i_blur:   glyph = glyph.filter(self.blur)
        self.alpha.paste(glyph, (box.orig_x, box.orig_y))

        self.boxes[c] = box
        self.widths[c] = w
        del glyph

    def AddString(self, s, charset=None, fail_silently=False):
        update_count = 0
        try:
            for c in ForceUnicode(s, self.GetCharset(charset)):
                if c in self.widths:
                    continue
                self.AddCharacter(c)
                update_count += 1
        except ValueError:
            if fail_silently:
                pass
            else:
                raise
        if not update_count: return
        self.img.putalpha(self.alpha)
        glBindTexture(GL_TEXTURE_2D, self.tex)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, \
                     self.width, self.height, 0, \
                     GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, self.img.tostring())

    def AllocateGlyphBox(self, w, h):
        if self.current_x + w > self.width:
            self.current_x = 0
            self.current_y += self.max_height
            self.max_height = 0
        if self.current_y + h > self.height:
            raise ValueError, "bitmap too small for all the glyphs"
        box = self.GlyphBox()
        box.orig_x = self.current_x
        box.orig_y = self.current_y
        box.size_x = w
        box.size_y = h
        box.x0 =  self.current_x      / float(self.width)
        box.y0 =  self.current_y      / float(self.height)
        box.x1 = (self.current_x + w) / float(self.width)
        box.y1 = (self.current_y + h) / float(self.height)
        box.dsx = w * PixelX
        box.dsy = h * PixelY
        self.current_x += w
        self.max_height = max(self.max_height, h)
        return box

    def GetCharset(self, charset=None):
        if charset: return charset
        return self.default_charset

    def SplitText(self, s, charset=None):
        return ForceUnicode(s, self.GetCharset(charset)).split(u'\n')

    def GetLineHeight(self):
        return self.line_height

    def GetTextWidth(self, s, charset=None):
        return max([self.GetTextWidthEx(line) for line in self.SplitText(s, charset)])

    def GetTextHeight(self, s, charset=None):
        return len(self.SplitText(s, charset)) * self.line_height

    def GetTextSize(self, s, charset=None):
        lines = self.SplitText(s, charset)
        return (max([self.GetTextWidthEx(line) for line in lines]), len(lines) * self.line_height)

    def GetTextWidthEx(self, u):
        if u: return sum([self.widths.get(c, 0) for c in u])
        else: return 0

    def GetTextHeightEx(self, u=[]):
        return self.line_height

    def AlignTextEx(self, x, u, align=Left):
        if not align: return x
        return x - (self.GetTextWidthEx(u) / align)

    def Draw(self, origin, text, charset=None, align=Left, color=(1.0, 1.0, 1.0), alpha=1.0, beveled=True):
        lines = self.SplitText(text, charset)
        x0, y0 = origin
        x0 -= self.feather
        y0 -= self.feather
        glEnable(GL_TEXTURE_2D)
        glEnable(GL_BLEND)
        glBindTexture(GL_TEXTURE_2D, self.tex)
        if beveled:
            glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_ALPHA)
            glColor4d(0.0, 0.0, 0.0, alpha)
            self.DrawLinesEx(x0, y0, lines, align)
        glBlendFunc(GL_ONE, GL_ONE)
        glColor3d(color[0] * alpha, color[1] * alpha, color[2] * alpha)
        self.DrawLinesEx(x0, y0, lines, align)
        glDisable(GL_BLEND)
        glDisable(GL_TEXTURE_2D)

    def DrawLinesEx(self, x0, y, lines, align=Left):
        global PixelX, PixelY
        glBegin(GL_QUADS)
        for line in lines:
            sy = y * PixelY
            x = self.AlignTextEx(x0, line, align)
            for c in line:
                if not c in self.widths: continue
                self.boxes[c].render(x * PixelX, sy)
                x += self.widths[c]
            y += self.line_height
        glEnd()

    class GlyphBox:
        def render(self, sx=0.0, sy=0.0):
            glTexCoord2d(self.x0, self.y0); glVertex2d(sx,          sy)
            glTexCoord2d(self.x0, self.y1); glVertex2d(sx,          sy+self.dsy)
            glTexCoord2d(self.x1, self.y1); glVertex2d(sx+self.dsx, sy+self.dsy)
            glTexCoord2d(self.x1, self.y0); glVertex2d(sx+self.dsx, sy)

# high-level draw function
def DrawOSD(x, y, text, halign=Auto, valign=Auto, alpha=1.0):
    if not(OSDFont) or not(text) or (alpha <= 0.004): return
    if alpha > 1.0: alpha = 1.0
    if halign == Auto:
        if x < 0:
            x += ScreenWidth
            halign = Right
        else:
            halign = Left
    if valign == Auto:
        if y < 0:
            y += ScreenHeight
            valign = Up
        else:
            valign = Down
        if valign != Down:
            y -= OSDFont.GetLineHeight() / valign
    if TextureTarget != GL_TEXTURE_2D:
        glDisable(TextureTarget)
    OSDFont.Draw((x, y), text, align=halign, alpha=alpha)

# very high-level draw function
def DrawOSDEx(position, text, alpha_factor=1.0):
    xpos = position >> 1
    y = (1 - 2 * (position & 1)) * OSDMargin
    if xpos < 2:
        x = (1 - 2 * xpos) * OSDMargin
        halign = Auto
    else:
        x = ScreenWidth / 2
        halign = Center
    DrawOSD(x, y, text, halign, alpha = OSDAlpha * alpha_factor)


##### PAGE RENDERING ###########################################################

# generate a dummy image
def DummyPage():
    img = Image.new('RGB', (ScreenWidth, ScreenHeight))
    img.paste(LogoImage, ((ScreenWidth  - LogoImage.size[0]) / 2,
                          (ScreenHeight - LogoImage.size[1]) / 2))
    return img

# load a page from a PDF file
def RenderPDF(page, MayAdjustResolution, ZoomMode):
    global Resolution, UseGhostScript
    UseGhostScriptOnce = False

    if Supersample and not(ZoomMode):
        UseRes = int(0.5 + Resolution) * Supersample
        AlphaBits = 1
    else:
        UseRes = int(0.5 + Resolution)
        AlphaBits = 4
    if ZoomMode:
        UseRes = 2 * UseRes

    # call pdftoppm to generate the page image
    if not UseGhostScript:
        imgfile = TempFileName + "-%06d.ppm" % page
        renderer = "pdftoppm"
        try:
            assert 0 == spawn(os.P_WAIT, \
                pdftoppmPath, ["pdftoppm", "-q"] + [ \
                "-f", str(page), "-l", str(page),
                "-r", str(int(UseRes)),
                FileNameEscape + FileName + FileNameEscape,
                TempFileName])
        except OSError, (errcode, errmsg):
            print >>sys.stderr, "Warning: Cannot start pdftoppm -", errmsg
            print >>sys.stderr, "Falling back to GhostScript (permanently)."
            UseGhostScript = True
        except AssertionError:
            print >>sys.stderr, "There was an error while rendering page %d" % page
            print >>sys.stderr, "Falling back to GhostScript for this page."
            UseGhostScriptOnce = True

    # fallback to GhostScript
    if UseGhostScript or UseGhostScriptOnce:
        imgfile = TempFileName + ".tif"
        renderer = "GhostScript"
        try:
            assert 0 == spawn(os.P_WAIT, \
                GhostScriptPath, ["gs", "-q"] + GhostScriptPlatformOptions + [ \
                "-dBATCH", "-dNOPAUSE", "-sDEVICE=tiff24nc", "-dUseCropBox",
                "-sOutputFile=" + imgfile, \
                "-dFirstPage=%d" % page, "-dLastPage=%d" % page,
                "-r%dx%d" % (UseRes, int(UseRes * PAR)), \
                "-dTextAlphaBits=%d" % AlphaBits, \
                "-dGraphicsAlphaBits=%s" % AlphaBits, \
                FileNameEscape + FileName + FileNameEscape])
        except OSError, (errcode, errmsg):
            print >>sys.stderr, "Error: Cannot start GhostScript -", errmsg
            return DummyPage()
        except AssertionError:
            print >>sys.stderr, "There was an error while rendering page %d" % page
            return DummyPage()

    # open the page image file with PIL
    try:
        img = Image.open(imgfile)
    except:
        print >>sys.stderr, "Error: %s produced an unreadable file (page %d)" % (renderer, page)
        return DummyPage()

    # try to delete the file again (this constantly fails on Win32 ...)
    try:
        os.remove(TempFileName + ".tif")
    except OSError:
        pass

    # apply rotation
    if Rotation:
        img = img.rotate(90 * (4 - Rotation))

    # determine real display size (don't care for ZoomMode, DisplayWidth and
    # DisplayHeight are only used for Supersample and AdjustResolution anyway)
    if Supersample:
        DisplayWidth  = img.size[0] / Supersample
        DisplayHeight = img.size[1] / Supersample
    else:
        DisplayWidth  = img.size[0]
        DisplayHeight = img.size[1]

    # if the image size is strange, re-adjust the rendering resolution
    if MayAdjustResolution \
    and ((abs(ScreenWidth  - DisplayWidth)  > 4) \
    or   (abs(ScreenHeight - DisplayHeight) > 4)):
        newsize = ZoomToFit((DisplayWidth,DisplayHeight))
        NewResolution = newsize[0] * Resolution/DisplayWidth
        if abs(1.0 - NewResolution / Resolution) > 0.05:
            # only modify anything if the resolution deviation is large enough
            Resolution = NewResolution
            return RenderPDF(page, False, ZoomMode)

    # downsample a supersampled image
    if Supersample and not(ZoomMode):
        return img.resize((DisplayWidth, DisplayHeight), Image.ANTIALIAS)

    return img


# load a page from an image file
def LoadImage(page,ZoomMode):
    # open the image file with PIL
    try:
        img = Image.open(os.path.join(FileName, FileList[page - 1]))
    except:
        print >>sys.stderr, "Image file `%s' is broken." % (FileList[page - 1])
        return DummyImage()

    # determine destination size
    newsize = ZoomToFit(img.size)
    # don't scale if the source size is too close to the destination size
    if abs(newsize[0] - img.size[0]) < 2: newsize = img.size
    # don't scale if the source is smaller than the destination
    if not(Scaling) and (newsize > img.size): newsize = img.size
    # zoom up (if wanted)
    if ZoomMode: newsize=(2 * newsize[0], 2 * newsize[1])
    # skip processing if there was no change
    if newsize == img.size: return img

    # select a nice filter and resize the image
    if newsize > img.size:
      filter = Image.BICUBIC
    else:
      filter = Image.ANTIALIAS
    return img.resize(newsize, filter)


# render a page to an OpenGL texture
def PageImage(page, ZoomMode=False, RenderMode=False):
    global CacheFilePos, OverviewNeedUpdate

    # check for the image in the cache
    Lcache.acquire()
    try:
        if (page in PageCache) and not(ZoomMode or RenderMode):
            if CacheFile:
                CacheFile.seek(PageCache[page])
                return CacheFile.read(TexSize)
            else:
                return PageCache[page]
    finally:
      Lcache.release()

    # if it's not cached, render it
    Lrender.acquire()
    try:
        if PDFinput:
            img = RenderPDF(page, not(ZoomMode), ZoomMode)
        else:
            img = LoadImage(page, ZoomMode)

        # create black background image to paste real image onto
        if ZoomMode:
            TextureImage = Image.new('RGB', (2 * TexWidth, 2 * TexHeight))
            TextureImage.paste(img, ((2 * ScreenWidth  - img.size[0]) / 2, \
                                     (2 * ScreenHeight - img.size[1]) / 2))
        else:
            TextureImage=Image.new('RGB', (TexWidth, TexHeight))
            TextureImage.paste(img, ((ScreenWidth  - img.size[0]) / 2, \
                                     (ScreenHeight - img.size[1]) / 2))

        # paste thumbnail into overview image
        if GetPageProp(page, 'overview', True) \
        and (page >= PageRangeStart) and (page <= PageRangeEnd) \
        and not(GetPageProp(page, 'OverviewRendered')) \
        and not(RenderMode):
            pos = OverviewPos(OverviewPageMapInv[page])
            Loverview.acquire()
            try:
                # first, fill the underlying area with black (i.e. remove the dummy logo)
                blackness = Image.new('RGB', (OverviewCellX - OverviewBorder, \
                                              OverviewCellY - OverviewBorder))
                OverviewImage.paste(blackness, (pos[0] + OverviewBorder / 2, \
                                                pos[1] + OverviewBorder))
                del blackness
                # then, scale down the original image and paste it
                img.thumbnail((OverviewCellX - 2 * OverviewBorder, \
                               OverviewCellY - 2 * OverviewBorder), \
                               Image.ANTIALIAS)
                OverviewImage.paste(img, \
                   (pos[0] + (OverviewCellX - img.size[0]) / 2, \
                    pos[1] + (OverviewCellY - img.size[1]) / 2))
            finally:
                Loverview.release()
            SetPageProp(page, 'OverviewRendered', True)
            OverviewNeedUpdate = True
        del img

        # return texture data
        if RenderMode:
            return TextureImage
        data=TextureImage.tostring()
        del TextureImage
    finally:
      Lrender.release()

    # finally add it back into the cache and return it
    if UseCache and not(ZoomMode) \
    and (page >= PageRangeStart) and (page <= PageRangeEnd):
        Lcache.acquire()
        try:
            if CacheFile:
                CacheFile.seek(CacheFilePos)
                CacheFile.write(data)
                PageCache[page] = CacheFilePos
                CacheFilePos += len(data)
            else:
                PageCache[page] = data
        finally:
            Lcache.release()
    return data

# render a page to an OpenGL texture
def RenderPage(page, target):
    glBindTexture(TextureTarget ,target)
    try:
        glTexImage2D(TextureTarget, 0, 3, TexWidth, TexHeight, 0,\
                     GL_RGB, GL_UNSIGNED_BYTE, PageImage(page))
    except GLerror:
        print >>sys.stderr, "I'm sorry, but your graphics card is not capable of rendering presentations"
        print >>sys.stderr, "in this resolution. Either the texture memory is exhausted, or there is no"
        print >>sys.stderr, "support for large textures (%dx%d). Please try to run KeyJnote in a" % (TexWidth, TexHeight)
        print >>sys.stderr, "smaller resolution using the -g command-line option."
        sys.exit(1)

# background rendering thread
def RenderThread(p1, p2):
    global RTrunning, RTrestart
    RTrunning = True
    RTrestart = True
    while RTrestart:
        RTrestart = False
        for page in xrange(1, PageCount + 1):
            if RTrestart: break
            if (page != p1) and (page != p2) \
            and (page >= PageRangeStart) and (page <= PageRangeEnd):
                PageImage(page)
    RTrunning = False
    if CacheFile:
        print >>sys.stderr, "Background rendering finished, used %.1f MiB of disk space." %\
              (CacheFilePos / 1048576.0)

##### INFO SCRIPT I/O ##########################################################

# info script reader
def LoadInfoScript():
    global PageProps
    try:
        OldPageProps = PageProps
        execfile(InfoScriptPath, globals())
        NewPageProps = PageProps
        PageProps = OldPageProps
        del OldPageProps
        PageProps.update(NewPageProps)
        del NewPageProps
    except IOError:
        pass
    except:
        print >>sys.stderr, 79 * '-'
        print >>sys.stderr, "Oops! The info script is damaged!"
        print >>sys.stderr, 79 * '-'
        raise

# we can't save lamba expressions, so we need to warn the user
# in every possible way
ScriptTainted = False
LambdaWarning = False
def here_was_a_lambda_expression_that_could_not_be_saved():
    global LambdaWarning
    if not LambdaWarning:
        print >>sys.stderr, "WARNING: The info script for the current file contained lambda expressions that"
        print >>sys.stderr, "         were removed during the a save operation."
        LambdaWarning = True

# "clean" a PageProps entry so that only 'public' properties are left
def GetPublicProps(props):
    props = props.copy()
    for priv in ('private_title', 'shown', 'OverviewRendered'):
        if priv in props:
            del props[priv]
    if props.get('overview', False):
        del props['overview']
    if not props.get('skip', True):
        del props['skip']
    if 'IsRandomTransition' in props:
        del props['IsRandomTransition']
        del props['transition']
    if ('boxes' in props) and not(props['boxes']):
        del props['boxes']
    return props

# Generate a string representation of a property value. Mainly this converts
# classes or instances to the name of the class.
def PropValueRepr(value):
    global ScriptTainted
    if type(value) == types.FunctionType:
        if value.__name__ != "<lambda>":
            return value.__name__
        if not ScriptTainted:
            print >>sys.stderr, "WARNING: The info script contains lambda expressions, which cannot be saved"
            print >>sys.stderr, "         back. The modifed script will be written into a separate file to"
            print >>sys.stderr, "         minimize data loss."
            ScriptTainted = True
        return "here_was_a_lambda_expression_that_could_not_be_saved"
    elif type(value) == types.ClassType:
        return value.__name__
    elif type(value) == types.InstanceType:
        return value.__class__.__name__
    else:
        return repr(value)

# generate a nicely formatted string representation of a page's properties
def SinglePagePropRepr(page):
    props = GetPublicProps(PageProps[page])
    if not props: return None
    return "\n%3d: {%s\n     }" % (page, \
        ",".join(["\n       " + repr(prop) + ": " + PropValueRepr(props[prop]) for prop in props]))

# generate a nicely formatted string representation of all page properties
def PagePropRepr():
    pages = PageProps.keys()
    pages.sort()
    return "PageProps = {%s\n}" % (",".join(filter(None, map(SinglePagePropRepr, pages))))

# count the characters of a python dictionary source code, correctly handling
# embedded strings and comments, and nested dictionaries
def CountDictChars(s, start=0):
    context = None
    level = 0
    for i in xrange(start, len(s)):
        c = s[i]
        if context is None:
            if c == '{': level += 1
            if c == '}': level -= 1
            if c == '#': context = '#'
            if c == '"': context = '"'
            if c == "'": context = "'"
        elif context[0] == "\\":
            context=context[1]
        elif context == '#':
            if c in "\r\n": context = None
        elif context == '"':
            if c == "\\": context = "\\\""
            if c == '"': context = None
        elif context == "'":
            if c == "\\": context = "\\'"
            if c == "'": context = None
        if level < 0: return i
    raise ValueError, "the dictionary never ends"

# modify and save a file's info script
def SaveInfoScript(filename):
    print filename
    # read the old info script
    try:
        f = file(filename, "r")
        script = f.read()
        f.close()
    except IOError:
        script = ""
    if not script:
        script = "# -*- coding: iso-8859-1 -*-\n"

    # replace the PageProps of the old info script with the current ones
    try:
        m = re.search("^.*(PageProps)\s*=\s*(\{).*$", script,re.MULTILINE)
        if m:
            script = script[:m.start(1)] + PagePropRepr() + \
                     script[CountDictChars(script, m.end(2)) + 1 :]
        else:
            script += "\n" + PagePropRepr() + "\n"
    except (AttributeError, ValueError):
        pass

    if ScriptTainted:
        filename += ".modified"

    # write the script back
    try:
        f = file(filename, "w")
        f.write(script)
        f.close()
    except:
        print >>sys.stderr, "Oops! Could not write info script!"


##### OPENGL RENDERING #########################################################

# helper function: draw a translated fullscreen quad
def DrawTranslatedFullQuad(dx, dy, i, a):
    glColor4d(i, i, i, a)
    glPushMatrix()
    glTranslated(dx, dy, 0.0)
    DrawFullQuad()
    glPopMatrix()

# draw a vertex in normalized screen coordinates,
# setting texture coordinates appropriately
def DrawPoint(x, y):
    glTexCoord2d(x *TexMaxS, y * TexMaxT)
    glVertex2d(x, y)
def DrawPointEx(x, y, a):
    glColor4d(1.0, 1.0, 1.0, a)
    glTexCoord2d(x * TexMaxS, y * TexMaxT)
    glVertex2d(x, y)

# draw OSD overlays
def DrawOverlays():
    reltime = pygame.time.get_ticks() - StartTime
    if EstimatedDuration and (OverviewMode or GetPageProp(Pcurrent, 'progress', True)):
        rel = (0.001 * reltime) / EstimatedDuration
        x = int(ScreenWidth * rel)
        y = 1.0 - ProgressBarSize * PixelX
        a = min(255, max(0, x - ScreenWidth))
        b = min(255, max(0, x - ScreenWidth - 256))
        r = a
        g = 255 - b
        b = 0
        glDisable(TextureTarget)
        glDisable(GL_TEXTURE_2D)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glBegin(GL_QUADS)
        glColor4ub(r, g, b, 0)
        glVertex2d(0, y)
        glVertex2d(rel, y)
        glColor4ub(r, g, b, ProgressBarAlpha)
        glVertex2d(rel, 1.0)
        glVertex2d(0, 1.0)
        glEnd()
        glDisable(GL_BLEND)
    if WantStatus:
        DrawOSDEx(OSDStatusPos, CurrentOSDStatus)
    if TimeDisplay:
        t = reltime / 1000
        DrawOSDEx(OSDTimePos, FormatTime(t, MinutesOnly))
    if CursorImage and CursorVisible:
        x, y = pygame.mouse.get_pos()
        x -= CursorHotspot[0]
        y -= CursorHotspot[1]
        X0 = x * PixelX
        Y0 = y * PixelY
        X1 = X0 + CursorSX
        Y1 = Y0 + CursorSY
        glDisable(TextureTarget)
        glEnable(GL_TEXTURE_2D)
        glBindTexture(GL_TEXTURE_2D, CursorTexture)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glColor4ub(255, 255, 255, 255)
        glBegin(GL_QUADS)
        glTexCoord2d(0.0,      0.0);       glVertex2d(X0, Y0)
        glTexCoord2d(CursorTX, 0.0);       glVertex2d(X1, Y0)
        glTexCoord2d(CursorTX, CursorTY);  glVertex2d(X1, Y1)
        glTexCoord2d(0.0,      CursorTY);  glVertex2d(X0, Y1)
        glEnd()
        glDisable(GL_BLEND)
        glDisable(GL_TEXTURE_2D)

# draw the complete image of the current page
def DrawCurrentPage(dark=1.0, do_flip=True):
    boxes = GetPageProp(Pcurrent, 'boxes')

    # pre-transform for zoom
    glLoadIdentity()
    glOrtho(ZoomX0, ZoomX0 + ZoomArea,  ZoomY0 + ZoomArea, ZoomY0,  -10.0, 10.0)

    # background layer -- the page's image, darkened if it has boxes
    glDisable(GL_BLEND)
    glEnable(TextureTarget)
    glBindTexture(TextureTarget, Tcurrent)
    if boxes or Tracing:
        light = 1.0 - 0.25 * dark
    else:
        light = 1.0
    glColor3d(light, light, light)
    DrawFullQuad()

    if boxes or Tracing:
        # alpha-blend the same image some times to blur it
        EnableAlphaBlend()
        DrawTranslatedFullQuad(+PixelX * ZoomArea, 0.0, light, dark / 2)
        DrawTranslatedFullQuad(-PixelX * ZoomArea, 0.0, light, dark / 3)
        DrawTranslatedFullQuad(0.0, +PixelY * ZoomArea, light, dark / 4)
        DrawTranslatedFullQuad(0.0, -PixelY * ZoomArea, light, dark / 5)

    if boxes:
        # draw outer box fade
        EnableAlphaBlend()
        for X0, Y0, X1, Y1 in boxes:
            glBegin(GL_QUAD_STRIP)
            DrawPointEx(X0, Y0, 1);  DrawPointEx(X0 - EdgeX, Y0 - EdgeY, 0)
            DrawPointEx(X1, Y0, 1);  DrawPointEx(X1 + EdgeX, Y0 - EdgeY, 0)
            DrawPointEx(X1, Y1, 1);  DrawPointEx(X1 + EdgeX, Y1 + EdgeY, 0)
            DrawPointEx(X0, Y1, 1);  DrawPointEx(X0 - EdgeX, Y1 + EdgeY, 0)
            DrawPointEx(X0, Y0, 1);  DrawPointEx(X0 - EdgeX, Y0 - EdgeY, 0)
            glEnd()

        # draw boxes
        glDisable(GL_BLEND)
        glBegin(GL_QUADS)
        for X0, Y0, X1, Y1 in boxes:
            DrawPoint(X0, Y0)
            DrawPoint(X1, Y0)
            DrawPoint(X1, Y1)
            DrawPoint(X0, Y1)
        glEnd()

    if Tracing:
        x, y = MouseToScreen(pygame.mouse.get_pos())
        # outer spot fade
        EnableAlphaBlend()
        glBegin(GL_TRIANGLE_STRIP)
        for x0, y0, x1, y1 in SpotMesh:
            DrawPointEx(x + x0, y + y0, 1)
            DrawPointEx(x + x1, y + y1, 0)
        glEnd()
        # inner spot
        glDisable(GL_BLEND)
        glBegin(GL_TRIANGLE_FAN)
        DrawPoint(x, y)
        for x0, y0, x1, y1 in SpotMesh:
            DrawPoint(x + x0, y + y0)
        glEnd()

    if Marking:
        # soft alpha-blended rectangle
        glDisable(TextureTarget)
        glColor4d(*MarkColor)
        EnableAlphaBlend()
        glBegin(GL_QUADS)
        glVertex2d(MarkUL[0], MarkUL[1])
        glVertex2d(MarkLR[0], MarkUL[1])
        glVertex2d(MarkLR[0], MarkLR[1])
        glVertex2d(MarkUL[0], MarkLR[1])
        glEnd()
        # bright red frame
        glDisable(GL_BLEND)
        glBegin(GL_LINE_STRIP)
        glVertex2d(MarkUL[0], MarkUL[1])
        glVertex2d(MarkLR[0], MarkUL[1])
        glVertex2d(MarkLR[0], MarkLR[1])
        glVertex2d(MarkUL[0], MarkLR[1])
        glVertex2d(MarkUL[0], MarkUL[1])
        glEnd()
        glEnable(TextureTarget)

    # unapply the zoom transform
    glLoadIdentity()
    glOrtho(0.0, 1.0,  1.0, 0.0,  -10.0, 10.0)

    # Done.
    DrawOverlays()
    if do_flip:
        pygame.display.flip()

# draw a black screen with the KeyJnote logo at the center
def DrawLogo():
    glClear(GL_COLOR_BUFFER_BIT)
    glColor3ub(255, 255, 255)
    if TextureTarget != GL_TEXTURE_2D:
        glDisable(TextureTarget)
    glEnable(GL_TEXTURE_2D)
    glBindTexture(GL_TEXTURE_2D, LogoTexture)
    glBegin(GL_QUADS)
    glTexCoord2d(0, 0);  glVertex2d(0.5 - 128.0 / ScreenWidth, 0.5 - 32.0 / ScreenHeight)
    glTexCoord2d(1, 0);  glVertex2d(0.5 + 128.0 / ScreenWidth, 0.5 - 32.0 / ScreenHeight)
    glTexCoord2d(1, 1);  glVertex2d(0.5 + 128.0 / ScreenWidth, 0.5 + 32.0 / ScreenHeight)
    glTexCoord2d(0, 1);  glVertex2d(0.5 - 128.0 / ScreenWidth, 0.5 + 32.0 / ScreenHeight)
    glEnd()
    if OSDFont:
        OSDFont.Draw((ScreenWidth / 2, ScreenHeight / 2 + 48), \
                     __version__, align=Center, alpha=0.25)
    glDisable(GL_TEXTURE_2D)

# draw the prerender progress bar
def DrawProgress(position):
    glDisable(TextureTarget)
    x0 = 0.1
    x2 = 1.0 - x0
    x1 = position * x2 + (1.0 - position) * x0
    y1 = 0.9
    y0 = y1 - 16.0 / ScreenHeight
    glBegin(GL_QUADS)
    glColor3ub( 64,  64,  64);  glVertex2d(x0, y0);  glVertex2d(x2, y0)
    glColor3ub(128, 128, 128);  glVertex2d(x2, y1);  glVertex2d(x0, y1)
    glColor3ub( 64, 128, 255);  glVertex2d(x0, y0);  glVertex2d(x1, y0)
    glColor3ub(  8,  32, 128);  glVertex2d(x1, y1);  glVertex2d(x0, y1)
    glEnd()
    glEnable(TextureTarget)

# fade mode
def DrawFadeMode(intensity, alpha):
    DrawCurrentPage(do_flip=False)
    glDisable(TextureTarget)
    EnableAlphaBlend()
    glColor4d(intensity, intensity, intensity, alpha)
    DrawFullQuad()
    glEnable(TextureTarget)
    pygame.display.flip()

def FadeMode(intensity):
    t0 = pygame.time.get_ticks()
    while True:
        if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]): break
        t = (pygame.time.get_ticks() - t0) * 1.0 / BlankFadeDuration
        if t >= 1.0: break
        DrawFadeMode(intensity, t)
    DrawFadeMode(intensity, 1.0)

    while True:
        event = pygame.event.wait()
        if event.type == QUIT:
            PageLeft()
            Quit()
        elif event.type == VIDEOEXPOSE:
            DrawFadeMode(intensity, 1.0)
        elif event.type == MOUSEBUTTONUP:
            break
        elif event.type == KEYDOWN:
            if event.unicode == u'q':
                pygame.event.post(pygame.event.Event(QUIT))
            else:
                break

    t0 = pygame.time.get_ticks()
    while True:
        if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]): break
        t = (pygame.time.get_ticks() - t0) * 1.0 / BlankFadeDuration
        if t >= 1.0: break
        DrawFadeMode(intensity, 1.0 - t)
    DrawCurrentPage()

# gamma control
def SetGamma(new_gamma=None, new_black=None, force=False):
    global Gamma, BlackLevel
    if new_gamma is None: new_gamma = Gamma
    if new_gamma <  0.1:  new_gamma = 0.1
    if new_gamma > 10.0:  new_gamma = 10.0
    if new_black is None: new_black = BlackLevel
    if new_black <   0:   new_black = 0
    if new_black > 254:   new_black = 254
    if not(force) and (abs(Gamma - new_gamma) < 0.01) and (new_black == BlackLevel):
        return
    Gamma = new_gamma
    BlackLevel = new_black
    scale = 1.0 / (255 - BlackLevel)
    power = 1.0 / Gamma
    ramp = [int(65535.0 * ((max(0, x - BlackLevel) * scale) ** power)) for x in range(256)]
    return pygame.display.set_gamma_ramp(ramp, ramp, ramp)

# cursor image
def PrepareCustomCursor(cimg):
    global CursorTexture, CursorSX, CursorSY, CursorTX, CursorTY
    w, h = cimg.size
    tw, th = map(npot, cimg.size)
    if (tw > 256) or (th > 256):
        print >>sys.stderr, "Custom cursor is rediculously large, reverting to normal one."
        return False
    img = Image.new('RGBA', (tw, th))
    img.paste(cimg, (0, 0))
    CursorTexture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, CursorTexture)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tw, th, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.tostring())
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    CursorSX = w * PixelX
    CursorSY = h * PixelY
    CursorTX = w / float(tw)
    CursorTY = h / float(th)
    return True


##### CONTROL AND NAVIGATION ###################################################

# update the applications' title bar
def UpdateCaption(page=0, force=False):
    global CurrentCaption, CurrentOSDCaption, CurrentOSDPage, CurrentOSDStatus
    if (page == CurrentCaption) and not(force):
        return
    CurrentCaption = page
    caption = "%s - %s" % (__title__, DocumentTitle)
    if page < 1:
        CurrentOSDCaption = ""
        CurrentOSDPage = ""
        CurrentOSDStatus = ""
        pygame.display.set_caption(caption, __title__)
        return
    CurrentOSDPage = "%d/%d" % (page, PageCount)
    caption = "%s (%s)" % (caption, CurrentOSDPage)
    title = GetPageProp(page, 'title') or GetPageProp(page, 'private_title')
    if title:
        caption += ": %s" % title
        CurrentOSDCaption = title
    else:
        CurrentOSDCaption = ""
    status = []
    if GetPageProp(page, 'skip', False):
        status.append("skipped: yes")
    if not GetPageProp(page, 'overview', True):
        status.append("on overview page: no")
    CurrentOSDStatus = ", ".join(status)
    pygame.display.set_caption(caption, __title__)

# get next/previous page
def GetNextPage(page, direction):
    try_page = page
    while True:
        try_page += direction
        if try_page == page:
            return 0  # tried all pages, but none found
        if Wrap:
            if try_page < 1: try_page = PageCount
            if try_page > PageCount: try_page = 1
        else:
            if try_page < 1 or try_page > PageCount:
                return 0  # start or end of presentation
        if not GetPageProp(try_page, 'skip', False):
            return try_page

# pre-load the following page into Pnext/Tnext
def PreloadNextPage(page):
    global Pnext, Tnext
    if (page < 1) or (page > PageCount):
        Pnext = 0
        return 0
    if page == Pnext:
        return 1
    RenderPage(page, Tnext)
    Pnext = page
    return 1

# perform box fading; the fade animation time is mapped through func()
def BoxFade(func):
    t0 = pygame.time.get_ticks()
    while 1:
        if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]): break
        t = (pygame.time.get_ticks() - t0) * 1.0 / BoxFadeDuration
        if t >= 1.0: break
        DrawCurrentPage(func(t))
    DrawCurrentPage(func(1.0))
    return 0

# reset the timer
def ResetTimer():
    global StartTime, PageEnterTime
    if TimeTracking and not(FirstPage):
        print "--- timer was reset here ---"
    StartTime = pygame.time.get_ticks()
    PageEnterTime = 0

# called each time a page is entered
def PageEntered(update_time=True):
    global PageEnterTime, SoundPlayerPID, IsZoomed, WantStatus
    if update_time:
        PageEnterTime = pygame.time.get_ticks() - StartTime
    IsZoomed = False  # no, we don't have a pre-zoomed image right now
    WantStatus = False  # don't show status unless it's changed interactively
    timeout = AutoAdvance
    shown = GetPageProp(Pcurrent, 'shown', 0)
    if not shown:
        timeout = GetPageProp(Pcurrent, 'timeout', timeout)
        sound = GetPageProp(Pcurrent, 'sound')
        if sound:
            StopSound()
            try:
                SoundPlayerPID = spawn(os.P_NOWAIT, \
                    SoundPlayerPath, [SoundPlayerPath] + SoundPlayerOptions + \
                    [FileNameEscape + sound + FileNameEscape])
            except OSError:
                SoundPlayerPID = 0
        func = GetPageProp(Pcurrent, 'OnEnterOnce')
        if func: func()
    func = GetPageProp(Pcurrent, 'OnEnter')
    if func: func()
    if timeout: pygame.time.set_timer(USEREVENT_PAGE_TIMEOUT, timeout)
    PageProps[Pcurrent]['shown'] = shown + 1

# called each time a page is left
def PageLeft(overview=False):
    global FirstPage, LastPage, WantStatus
    WantStatus = False
    if not overview:
        if GetTristatePageProp(Pcurrent, 'reset'):
            ResetTimer()
        FirstPage = False
        LastPage = Pcurrent
        func = GetPageProp(Pcurrent, 'OnLeaveOnce')
        if func and (GetPageProp(Pcurrent, 'shown', 0) == 1): func()
        func = GetPageProp(Pcurrent, 'OnLeave')
        if func: func()
    if TimeTracking:
        t1 = pygame.time.get_ticks() - StartTime
        dt = (t1 - PageEnterTime + 500) / 1000
        if overview:
            p = "over"
        else:
            p = "%4d" % Pcurrent
        print "%s%9s%9s%9s" % (p, FormatTime(dt), \
                                  FormatTime(PageEnterTime / 1000), \
                                  FormatTime(t1 / 1000))

# perform a transition to a specified page
def TransitionTo(page):
    global Pcurrent, Pnext, Tcurrent, Tnext
    global PageCount, Marking, Tracing, Panning

    # first, stop the auto-timer
    pygame.time.set_timer(USEREVENT_PAGE_TIMEOUT, 0)

    # invalid page? go away
    if not PreloadNextPage(page):
        return 0

    # notify that the page has been left
    PageLeft()

    # box fade-out
    if GetPageProp(Pcurrent, 'boxes') or Tracing:
        skip = BoxFade(lambda t: 1.0 - t)
    else:
        skip = 0

    # some housekeeping
    Marking = False
    Tracing = False
    UpdateCaption(page)

    # check if the transition is valid
    tpage = min(Pcurrent, Pnext)
    trans = PageProps[tpage]['transition']
    if trans is None:
        transtime = 0
    else:
        transtime = GetPageProp(tpage, 'transtime', TransitionDuration)
        try:
            dummy = trans.__class__
        except AttributeError:
            # ah, gotcha! the transition is not yet intantiated!
            trans = trans()
            PageProps[tpage]['transition'] = trans

    # backward motion? then swap page buffers now
    backward = (Pnext < Pcurrent)
    if backward:
        Pcurrent, Pnext = (Pnext, Pcurrent)
        Tcurrent, Tnext = (Tnext, Tcurrent)

    # transition animation
    if not(skip) and transtime:
        transtime = 1.0 / transtime
        t0 = pygame.time.get_ticks()
        while True:
            if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]):
                skip = 1
                break
            t = (pygame.time.get_ticks() - t0) * transtime
            if t >= 1.0: break
            if backward: t = 1.0 - t
            glEnable(TextureTarget)
            trans.render(t)
            DrawOverlays()
            pygame.display.flip()

    # forward motion => swap page buffers now
    if not backward:
        Pcurrent, Pnext = (Pnext, Pcurrent)
        Tcurrent, Tnext = (Tnext, Tcurrent)

    # box fade-in
    if not(skip) and GetPageProp(Pcurrent, 'boxes'): BoxFade(lambda t: t)

    # finally update the screen and preload the next page
    DrawCurrentPage() # I do that twice because for some strange reason, the
    PageEntered()
    if not PreloadNextPage(GetNextPage(Pcurrent, 1)):
        PreloadNextPage(GetNextPage(Pcurrent, -1))
    return 1

# zoom mode animation
def ZoomAnimation(targetx, targety, func):
    global ZoomX0, ZoomY0, ZoomArea
    t0 = pygame.time.get_ticks()
    while True:
        if pygame.event.get([KEYDOWN,MOUSEBUTTONUP]): break
        t = (pygame.time.get_ticks() - t0)* 1.0 / ZoomDuration
        if t >= 1.0: break
        t = func(t)
        t = (2.0 - t) * t
        ZoomX0 = targetx * t
        ZoomY0 = targety * t
        ZoomArea = 1.0 - 0.5 * t
        DrawCurrentPage()
    t = func(1.0)
    ZoomX0 = targetx * t
    ZoomY0 = targety * t
    ZoomArea = 1.0 - 0.5 * t
    GenerateSpotMesh()
    DrawCurrentPage()

# enter zoom mode
def EnterZoomMode(targetx, targety):
    global ZoomMode, IsZoomed, ZoomWarningIssued
    ZoomAnimation(targetx, targety, lambda t: t)
    ZoomMode = True
    if TextureTarget != GL_TEXTURE_2D:
        if not ZoomWarningIssued:
            print >>sys.stderr, "Sorry, but I can't increase the detail level in zoom mode any further when"
            print >>sys.stderr, "GL_ARB_texture_rectangle is used. Please try running KeyJnote with the"
            print >>sys.stderr, "'-e' parameter. If a modern nVidia or ATI graphics card is used, a driver"
            print >>sys.stderr, "update may also fix the problem."
            ZoomWarningIssued = True
        return
    if not IsZoomed:
        glBindTexture(TextureTarget, Tcurrent)
        try:
            glTexImage2D(TextureTarget, 0, 3, TexWidth * 2, TexHeight * 2, 0, \
                         GL_RGB, GL_UNSIGNED_BYTE, PageImage(Pcurrent, True))
        except GLerror:
            if not ZoomWarningIssued:
                print >>sys.stderr, "Sorry, but I can't increase the detail level in zoom mode any further, because"
                print >>sys.stderr, "your OpenGL implementation does not support that. Either the texture memory is"
                print >>sys.stderr, "exhausted, or there is no support for large textures (%dx%d). If you really" % (TexWidth * 2, TexHeight * 2)
                print >>sys.stderr, "need high-res zooming, please try to run KeyJnote in a smaller resolution"
                print >>sys.stderr, "using the -g command-line option."
                ZoomWarningIssued = True
            return
        DrawCurrentPage()
        IsZoomed = True

# leave zoom mode (if enabled)
def LeaveZoomMode():
    global ZoomMode
    if not ZoomMode: return
    ZoomAnimation(ZoomX0, ZoomY0, lambda t: 1.0 - t)
    ZoomMode = False
    Panning = False

# increment/decrement spot radius
def IncrementSpotSize(delta):
    global SpotRadius
    if not Tracing:
        return
    SpotRadius = max(SpotRadius + delta, 8)
    GenerateSpotMesh()
    DrawCurrentPage()

# post-initialize the page transitions
def PrepareTransitions():
    Unspecified = 0xAFFED00F
    # STEP 1: randomly assign transitions where the user didn't specify them
    cnt = sum([1 for page in xrange(1, PageCount + 1) \
               if GetPageProp(page, 'transition', Unspecified) == Unspecified])
    newtrans = ((cnt / len(AvailableTransitions) + 1) * AvailableTransitions)[:cnt]
    random.shuffle(newtrans)
    for page in xrange(1, PageCount + 1):
        if GetPageProp(page, 'transition', Unspecified) == Unspecified:
            SetPageProp(page, 'transition', newtrans.pop())
            SetPageProp(page, 'IsRandomTransition', True)
    # STEP 2: instantiate transitions
    for page in PageProps:
        trans = PageProps[page]['transition']
        if trans is not None:
            PageProps[page]['transition'] = trans()

# update timer values and screen timer
def TimerTick():
    global CurrentTime, ProgressBarPos
    redraw = False
    newtime = (pygame.time.get_ticks() - StartTime) * 0.001
    if EstimatedDuration:
        newpos = int(ScreenWidth * newtime / EstimatedDuration)
        if newpos != ProgressBarPos:
            redraw = True
        ProgressBarPos = newpos
    newtime = int(newtime)
    if TimeDisplay and (CurrentTime != newtime):
        redraw = True
    CurrentTime = newtime
    return redraw

# set cursor visibility
def SetCursor(visible):
    global CursorVisible
    CursorVisible = visible
    if not CursorImage:
        pygame.mouse.set_visible(visible)

# shortcut handling
def IsValidShortcutKey(key):
    return ((key >= K_a)  and (key <= K_z)) \
        or ((key >= K_0)  and (key <= K_9)) \
        or ((key >= K_F1) and (key <= K_F12))
def FindShortcut(shortcut):
    for page, props in PageProps.iteritems():
        try:
            check = props['shortcut']
            if type(check) != types.StringType:
                check = int(check)
            elif (len(check) > 1) and (check[0] in "Ff"):
                check = K_F1 - 1 + int(check[1:])
            else:
                check = ord(check.lower())
        except (KeyError, TypeError, ValueError):
            continue
        if check == shortcut:
            return page
    return None
def AssignShortcut(page, key):
    old_page = FindShortcut(key)
    if old_page:
        del PageProps[old_page]['shortcut']
    if key < 127:
        shortcut = chr(key)
    elif (key >= K_F1) and (key <= K_F15):
        shortcut = "F%d" % (key - K_F1 + 1)
    else:
        shortcut = int(key)
    SetPageProp(page, 'shortcut', shortcut)



##### OVERVIEW MODE ############################################################

def UpdateOverviewTexture():
    global OverviewNeedUpdate
    glBindTexture(TextureTarget, Tnext)
    Loverview.acquire()
    try:
        glTexImage2D(TextureTarget, 0, 3, TexWidth, TexHeight, 0, \
                     GL_RGB, GL_UNSIGNED_BYTE, OverviewImage.tostring())
    finally:
        Loverview.release()
    OverviewNeedUpdate = False

# draw the overview page
def DrawOverview():
    glDisable(GL_BLEND)
    glEnable(TextureTarget)
    glBindTexture(TextureTarget, Tnext)
    glColor3ub(192, 192, 192)
    DrawFullQuad()

    pos = OverviewPos(OverviewSelection)
    X0 = PixelX *  pos[0]
    Y0 = PixelY *  pos[1]
    X1 = PixelX * (pos[0] + OverviewCellX)
    Y1 = PixelY * (pos[1] + OverviewCellY)
    glColor3d(1.0, 1.0, 1.0)
    glBegin(GL_QUADS)
    DrawPoint(X0, Y0)
    DrawPoint(X1, Y0)
    DrawPoint(X1, Y1)
    DrawPoint(X0, Y1)
    glEnd()

    DrawOSDEx(OSDTitlePos,  CurrentOSDCaption)
    DrawOSDEx(OSDPagePos,   CurrentOSDPage)
    DrawOSDEx(OSDStatusPos, CurrentOSDStatus)
    DrawOverlays()
    pygame.display.flip()

# overview zoom effect, time mapped through func
def OverviewZoom(func):
    pos = OverviewPos(OverviewSelection)
    X0 = PixelX * (pos[0] + OverviewBorder)
    Y0 = PixelY * (pos[1] + OverviewBorder)
    X1 = PixelX * (pos[0] - OverviewBorder + OverviewCellX)
    Y1 = PixelY * (pos[1] - OverviewBorder + OverviewCellY)

    t0 = pygame.time.get_ticks()
    while True:
        t = (pygame.time.get_ticks() - t0) * 1.0 / ZoomDuration
        if t >= 1.0: break
        t = func(t)
        t1 = t*t
        t = 1.0 - t1

        zoom = (t * (X1 - X0) + t1) / (X1 - X0)
        OX = zoom * (t * X0 - X0) - (zoom - 1.0) * t * X0
        OY = zoom * (t * Y0 - Y0) - (zoom - 1.0) * t * Y0
        OX = t * X0 - zoom * X0
        OY = t * Y0 - zoom * Y0

        glDisable(GL_BLEND)
        glEnable(TextureTarget)
        glBindTexture(TextureTarget, Tnext)
        glBegin(GL_QUADS)
        glColor3ub(192, 192, 192)
        glTexCoord2d(    0.0,     0.0);  glVertex2d(OX,        OY)
        glTexCoord2d(TexMaxS,     0.0);  glVertex2d(OX + zoom, OY)
        glTexCoord2d(TexMaxS, TexMaxT);  glVertex2d(OX + zoom, OY + zoom)
        glTexCoord2d(    0.0, TexMaxT);  glVertex2d(OX,        OY + zoom)
        glColor3ub(255, 255, 255)
        glTexCoord2d(X0 * TexMaxS, Y0 * TexMaxT);  glVertex2d(OX + X0*zoom, OY + Y0 * zoom)
        glTexCoord2d(X1 * TexMaxS, Y0 * TexMaxT);  glVertex2d(OX + X1*zoom, OY + Y0 * zoom)
        glTexCoord2d(X1 * TexMaxS, Y1 * TexMaxT);  glVertex2d(OX + X1*zoom, OY + Y1 * zoom)
        glTexCoord2d(X0 * TexMaxS, Y1 * TexMaxT);  glVertex2d(OX + X0*zoom, OY + Y1 * zoom)
        glEnd()

        EnableAlphaBlend()
        glBindTexture(TextureTarget, Tcurrent)
        glColor4d(1.0, 1.0, 1.0, 1.0 - t * t * t)
        glBegin(GL_QUADS)
        glTexCoord2d(    0.0,     0.0);  glVertex2d(t * X0,      t * Y0)
        glTexCoord2d(TexMaxS,     0.0);  glVertex2d(t * X1 + t1, t * Y0)
        glTexCoord2d(TexMaxS, TexMaxT);  glVertex2d(t * X1 + t1, t * Y1 + t1)
        glTexCoord2d(    0.0, TexMaxT);  glVertex2d(t * X0,      t * Y1 + t1)
        glEnd()

        DrawOSDEx(OSDTitlePos,  CurrentOSDCaption, alpha_factor=t)
        DrawOSDEx(OSDPagePos,   CurrentOSDPage,    alpha_factor=t)
        DrawOSDEx(OSDStatusPos, CurrentOSDStatus,  alpha_factor=t)
        DrawOverlays()
        pygame.display.flip()

# overview keyboard navigation
def OverviewKeyboardNav(delta):
    global OverviewSelection
    dest = OverviewSelection + delta
    if (dest >= OverviewPageCount) or (dest < 0):
        return
    OverviewSelection = dest
    x, y = OverviewPos(OverviewSelection)
    pygame.mouse.set_pos((x + (OverviewCellX / 2), y + (OverviewCellY / 2)))

# overview mode PageProp toggle
def OverviewTogglePageProp(prop, default):
    if (OverviewSelection < 0) or (OverviewSelection >= len(OverviewPageMap)):
        return
    page = OverviewPageMap[OverviewSelection]
    SetPageProp(page, prop, not(GetPageProp(page, prop, default)))
    UpdateCaption(page, force=True)
    DrawOverview()

# overview event handler
def HandleOverviewEvent(event):
    global OverviewSelection, TimeDisplay

    if event.type == QUIT:
        PageLeft(overview=True)
        Quit()
    elif event.type == VIDEOEXPOSE:
        DrawOverview()

    elif event.type == KEYDOWN:
        if (event.key == K_ESCAPE) or (event.unicode == u'q'):
            pygame.event.post(pygame.event.Event(QUIT))
        elif event.unicode == u'f':
            SetFullscreen(not Fullscreen)
        elif event.unicode == u't':
            TimeDisplay = not(TimeDisplay)
            DrawOverview()
        elif event.unicode == u'r':
            ResetTimer()
            if TimeDisplay: DrawOverview()
        elif event.unicode == u's':
            SaveInfoScript(InfoScriptPath)
        elif event.unicode == u'o':
            OverviewTogglePageProp('overview', True)
        elif event.unicode == u'i':
            OverviewTogglePageProp('skip', False)
        elif event.key == K_UP:    OverviewKeyboardNav(-OverviewGridSize)
        elif event.key == K_LEFT:  OverviewKeyboardNav(-1)
        elif event.key == K_RIGHT: OverviewKeyboardNav(+1)
        elif event.key == K_DOWN:  OverviewKeyboardNav(+OverviewGridSize)
        elif event.key == K_TAB:
            OverviewSelection = -1
            return 0
        elif event.key in (K_RETURN, K_KP_ENTER):
            return 0
        elif IsValidShortcutKey(event.key):
            if event.mod & KMOD_SHIFT:
                try:
                    AssignShortcut(OverviewPageMap[OverviewSelection], event.key)
                except IndexError:
                    pass   # no valid page selected
            else:
                # load shortcut
                page = FindShortcut(event.key)
                if page:
                    OverviewSelection = OverviewPageMapInv[page]
                    x, y = OverviewPos(OverviewSelection)
                    pygame.mouse.set_pos((x + (OverviewCellX / 2), \
                                          y + (OverviewCellY / 2)))
                    DrawOverview()

    elif event.type == MOUSEBUTTONUP:
        if event.button == 1:
            return 0
        elif event.button in (2, 3):
            OverviewSelection = -1
            return 0

    elif event.type == MOUSEMOTION:
        pygame.event.clear(MOUSEMOTION)
        # mouse move in fullscreen mode -> show mouse cursor and reset mouse timer
        if Fullscreen:
            pygame.time.set_timer(USEREVENT_HIDE_MOUSE, MouseHideDelay)
            SetCursor(True)
        # determine highlighted page
        OverviewSelection = \
             int((event.pos[0] - OverviewOfsX) / OverviewCellX) + \
             int((event.pos[1] - OverviewOfsY) / OverviewCellY) * OverviewGridSize
        if (OverviewSelection < 0) or (OverviewSelection >= len(OverviewPageMap)):
            UpdateCaption(0)
        else:
            UpdateCaption(OverviewPageMap[OverviewSelection])
        DrawOverview()

    elif event.type == USEREVENT_HIDE_MOUSE:
        # mouse timer event -> hide fullscreen cursor
        pygame.time.set_timer(USEREVENT_HIDE_MOUSE, 0)
        SetCursor(False)
        DrawOverview()

    return 1

# overview mode entry/loop/exit function
def DoOverview():
    global Pcurrent, Pnext, Tcurrent, Tnext, Tracing, OverviewSelection
    global PageEnterTime, OverviewMode

    pygame.time.set_timer(USEREVENT_PAGE_TIMEOUT, 0)
    PageLeft()
    UpdateOverviewTexture()

    if GetPageProp(Pcurrent, 'boxes') or Tracing:
        BoxFade(lambda t: 1.0 - t)
    Tracing = False
    OverviewSelection = OverviewPageMapInv[Pcurrent]

    OverviewMode = True
    OverviewZoom(lambda t: 1.0 - t)
    DrawOverview()
    PageEnterTime = pygame.time.get_ticks() - StartTime
    while True:
        event = pygame.event.poll()
        if event.type == NOEVENT:
            force_update = OverviewNeedUpdate
            if OverviewNeedUpdate:
                UpdateOverviewTexture()
            if TimerTick() or force_update:
                DrawOverview()
            pygame.time.wait(20)
        elif not HandleOverviewEvent(event):
            break
    PageLeft(overview=True)

    if (OverviewSelection < 0) or (OverviewSelection >= OverviewPageCount):
        OverviewSelection = OverviewPageMapInv[Pcurrent]
        Pnext = Pcurrent
    else:
        Pnext = OverviewPageMap[OverviewSelection]
    if Pnext != Pcurrent:
        Pcurrent = Pnext
        RenderPage(Pcurrent, Tcurrent)
    UpdateCaption(Pcurrent)
    OverviewZoom(lambda t: t)
    DrawCurrentPage()
    OverviewMode = False

    if GetPageProp(Pcurrent, 'boxes'):
        BoxFade(lambda t: t)
    PageEntered()
    if not PreloadNextPage(GetNextPage(Pcurrent, 1)):
        PreloadNextPage(GetNextPage(Pcurrent, -1))


##### EVENT HANDLING ###########################################################

# set fullscreen mode
def SetFullscreen(fs, do_init=True):
    global Fullscreen

    # let pygame do the real work
    if do_init:
        if fs == Fullscreen: return
        if not pygame.display.toggle_fullscreen(): return
    Fullscreen=fs

    # redraw the current page (pygame is too lazy to send an expose event ...)
    DrawCurrentPage()

    # show cursor and set auto-hide timer
    if fs:
        pygame.time.set_timer(USEREVENT_HIDE_MOUSE, MouseHideDelay)
    else:
        pygame.time.set_timer(USEREVENT_HIDE_MOUSE, 0)
        SetCursor(True)

# PageProp toggle
def TogglePageProp(prop, default):
    global WantStatus
    SetPageProp(Pcurrent, prop, not(GetPageProp(Pcurrent, prop, default)))
    UpdateCaption(Pcurrent, force=True)
    WantStatus = True
    DrawCurrentPage()

# main event handling function
def HandleEvent(event):
    global HaveMark, ZoomMode, Marking, Tracing, Panning, SpotRadius, FileStats
    global MarkUL, MarkLR, MouseDownX, MouseDownY, PanAnchorX, PanAnchorY
    global ZoomX0, ZoomY0, PageCache, CacheFilePos, RTrunning, RTrestart
    global CurrentTime, TimeDisplay, TimeTracking, ProgressBarPos
    global StartTime, PageEnterTime

    if event.type == QUIT:
        PageLeft()
        Quit()
    elif event.type == VIDEOEXPOSE:
        DrawCurrentPage()

    elif event.type == KEYDOWN:
        if (event.key == K_ESCAPE) or (event.unicode == u'q'):
            pygame.event.post(pygame.event.Event(QUIT))
        elif event.unicode == u'f':
            SetFullscreen(not Fullscreen)
        elif event.unicode == u's':
            print "save"
            SaveInfoScript(InfoScriptPath)
        elif event.unicode == u'z':  # handle QWERTY and QWERTZ keyboards
            if ZoomMode:
                LeaveZoomMode()
            else:
                tx, ty = MouseToScreen(pygame.mouse.get_pos())
                EnterZoomMode(0.5 * tx, 0.5 * ty)
        elif event.unicode == u'b':
            FadeMode(0.0)
        elif event.unicode == u'w':
            FadeMode(1.0)
        elif event.unicode == u't':
            TimeDisplay = not(TimeDisplay)
            DrawCurrentPage()
            if TimeDisplay and not(TimeTracking) and FirstPage:
                print >>sys.stderr, "Time tracking mode enabled."
                TimeTracking = True
                print "page duration    enter    leave"
                print "---- -------- -------- --------"
        elif event.unicode == u'r':
            ResetTimer()
            if TimeDisplay: DrawCurrentPage()
        elif event.unicode == u'l':
            TransitionTo(LastPage)
        elif event.unicode == u'o':
            TogglePageProp('overview', True)
        elif event.unicode == u'i':
            TogglePageProp('skip', False)
        elif event.key == K_TAB:
            LeaveZoomMode()
            DoOverview()
        elif event.key in (32, K_DOWN, K_RIGHT, K_PAGEDOWN):
            LeaveZoomMode()
            TransitionTo(GetNextPage(Pcurrent, 1))
        elif event.key in (K_BACKSPACE, K_UP, K_LEFT, K_PAGEUP):
            LeaveZoomMode()
            TransitionTo(GetNextPage(Pcurrent, -1))
        elif event.key == K_HOME:
            if Pcurrent != 1:
                TransitionTo(1)
        elif event.key == K_END:
            if Pcurrent != PageCount:
                TransitionTo(PageCount)
        elif event.key in (K_RETURN, K_KP_ENTER):
            if not(GetPageProp(Pcurrent, 'boxes')) and Tracing:
                BoxFade(lambda t: 1.0 - t)
            Tracing = not(Tracing)
            if not(GetPageProp(Pcurrent, 'boxes')) and Tracing:
                BoxFade(lambda t: t)
        elif event.unicode == u'+':
            IncrementSpotSize(+8)
        elif event.unicode == u'-':
            IncrementSpotSize(-8)
        elif event.unicode == u'[':
            SetGamma(new_gamma=Gamma / GammaStep)
        elif event.unicode == u']':
            SetGamma(new_gamma=Gamma * GammaStep)
        elif event.unicode == u'{':
            SetGamma(new_black=BlackLevel - BlackLevelStep)
        elif event.unicode == u'}':
            SetGamma(new_black=BlackLevel + BlackLevelStep)
        elif event.unicode == u'\\':
            SetGamma(1.0, 0)
        elif IsValidShortcutKey(event.key):
            if event.mod & KMOD_SHIFT:
                AssignShortcut(Pcurrent, event.key)
            else:
                # load keyboard shortcut
                page = FindShortcut(event.key)
                if page and (page != Pcurrent):
                    TransitionTo(page)

    elif event.type == MOUSEBUTTONDOWN:
        MouseDownX, MouseDownY = event.pos
        if event.button == 1:
            MarkUL = MarkLR = MouseToScreen(event.pos)
        elif (event.button == 3) and ZoomMode:
            PanAnchorX = ZoomX0
            PanAnchorY = ZoomY0
        elif event.button == 4:
            IncrementSpotSize(+8)
        elif event.button == 5:
            IncrementSpotSize(-8)

    elif event.type == MOUSEBUTTONUP:
        if event.button == 2:
            LeaveZoomMode()
            DoOverview()
            return
        if event.button == 1:
            if Marking:
                # left mouse button released in marking mode -> stop box marking
                Marking = False
                # reject too small boxes
                if  (abs(MarkUL[0] - MarkLR[0]) > 0.04) \
                and (abs(MarkUL[1] - MarkLR[1]) > 0.03):
                    boxes = GetPageProp(Pcurrent, 'boxes', [])
                    oldboxcount = len(boxes)
                    boxes.append(NormalizeRect(MarkUL[0], MarkUL[1], MarkLR[0], MarkLR[1]))
                    SetPageProp(Pcurrent, 'boxes', boxes)
                    if not(oldboxcount) and not(Tracing):
                        BoxFade(lambda t: t)
                DrawCurrentPage()
            else:
                # left mouse button released, but no marking -> proceed to next page
                LeaveZoomMode()
                TransitionTo(GetNextPage(Pcurrent, 1))
        if (event.button == 3) and not(Panning):
            # right mouse button -> check if a box has to be killed
            boxes = GetPageProp(Pcurrent, 'boxes', [])
            x, y = MouseToScreen(event.pos)
            try:
                # if a box is already present around the clicked position, kill it
                idx = FindBox(x, y, boxes)
                if (len(boxes) == 1) and not(Tracing):
                    BoxFade(lambda t: 1.0 - t)
                del boxes[idx]
                SetPageProp(Pcurrent, 'boxes', boxes)
                DrawCurrentPage()
            except ValueError:
                # no box present -> go to previous page
                LeaveZoomMode()
                TransitionTo(GetNextPage(Pcurrent, -1))
        Panning=False

    elif event.type == MOUSEMOTION:
        pygame.event.clear(MOUSEMOTION)
        # mouse move in fullscreen mode -> show mouse cursor and reset mouse timer
        if Fullscreen:
            pygame.time.set_timer(USEREVENT_HIDE_MOUSE, MouseHideDelay)
            SetCursor(True)
        # activate marking if mouse is moved away far enough
        if event.buttons[0] and not(Marking):
            x, y = event.pos
            if (abs(x - MouseDownX) > 4) and (abs(y - MouseDownY) > 4):
                Marking = True
        # mouse move while marking -> update marking box
        if Marking:
            MarkLR = MouseToScreen(event.pos)
        # mouse move while RMB is pressed -> panning
        if event.buttons[2] and ZoomMode:
            x, y = event.pos
            if not(Panning) and (abs(x - MouseDownX) > 4) and (abs(y - MouseDownY) > 4):
                Panning = True
            ZoomX0 = PanAnchorX + (MouseDownX - x) * ZoomArea / ScreenWidth
            ZoomY0 = PanAnchorY + (MouseDownY - y) * ZoomArea / ScreenHeight
            ZoomX0 = min(max(ZoomX0, 0.0), 1.0 - ZoomArea)
            ZoomY0 = min(max(ZoomY0, 0.0), 1.0 - ZoomArea)
        # if anything changed, redraw the page
        if Marking or Tracing or event.buttons[2] or (CursorImage and CursorVisible):
            DrawCurrentPage()

    elif event.type == USEREVENT_HIDE_MOUSE:
        # mouse timer event -> hide fullscreen cursor
        pygame.time.set_timer(USEREVENT_HIDE_MOUSE, 0)
        SetCursor(False)
        DrawCurrentPage()

    elif event.type == USEREVENT_PAGE_TIMEOUT:
        TransitionTo(GetNextPage(Pcurrent, 1))

    elif event.type == USEREVENT_POLL_FILE:
        NewStats = GetFileStats()
        if NewStats != FileStats:
            # first, check if the new file is valid
            if PDFinput:
                if not os.path.isfile(FileName): return
            else:
                if not os.path.isfile(os.path.join(FileName, FileList[Pcurrent - 1])):
                    return
            # invalidate the cache. completely.
            Lcache.acquire()
            try:
                PageCache = {}
                CacheFilePos = 0
            finally:
                Lcache.release()
            FileStats = NewStats
            # invalidate the overview page, too
            for props in PageProps.itervalues():
                if 'OverviewRendered' in props:
                    del props['OverviewRendered']
            # reload the info script
            LoadInfoScript()
            # force a transition to the current page, reloading it
            Pnext=-1
            TransitionTo(Pcurrent)
            # restart the background renderer thread. this is not completely safe,
            # i.e. there's a small chance that we fail to restart the thread, but
            # this isn't critical
            if UseCache and BackgroundRendering:
                if RTrunning:
                    RTrestart = True
                else:
                    RTrunning = True
                    thread.start_new_thread(RenderThread, (Pcurrent, Pnext))

    elif event.type == USEREVENT_TIMER_UPDATE:
        if TimerTick():
            DrawCurrentPage()


##### RENDER MODE ##############################################################

def DoRender():
    global TexWidth, TexHeight
    TexWidth = ScreenWidth
    TexHeight = ScreenHeight
    if os.path.exists(RenderToDirectory):
        print >>sys.stderr, "Destination directory `%s' already exists," % RenderToDirectory
        print >>sys.stderr, "refusing to overwrite anything."
        return 1
    try:
        os.mkdir(RenderToDirectory)
    except OSError, e:
        print >>sys.stderr, "Cannot create destination directory `%s':" % RenderToDirectory
        print >>sys.stderr, e.strerror
        return 1
    print >>sys.stderr, "Rendering presentation into `%s'" % RenderToDirectory
    for page in xrange(1, PageCount + 1):
        PageImage(page, RenderMode=True).save("%s/page%04d.png" % (RenderToDirectory, page))
        sys.stdout.write("[%d] " % page)
        sys.stdout.flush()
    print >>sys.stderr
    print >>sys.stderr, "Done."
    return 0


##### INITIALIZATION ###########################################################

def main():
    global ScreenWidth, ScreenHeight, TexWidth, TexHeight, TexSize, LogoImage
    global TexMaxS, TexMaxT, MeshStepX, MeshStepY, EdgeX, EdgeY, PixelX, PixelY
    global OverviewGridSize, OverviewCellX, OverviewCellY
    global OverviewOfsX, OverviewOfsY, OverviewImage, OverviewPageCount
    global OverviewPageMap, OverviewPageMapInv, PDFinput, FileList, PageCount
    global Resolution, DocumentTitle, PageProps, LogoTexture, OSDFont
    global Pcurrent, Pnext, Tcurrent, Tnext, InitialPage, CacheFile
    global Extensions, AllowExtensions, TextureTarget, PAR, DAR, TempFileName
    global BackgroundRendering, FileStats, RTrunning, RTrestart, StartTime
    global CursorImage, CursorVisible, InfoScriptPath

    DocumentTitle = os.path.splitext(os.path.split(FileName)[1])[0]
    PageCount = 0

    # allocate temporary TIFF file
    TempFileName=tempfile.mktemp(prefix="keyjnote-",suffix="_tmp")

    # try to get a list of image files
    PDFinput = False
    if FileName:
        try:
            FileList = filter(IsImageFileName, os.listdir(FileName))
            FileList.sort(lambda a, b: cmp(a.lower(), b.lower()))
        except OSError:  # this occurs if the parameter is not a directory
            PDFinput = True

    # set up the input files
    if not PDFinput:
        # image input: count images and assign file names as default titles
        PageCount = len(FileList)
        for page, file in zip(range(1, PageCount + 1), FileList):
            SetPageProp(page, 'private_title', os.path.split(file)[-1])
    else:
        # PDF input -> try to pre-parse the PDF file
        # phase 1: internal PDF parser
        try:
            PageCount, pdf_width, pdf_height = analyze_pdf(FileName)
            if Rotation & 1:
                pdf_width, pdf_height = (pdf_height, pdf_width)
            Resolution = min(ScreenWidth  * 72.0 / pdf_width, \
                             ScreenHeight * 72.0 / pdf_height)
        except:
            Resolution = 72.0

        # phase 2: use pdftk
        try:
            assert 0 == spawn(os.P_WAIT, pdftkPath, \
                ["pdftk", FileNameEscape + FileName + FileNameEscape, \
                 "dump_data", "output", TempFileName + ".txt"])
            pdftkParse(TempFileName + ".txt")
        except:
            pass

    # no pages? strange ...
    if not PageCount:
        print >>sys.stderr, "Cannot analyze `%s', quitting." % FileName
        sys.exit(1)

    # if rendering is wanted, do it NOW
    if RenderToDirectory:
        sys.exit(DoRender())

    # load and execute info script
    if not InfoScriptPath:
        InfoScriptPath = FileName + ".info"
    LoadInfoScript()

    # initialize graphics
    pygame.init()
    if Fullscreen and UseAutoScreenSize:
        size = GetScreenSize()
        if size:
            ScreenWidth, ScreenHeight = size
            print >>sys.stderr, "Detected screen size: %dx%d pixels" % (ScreenWidth, ScreenHeight)
    flags = OPENGL|DOUBLEBUF
    if Fullscreen:
        flags |= FULLSCREEN
    try:
        pygame.display.set_mode((ScreenWidth, ScreenHeight), flags)
    except:
        print >>sys.stderr, "FATAL: cannot create rendering surface in the desired resolution (%dx%d)" % (ScreenWidth, ScreenHeight)
        sys.exit(1)
    pygame.display.set_caption(__title__)
    pygame.key.set_repeat(500, 30)
    if Fullscreen:
        pygame.mouse.set_visible(False)
        CursorVisible = False
    glOrtho(0.0, 1.0,  1.0, 0.0,  -10.0, 10.0)
    if (Gamma <> 1.0) or (BlackLevel <> 0):
        SetGamma(force=True)

    # setup the OpenGL texture mode
    Extensions = dict([(ext.split('_', 2)[-1], None) for ext in \
                 glGetString(GL_EXTENSIONS).split()])
    if AllowExtensions and ("texture_non_power_of_twoXX" in Extensions):
        print >>sys.stderr, "Using GL_ARB_texture_non_power_of_two."
        TextureTarget = GL_TEXTURE_2D
        TexWidth  = ScreenWidth
        TexHeight = ScreenHeight
        TexMaxS = 1.0
        TexMaxT = 1.0
    elif AllowExtensions and ("texture_rectangle" in Extensions):
        print >>sys.stderr, "Using GL_ARB_texture_rectangle."
        TextureTarget = 0x84F5  # GL_TEXTURE_RECTANGLE_ARB
        TexWidth  = ScreenWidth
        TexHeight = ScreenHeight
        TexMaxS = ScreenWidth
        TexMaxT = ScreenHeight
    else:
        print >>sys.stderr, "Using conventional power-of-two textures with padding."
        TextureTarget = GL_TEXTURE_2D
        TexWidth  = npot(ScreenWidth)
        TexHeight = npot(ScreenHeight)
        TexMaxS = ScreenWidth  * 1.0 / TexWidth
        TexMaxT = ScreenHeight * 1.0 / TexHeight
    TexSize = TexWidth * TexHeight * 3

    # set up some variables
    if DAR is not None:
        PAR = DAR / float(ScreenWidth) * float(ScreenHeight)
    MeshStepX = 1.0 / MeshResX
    MeshStepY = 1.0 / MeshResY
    PixelX = 1.0 / ScreenWidth
    PixelY = 1.0 / ScreenHeight
    EdgeX = BoxEdgeSize * 1.0 / ScreenWidth
    EdgeY = BoxEdgeSize * 1.0 / ScreenHeight
    if InitialPage is None:
        InitialPage = GetNextPage(0, 1)
    Pcurrent = InitialPage
    if UseCache and CacheFile:
        CacheFile = tempfile.TemporaryFile(prefix="keyjnote-", suffix=".cache")

    # prepare logo image
    LogoImage = Image.open(StringIO.StringIO(LOGO))
    LogoTexture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, LogoTexture)
    glTexImage2D(GL_TEXTURE_2D, 0, 1, 256, 64, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, LogoImage.tostring())
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    DrawLogo()
    pygame.display.flip()

    # initialize OSD font
    try:
        OSDFont = GLFont(FontTextureWidth, FontTextureHeight, FontList, FontSize, search_path=FontPath)
        DrawLogo()
        titles = []
        for key in ('title', 'private_title'):
            titles.extend([p[key] for p in PageProps.itervalues() if key in p])
        if titles:
            OSDFont.AddString("".join(titles))
    except ValueError:
        print >>sys.stderr, "The OSD font size is too large, the OSD will be rendered incompletely."
    except IOError:
        print >>sys.stderr, "Could not open OSD font file, disabling OSD."
    except (NameError, AttributeError, TypeError):
        print >>sys.stderr, "Your version of PIL is too old or incomplete, disabling OSD."

    # initialize mouse cursor
    if CursorImage:
        try:
            CursorImage = PrepareCustomCursor(Image.open(CursorImage))
        except:
            print >>sys.stderr, "Could not open the mouse cursor image, using standard cursor."
            CursorImage = False

    # initialize overview metadata
    OverviewPageMap=[i for i in xrange(1, PageCount + 1) \
        if GetPageProp(i, 'overview', True) and (i >= PageRangeStart) and (i <= PageRangeEnd)]
    OverviewPageCount = max(len(OverviewPageMap), 1)
    OverviewPageMapInv = {}
    for page in xrange(1, PageCount + 1):
        OverviewPageMapInv[page] = len(OverviewPageMap) - 1
        for i in xrange(len(OverviewPageMap)):
            if OverviewPageMap[i] >= page:
                OverviewPageMapInv[page] = i
                break

    # initialize overview page geometry
    OverviewGridSize = 1
    while OverviewPageCount > OverviewGridSize * OverviewGridSize:
        OverviewGridSize += 1
    OverviewCellX = int(ScreenWidth  / OverviewGridSize)
    OverviewCellY = int(ScreenHeight / OverviewGridSize)
    OverviewOfsX = int((ScreenWidth  - OverviewCellX * OverviewGridSize)/2)
    OverviewOfsY = int((ScreenHeight - OverviewCellY * \
                   int((OverviewPageCount + OverviewGridSize - 1) / OverviewGridSize)) / 2)
    OverviewImage = Image.new('RGB', (TexWidth, TexHeight))

    # fill overlay "dummy" images
    dummy = LogoImage.copy()
    maxsize = (OverviewCellX - 2 * OverviewBorder, OverviewCellY - 2 * OverviewBorder)
    if (dummy.size[0] > maxsize[0]) or (dummy.size[1] > maxsize[1]):
        dummy.thumbnail(ZoomToFit(dummy.size, maxsize), Image.ANTIALIAS)
    margX = int((OverviewCellX - dummy.size[0]) / 2)
    margY = int((OverviewCellY - dummy.size[1]) / 2)
    dummy = dummy.convert(mode='RGB')
    for page in range(OverviewPageCount):
        pos = OverviewPos(page)
        OverviewImage.paste(dummy, (pos[0] + margX, pos[1] + margY))
    del dummy

    # set up background rendering
    if not EnableBackgroundRendering:
        print >>sys.stderr, "Background rendering isn't available on this platform."
        BackgroundRendering = False

    # if caching is enabled, pre-render all pages
    if UseCache and not(BackgroundRendering):
        DrawLogo()
        DrawProgress(0.0)
        pygame.display.flip()
        stop = False
        progress = 0.0
        for page in range(InitialPage, PageCount + 1) + range(1, InitialPage):
            event = pygame.event.poll()
            while event.type != NOEVENT:
                if event.type == KEYDOWN:
                    if (event.key == K_ESCAPE) or (event.unicode == u'q'):
                        Quit()
                    stop = True
                elif event.type == MOUSEBUTTONUP:
                    stop = True
                event = pygame.event.poll()
            if stop: break
            if (page >= PageRangeStart) and (page <= PageRangeEnd):
              PageImage(page)
            DrawLogo()
            progress += 1.0 / PageCount;
            DrawProgress(progress)
            pygame.display.flip()

    # create buffer textures
    DrawLogo()
    pygame.display.flip()
    glEnable(TextureTarget)
    Tcurrent, Tnext = glGenTextures(2)
    for T in (Tcurrent, Tnext):
        glBindTexture(TextureTarget, T)
        glTexParameteri(TextureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexParameteri(TextureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
        glTexParameteri(TextureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP)
        glTexParameteri(TextureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP)

    # prebuffer current and next page
    Pnext = 0
    RenderPage(Pcurrent, Tcurrent)
    PageEntered(update_time=False)
    PreloadNextPage(GetNextPage(Pcurrent, 1))

    # some other preparations
    PrepareTransitions()
    GenerateSpotMesh()

    # if update polling is enabled, get initial file stats now
    if PollInterval:
        FileStats = GetFileStats()
        pygame.time.set_timer(USEREVENT_POLL_FILE, PollInterval * 1000)

    # start the background rendering thread
    if UseCache and BackgroundRendering:
        RTrunning = True
        thread.start_new_thread(RenderThread, (Pcurrent, Pnext))

    # start output and enter main loop
    StartTime = pygame.time.get_ticks()
    pygame.time.set_timer(USEREVENT_TIMER_UPDATE, 100)
    if not(Fullscreen) and CursorImage:
        pygame.mouse.set_visible(False)
    DrawCurrentPage()
    UpdateCaption(Pcurrent)
    while True:
        HandleEvent(pygame.event.wait())


##### COMMAND-LINE PARSER AND HELP #############################################

def if_op(cond, res_then, res_else):
    if cond: return res_then
    else:    return res_else

def HelpExit(code=0):
    print """A nice presentation tool.

Usage: """+os.path.basename(sys.argv[0])+""" [OPTION...] <INPUT(S)...>

You may either play a PDF file, a directory containing image files or
individual image files.

Input options:
  -r,  --rotate <n>       rotate pages clockwise in 90-degree steps
       --scale            scale images to fit screen (not used in PDF mode)
       --supersample      use supersampling (only used in PDF mode)
  -s                      --supersample in PDF mode, --scale else
  -I,  --script           set the path of the info script
  -u,  --poll <seconds>   check periodically if the source file has been
                          updated and reload it if it did
  -o,  --output <dir>     don't display the presentation, only render to .png
  -h,  --help             show this help text and exit

Output options:
  -f,  --fullscreen       """+if_op(Fullscreen,"do NOT ","")+"""start in fullscreen mode
  -g,  --geometry <WxH>   set window size or fullscreen resolution
  -A,  --aspect <X:Y>     adjust for a specific display aspect ratio (e.g. 5:4)
  -G,  --gamma <G[:BL]>   specify startup gamma and black level

Page options:
  -i,  --initialpage <n>  start with page <n>
  -p,  --pages <A-B>      only cache pages in the specified range;
                          implicitly sets -i <A>
  -w,  --wrap             go back to the first page after the last page
  -a,  --auto <seconds>   automatically advance to next page after some seconds

Display options:
  -t,  --transition <trans[,trans2...]>
                          force a specific transitions or set of transitions
  -l,  --listtrans        print a list of available transitions and exit
  -F,  --font <file>      use a specific TrueType font file for the OSD
  -S,  --fontsize <px>    specify the OSD font size in pixels
  -C,  --cursor <F[:X,Y]> use a .png image as the mouse cursor
  -L,  --layout <spec>    set the OSD layout (please read the documentation)

Timing options:
  -M,  --minutes          display time in minutes, not seconds
  -d,  --duration <time>  set the desired duration of the presentation and show
                          a progress bar at the bottom of the screen
  -T,  --transtime <ms>   set transition duration in milliseconds
  -D,  --mousedelay <ms>  set mouse hide delay for fullscreen mode (in ms)
  -B,  --boxfade <ms>     set highlight box fade duration in milliseconds
  -Z,  --zoom <ms>        set zoom duration in milliseconds

Advanced options:
  -c,  --nocache          disable page image cache (saves a lot of RAM or disk)
  -m,  --memcache         store page image cache in RAM instead of a temp file
  -b,  --noback           don't pre-render images in the background
  -P,  --gspath <path>    set path to GhostScript or pdftoppm executable
  -R,  --meshres <XxY>    set mesh resolution for effects (default: 48x36)
  -e,  --noext            don't use OpenGL texture size extensions

For detailed information, visit""", __website__
    sys.exit(code)

def ListTransitions():
    print "Available transitions:"
    trans = [(tc.__name__, tc.__doc__) for tc in AllTransitions]
    trans.append(('None', "no transition"))
    trans.sort()
    maxlen = max([len(item[0]) for item in trans])
    for item in trans:
        print "*", item[0].ljust(maxlen), "-", item[1]
    sys.exit(0)

def TryTime(s, regexp, func):
    m = re.match(regexp, s, re.I)
    if not m: return 0
    return func(map(int, m.groups()))
def ParseTime(s):
    return TryTime(s, r'([0-9]+)s?$', lambda m: m[0]) \
        or TryTime(s, r'([0-9]+)m$', lambda m: m[0] * 60) \
        or TryTime(s, r'([0-9]+)[m:]([0-9]+)[ms]?$', lambda m: m[0] * 60 + m[1]) \
        or TryTime(s, r'([0-9]+)[h:]([0-9]+)[hm]?$', lambda m: m[0] * 3600 + m[1] * 60) \
        or TryTime(s, r'([0-9]+)[h:]([0-9]+)[m:]([0-9]+)s?$', lambda m: m[0] * 3600 + m[1] * 60 + m[2])

def opterr(msg):
    print >>sys.stderr, "command line parse error:", msg
    print >>sys.stderr, "use `%s -h' to get help" % sys.argv[0]
    print >>sys.stderr, "or visit", __website__, "for full documentation"
    sys.exit(2)

def SetTransitions(list):
    global AvailableTransitions
    index = dict([(tc.__name__.lower(), tc) for tc in AllTransitions])
    index['none'] = None
    AvailableTransitions=[]
    for trans in list.split(','):
        try:
            AvailableTransitions.append(index[trans.lower()])
        except KeyError:
            opterr("unknown transition `%s'" % trans)

def ParseLayoutPosition(value):
    xpos = []
    ypos = []
    for c in value.strip().lower():
        if   c == 't': ypos.append(0)
        elif c == 'b': ypos.append(1)
        elif c == 'l': xpos.append(0)
        elif c == 'r': xpos.append(1)
        elif c == 'c': xpos.append(2)
        else: opterr("invalid position specification `%s'" % value)
    if not xpos: opterr("position `%s' lacks X component" % value)
    if not ypos: opterr("position `%s' lacks Y component" % value)
    if len(xpos)>1: opterr("position `%s' has multiple X components" % value)
    if len(ypos)>1: opterr("position `%s' has multiple Y components" % value)
    return (xpos[0] << 1) | ypos[0]
def SetLayoutSubSpec(key, value):
    global OSDTimePos, OSDTitlePos, OSDPagePos, OSDStatusPos
    global OSDAlpha, OSDMargin
    lkey = key.strip().lower()
    if lkey in ('a', 'alpha', 'opacity'):
        try:
            OSDAlpha = float(value)
        except ValueError:
            opterr("invalid alpha value `%s'" % value)
        if OSDAlpha > 1.0:
            OSDAlpha *= 0.01  # accept percentages, too
        if (OSDAlpha < 0.0) or (OSDAlpha > 1.0):
            opterr("alpha value %s out of range" % value)
    elif lkey in ('margin', 'dist', 'distance'):
        try:
            OSDMargin = float(value)
        except ValueError:
            opterr("invalid margin value `%s'" % value)
        if OSDMargin < 0:
            opterr("margin value %s out of range" % value)
    elif lkey in ('t', 'time'):
        OSDTimePos = ParseLayoutPosition(value)
    elif lkey in ('title', 'caption'):
        OSDTitlePos = ParseLayoutPosition(value)
    elif lkey in ('page', 'number'):
        OSDPagePos = ParseLayoutPosition(value)
    elif lkey in ('status', 'info'):
        OSDStatusPos = ParseLayoutPosition(value)
    else:
        opterr("unknown layout element `%s'" % key)
def SetLayout(spec):
    for sub in spec.replace(':', '=').split(','):
        try:
            key, value = sub.split('=')
        except ValueError:
            opterr("invalid layout spec `%s'" % sub)
        SetLayoutSubSpec(key, value)

def ParseOptions(argv):
    global FileName, FileList, Fullscreen, Scaling, Supersample, UseCache
    global TransitionDuration, MouseHideDelay, BoxFadeDuration, ZoomDuration
    global ScreenWidth, ScreenHeight, MeshResX, MeshResY, InitialPage, Wrap
    global AutoAdvance, RenderToDirectory, Rotation, AllowExtensions, DAR
    global BackgroundRendering, UseAutoScreenSize, CacheFile, PollInterval
    global PageRangeStart, PageRangeEnd, FontList, FontSize, Gamma, BlackLevel
    global EstimatedDuration, CursorImage, CursorHotspot, MinutesOnly
    global GhostScriptPath, pdftoppmPath, UseGhostScript, InfoScriptPath

    try:  # unused short options: jknqvxyzEHJKNOQUVWXY
        opts, args = getopt.getopt(argv, \
            "hfg:sci:wa:t:lo:r:T:D:B:Z:P:R:eA:mbp:u:F:S:G:d:C:ML:I:", \
           ["help", "fullscreen", "geometry=", "scale", "supersample", \
            "nocache", "initialpage=", "wrap", "auto", "listtrans", "output=", \
            "rotate=", "transition=", "transtime=", "mousedelay=", "boxfade=", \
            "zoom=", "gspath=", "meshres=", "noext", "aspect", "memcache", \
            "noback", "pages=", "poll=", "font=", "fontsize=", "gamma=",
            "duration=", "cursor=", "minutes", "layout=", "script="])
    except getopt.GetoptError, message:
        opterr(message)

    for opt, arg in opts:
        if opt in ("-h", "--help"):
            HelpExit()
        if opt in ("-l", "--listtrans"):
            ListTransitions()
        if opt in ("-f", "--fullscreen"):
            Fullscreen = not(Fullscreen)
        if opt in ("-e", "--noext"):
            AllowExtensions = not(AllowExtensions)
        if opt in ("-s", "--scale"):
            Scaling = not(Scaling)
        if opt in ("-s", "--supersample"):
            Supersample = 2
        if opt in ("-w", "--wrap"):
            Wrap = not(Wrap)
        if opt in ("-c", "--nocache"):
            UseCache = not(UseCache)
        if opt in ("-m", "--memcache"):
            CacheFile = not(CacheFile)
        if opt in ("-M", "--minutes"):
            MinutesOnly = not(MinutesOnly)
        if opt in ("-b", "--noback"):
            BackgroundRendering = not(BackgroundRendering)
        if opt in ("-t", "--transition"):
            SetTransitions(arg)
        if opt in ("-L", "--layout"):
            SetLayout(arg)
        if opt in ("-o", "--output"):
            RenderToDirectory = arg
        if opt in ("-I", "--script"):
            InfoScriptPath = arg
        if opt in ("-F", "--font"):
            FontList = [arg]
        if opt in ("-P", "--gspath"):
            UseGhostScript = (arg.replace("\\", "/").split("/")[-1].lower().find("pdftoppm") < 0)
            if UseGhostScript:
                GhostScriptPath = arg
            else:
                pdftoppmPath = arg
        if opt in ("-S", "--fontsize"):
            try:
                FontSize = int(arg)
                assert FontSize > 0
            except:
                opterr("invalid parameter for --fontsize")
        if opt in ("-i", "--initialpage"):
            try:
                InitialPage = int(arg)
                assert InitialPage > 0
            except:
                opterr("invalid parameter for --initialpage")
        if opt in ("-d", "--duration"):
            try:
                EstimatedDuration = ParseTime(arg)
                assert EstimatedDuration > 0
            except:
                opterr("invalid parameter for --duration")
        if opt in ("-a", "--auto"):
            try:
                AutoAdvance = int(arg) * 1000
                assert (AutoAdvance > 0) and (AutoAdvance <= 86400000)
            except:
                opterr("invalid parameter for --auto")
        if opt in ("-T", "--transtime"):
            try:
                TransitionDuration = int(arg)
                assert (TransitionDuration >= 0) and (TransitionDuration < 32768)
            except:
                opterr("invalid parameter for --transtime")
        if opt in ("-D", "--mousedelay"):
            try:
                MouseHideDelay = int(arg)
                assert (MouseHideDelay >= 0) and (MouseHideDelay < 32768)
            except:
                opterr("invalid parameter for --mousedelay")
        if opt in ("-B", "--boxfade"):
            try:
                BoxFadeDuration = int(arg)
                assert (BoxFadeDuration >= 0) and (BoxFadeDuration < 32768)
            except:
                opterr("invalid parameter for --boxfade")
        if opt in ("-Z", "--zoom"):
            try:
                ZoomDuration = int(arg)
                assert (ZoomDuration >= 0) and (ZoomDuration < 32768)
            except:
                opterr("invalid parameter for --zoom")
        if opt in ("-r", "--rotate"):
            try:
                Rotation = int(arg)
            except:
                opterr("invalid parameter for --rotate")
            while Rotation < 0: Rotation += 4
            Rotation = Rotation & 3
        if opt in ("-u", "--poll"):
            try:
                PollInterval = int(arg)
                assert PollInterval >= 0
            except:
                opterr("invalid parameter for --poll")
        if opt in ("-g", "--geometry"):
            try:
                ScreenWidth, ScreenHeight = map(int, arg.split("x"))
                assert (ScreenWidth  >= 320) and (ScreenWidth  < 4096)
                assert (ScreenHeight >= 200) and (ScreenHeight < 4096)
                UseAutoScreenSize = False
            except:
                opterr("invalid parameter for --geometry")
        if opt in ("-R", "--meshres"):
            try:
                MeshResX, MeshResY = map(int, arg.split("x"))
                assert (MeshResX > 0) and (MeshResX <= ScreenWidth)
                assert (MeshResY > 0) and (MeshResY <= ScreenHeight)
            except:
                opterr("invalid parameter for --meshres")
        if opt in ("-p", "--pages"):
            try:
                PageRangeStart, PageRangeEnd = map(int, arg.split("-"))
                assert PageRangeStart > 0
                assert PageRangeStart <= PageRangeEnd
            except:
                opterr("invalid parameter for --pages")
            InitialPage=PageRangeStart
        if opt in ("-A", "--aspect"):
            try:
                if ':' in arg:
                    fx, fy = map(float, arg.split(':'))
                    DAR = fx / fy
                else:
                    DAR = float(arg)
                assert DAR > 0.0
            except:
                opterr("invalid parameter for --aspect")
        if opt in ("-G", "--gamma"):
            try:
                if ':' in arg:
                    arg, bl = arg.split(':', 1)
                    BlackLevel = int(bl)
                Gamma = float(arg)
                assert Gamma > 0.0
                assert (BlackLevel >= 0) and (BlackLevel < 255)
            except:
                opterr("invalid parameter for --gamma")
        if opt in ("-C", "--cursor"):
            try:
                if ':' in arg:
                    arg = arg.split(':')
                    assert len(arg) > 1
                    CursorImage = ':'.join(arg[:-1])
                    CursorHotspot = map(int, arg[-1].split(','))
                else:
                    CursorImage = arg
                assert (BlackLevel >= 0) and (BlackLevel < 255)
            except:
                opterr("invalid parameter for --cursor")

    files = []
    for arg in args:
        files.extend(glob.glob(arg))
    if not files:
        opterr("no file to play")

    # determine whether it's a PDF file or a list of image files
    FileList = filter(IsImageFileName, files)
    if FileList:
        # there are image files in here, so switch to image file mode
        FileName = ""
    elif len(files) > 1:
        # no image files, but more than one parameter? that's bad, too
        opterr("too much files to play")
    else:
        # only one parameter, and it's not an image file -> seems to be OK
        FileName = files[0]


################################################################################

# wrapper around main() that ensures proper uninitialization
def run_main():
    global CacheFile
    try:
        main()
    finally:
        StopSound()
        # ensure that background rendering is halted
        Lrender.acquire()
        Lcache.acquire()
        # remove all temp files
        if 'CacheFile' in globals():
            del CacheFile
        try:
            for tmp in glob.glob(TempFileName + "*"):
                os.remove(tmp)
        except OSError:
            pass
        pygame.quit()

# use this function if you intend to use KeyJnote as a library
def run():
    try:
        run_main()
    except SystemExit, e:
        return e.code

if __name__=="__main__":
    ParseOptions(sys.argv[1:])
    run_main()
