#!/usr/bin/python3 ''' SAL's presentation viewer, version 1.1 (c) 2005,2014,2018-2021 Jan ONDREJ (SAL) 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. ''' from __future__ import print_function import pygame import time import sys import re import copy import os import html from pygame.constants import * from io import StringIO import effect default_size = 1024, 768 size = 1024, 768 charset = "UTF-8" TIMER = 60*45 # in seconds TIME0 = 0 MOUSE_VISIBILITY = 20 VSPACE = 1.0 ARGV0 = sys.argv[0] HTML_TEMPLATE = '''\
PAGE %(CONTENT)s
PREVIOUS NEXT
''' PHP_HEADER = '''\ ''' PHP_TAILER = '''\ ''' def colors(color): COLORS = { 'black': (0, 0, 0), 'red': (255, 0, 0), 'green': (0, 255, 0), 'blue': (0, 0, 255), 'yellow': (255, 255, 0), 'purple': (255, 0, 255), 'cyan': (0, 255, 255), 'gray': (180, 180, 180), 'gray70': (178, 178, 178), 'white': (255, 255, 255), 'navyblue': (0, 0, 150) } return COLORS[color] class ptfont_t: font_paths = [ #'/usr/share/fonts/bitstream-vera-sans-mono-fonts', #'/usr/share/fonts/liberation/', os.path.join(os.path.dirname(ARGV0), 'fonts/'), './' ] + [x[0] for x in os.walk("/usr/share/fonts")] font_files = { "standard": 'DejaVuLGCSans.ttf', #"fixed": 'VeraMoBd.ttf', "fixed": 'DejaVuSansMono-Bold.ttf', "table": 'DejaVuLGCSansMono-Bold.ttf', "nice": "merlinn_.ttf", #"nice": 'Caveat-Bold.ttf', #"nice": "Pacifico-Regular.ttf", } loaded_fonts = {} scale = 6.0*size[1]/600.0 def font_filenames(self, fontname): if fontname in self.font_files: for fp in self.font_paths: yield os.path.join(fp, self.font_files[fontname]) for fp in self.font_paths: yield os.path.join(fp, fontname+'.ttf') yield os.path.join(fp, fontname) def load(self, fontname, fontsize): if not (fontname, fontsize) in list(self.loaded_fonts.keys()): for filename in self.font_filenames(fontname): try: self.loaded_fonts[fontname, fontsize] = pygame.font.Font( filename, int(self.scale*float(fontsize))) return except IOError: pass raise ValueError("FontNotFound %s" % fontname) def get(self, fontname, fontsize): global VSPACE if not (fontname, fontsize) in list(self.loaded_fonts.keys()): self.load(fontname, fontsize) VSPACE = 1.0 # if 'DejaVu' in self.font_files[fontname]: # VSPACE = 0.75 return self.loaded_fonts[fontname, fontsize] class defaults_t: def __init__(self): self.line = {0: parse_args('size 5, left, font "standard", prefix 0, ' 'back "navyblue", fore "white"')} self.tabs = {} def add(self, line, args): self.line[int(line)] = parse_args(args) def copy(self): self.line = self.line.copy() return self def get_last(self, line, param): for l in range(line, -2, -1): try: return self.line[l].get_last(param) except KeyError: continue except IndexError: continue style_aliases = {} class render_page: def __init__(self, color, dirname): self.dirname = dirname self.y = 0 self.linesize = 0 self.xspace = 0 self.printed_chars = '' self.params = { 'font': 'standard', 'size': 5, 'prefix': 0, 'align': 'left', 'fore': (255, 255, 255), 'icon': [], 'marker': 0 } self.param_set = {} for k in list(self.params.keys()): self.param_set[k] = False screen0.fill(color) def vlsize(self): wp, hp = self.font.render('AMYX', 1, (0, 0, 0)).get_size() if self.linesize < hp: self.linesize = hp*VSPACE def newline(self, parent=None): # marker if parent: if parent.linesize > self.linesize: self.linesize = parent.linesize if parent.y > self.y: self.y = parent.y self.y += self.linesize for delkey in ('mark', 'again', 'pause', 'bar', 'vgap', 'alias', 'call'): if delkey in self.params: del self.params[delkey] self.linesize = 0 self.xspace = 0 self.printed_chars = '' def before(self): # show a bar if 'bar' in self.params: for img_name in [self.params['bar'], 'rulers/blurulr1.gif']: try: img = pygame.transform.scale( pygame.image.load(os.path.join( self.dirname, img_name)), (size[0], 5) ) if self.params['bar']: # epty string? print("bar:", img_name) screen0.blit(img, (0, self.y)) break except: pass # vgap if 'vgap' in self.params: w, h = self.font.render('X', 1, (0, 0, 0)).get_size() self.y += int(h*self.params['vgap']/100) print("vgap:", 'vgap' in self.params, self.y) if 'bar' in self.params: self.newline() def after(self): pass def echo(self, data, special=True): # prepend default prefix if it is left if self.params['align'] == 'left': try: self.xspace += int(int(self.params['prefix'])*size[0]/100) except (ValueError, TypeError): prefix = self.font.render( self.params['prefix'], 1, self.params['fore']) w, h = prefix.get_size() screen0.blit(prefix, (0, self.y)) self.xspace += w if self.params['icon']: icon_size = int(self.params['icon'][2]) # get font size w, h = self.font.render('X', 1, (0, 0, 0)).get_size() cx, cy = self.xspace+w/2, (self.y)+h/2 sx, sy = w*icon_size/200, h*icon_size/200 polygons = { 'box': [(cx-sx, cy-sx), (cx+sx, cy-sx), (cx+sx, cy+sx), (cx-sx, cy+sx)], 'arc': [(cx-sx, cy-sy/2), (cx-sx, cy+sy/2), (cx+sx-1, cy)], 'delta1': [(cx, cy-sy), (cx+sx, cy+sy), (cx-sx, cy+sy)], 'delta2': [(cx-sx, cy-sy), (cx+sx, cy-sy), (cx, cy+sy)], 'delta3': [(cx-sx, cy-sy), (cx-sx, cy+sy), (cx+sx-1, cy)], 'delta4': [(cx+sx, cy-sy), (cx+sx, cy+sy), (cx-sx+1, cy)] } if special and data: pygame.draw.polygon(screen0, colors(self.params['icon'][1]), polygons[self.params['icon'][0]]) self.xspace += int(w*1.3) self.printed_chars += data x = len(data)+1 while data: text = self.font.render(data[:x], 1, self.params['fore']) w, h = text.get_size() if (w+self.xspace) > (size[0]): try: x = data[:x].rindex(' ') continue except: pass if self.params['align'] == 'center': screen0.blit(text, ((size[0]-w)/2, self.y)) elif self.params['align'] == 'left': screen0.blit(text, (self.xspace, self.y)) elif self.params['align'] == 'right': screen0.blit(text, (size[0]-w, self.y)) self.vlsize() if x < len(data): self.newline() self.echo(data[x+1:], False) self.xspace += w return self.vlsize() def icon(self): # %icon # %icon self.printed_chars += '^' if not self.params['icon']: return '' return '>' def bimage(self): try: img = pygame.transform.scale( pygame.image.load(os.path.join( self.dirname, self.params['bimage'])), size ) screen0.blit(img, (0, 0)) except: return del self.params['bimage'] def image(self, filename): if ' ' in filename: img_scale = [float(x)/100.0 for x in filename.split(' ', 2)[1:]] filename = filename.split(' ', 1)[0] else: img_scale = [1.0, 1.0] # prepend default prefix if it is left if (self.params['align'] == 'left') and (self.xspace == 0): try: self.xspace += int(int('0'+self.params['prefix'])*size[0]/100) except (ValueError, TypeError) as e: self.xspace = self.font.render( self.params['prefix'], 1, self.params['fore']).get_size()[0] # load image try: img = pygame.image.load(os.path.join(self.dirname, filename)) oldsize = img.get_size() except: return newsize = (int(oldsize[0]*size[0]/default_size[0]*img_scale[0]), int(oldsize[1]*size[1]/default_size[1]*img_scale[1])) img = pygame.transform.scale(img, newsize) w, h = img.get_size() if self.params['align'] == 'center': print("Image:", ((size[0]-w)/2, self.y), self.params['prefix'], self.params['align'], filename) screen0.blit(img, ((size[0]-w)/2, self.y)) elif self.params['align'] == 'left': print("Image:", (self.xspace, self.y), self.params['prefix'], self.params['align'], filename) screen0.blit(img, (self.xspace, self.y)) elif self.params['align'] == 'right': print("Image:", (size[0]-w, self.y), self.params['prefix'], self.params['align'], filename) screen0.blit(img, (size[0]-w, self.y)) self.xspace += w if self.linesize < h: self.linesize = h*VSPACE # TODO: save/load alias must start with page on which aliases are defined def save_alias(self, name): style_aliases[name] = copy.copy(self.params) print("Alias saved:", name, self.params) def load_alias(self, name): self.params = copy.copy(style_aliases[name]) print("Alias loaded:", name, self.params) class parse_args: def __init__(self, cmds=''): self.args = [] if cmds: self.add(cmds) def __repr__(self): return str(self.args) def __contains__(self, a): return a in self.keys() def add(self, cmds): quote = False arg = '' cmd = [] n = 0 for c in cmds+',': n += 1 if c in ['"', "'"]: quote = not quote if not quote: if c == ' ' and arg != '': cmd.append(arg.strip().strip('"\'')) arg = '' elif c == ',': if arg: cmd.append(arg.strip().strip('"\'')) arg = '' # print n,arg,cmd if cmd[0].lower() in ['center', 'left', 'right']: self.args.append(('align', cmd[0].lower())) elif len(cmd) == 1: self.args.append((cmd[0].lower(), [])) elif len(cmd) == 2: self.args.append((cmd[0].lower(), cmd[1])) else: self.args.append((cmd[0].lower(), cmd[1:])) cmd = [] continue arg += c def get_last(self, key): r = None for k, v in self.args: if k == key: r = v if r != None: return r else: raise KeyError(key) def has_key(self, key): for k, v in self.args: if k == key: return True return False def keys(self): k = [] for key, val in self.args: k.append(key) return k def values(self): v = [] for key, val in self.args: v.append(val) return v def items(self): args = {} for k, v in self.args: args[k] = v return args class ropen: def __init__(self, filename): self.dirname = os.path.dirname(filename) self.f = [open(filename)] def readline(self): r = self.f[-1].readline() if not r: self.f = self.f[:-1] if self.f: return self.readline() return r def open(self, filename): self.f.append(open(os.path.join(self.dirname, filename))) def load_pages(filename): defaults = defaults_t() pages = [] cpage = None args = parse_args() f = ropen(filename) while True: l = f.readline() if not l: break l = re.compile('%%.*$').sub('', l.strip('\r\n')) # read next line if this ends with backslash while l and l[-1] == '\\': l = l[:-1]+re.compile('%%.*$').sub('', f.readline().strip('\t\r\n')) # is this line a special command? if l and l[0] == '%': reg_include = re.compile('^%include +"([^"]+)"', re.I).search(l) if reg_include: f.open(reg_include.group(1)) continue reg_deffont = re.compile( '^%deffont +"([^"]+)" +(.+)$', re.I).search(l) if reg_deffont: # ignored now continue reg_default = re.compile( '^%default +([0-9]+) +(.+)$', re.I).search(l) if reg_default: defaults.add(int(reg_default.group(1)), reg_default.group(2)) continue reg_tab = re.compile('^%tab +([0-9]+) +(.+)$', re.I).search(l) if reg_tab: defaults.tabs[int(reg_tab.group(1))] = parse_args( reg_tab.group(2)) continue reg_nodefault = re.compile('^%nodefault *$', re.I).search(l) if reg_nodefault: cdefaults = defaults_t() continue reg_page = re.compile('^%page *$', re.I).search(l) if reg_page: if cpage: pages.append((cpage, cdefaults.copy())) cpage = [] cdefaults = defaults args = parse_args() continue # add new arguments: args.add(l[1:]) elif cpage != None: cpage.append((l, args)) args = parse_args() if cpage: pages.append((cpage, cdefaults.copy())) return pages, f.dirname def param_convert(key, value): param_convert = { 'size': float, 'fore': colors, 'vgap': int, } try: return param_convert[key](value) except KeyError: return value def waitfor(event_type=KEYUP): # wait for keyup for event in pygame.event.get(): if event.type != event_type: print("EVENTUP:", event) break pygame.event.clear() def pause(pg_num, total=99999): global MOUSE_VISIBILITY while True: # show timer if TIME0: tx1 = (size[0]-1)*(pg_num-1)/(total-1) tx2 = (size[0]-1)*pg_num/(total-1) pygame.draw.line(screen, colors('green'), (tx2, size[1]-3), (tx2, size[1]-1)) tx0 = size[0]*(time.time()-TIME0)/TIMER if tx0 < tx1: txc = colors('yellow') elif tx0 < tx2: txc = colors('green') else: txc = colors('red') pygame.draw.line(screen, txc, (0, size[1]-1), (tx0, size[1]-1)) pygame.display.flip() event = pygame.event.wait() pygame.event.clear() print("EVENT:", event) if event.type == KEYUP: if event.key in [K_ESCAPE, K_q]: sys.exit() elif event.key in [K_RIGHT, K_DOWN, K_PAGEDOWN, K_SPACE]: return 1 elif event.key in [K_LEFT, K_UP, K_PAGEUP, K_BACKSPACE]: return -1 elif event.key == K_f: pygame.display.toggle_fullscreen() elif event.key == K_m: pygame.mouse.set_visible(1) elif event.key == K_n: pygame.mouse.set_visible(0) elif event.type == MOUSEMOTION: MOUSE_VISIBILITY = 15 elif event.type == MOUSEBUTTONUP: if event.button == 1: return 1 elif event.button == 3: return -1 if MOUSE_VISIBILITY > 0: MOUSE_VISIBILITY -= 1 pygame.mouse.set_visible(1) else: pygame.mouse.set_visible(0) time.sleep(0.1) return 0 class save: fext = "html" template = HTML_TEMPLATE header = '
' tailer = '
' def __init__(self, dirname='.', html=False): self.dirname = dirname self.html = html if html: self.index = open(dirname+'/index.'+self.fext, 'w') self.index.write(self.header % dict(FILENAME="")) else: self.index = StringIO() def __del__(self): self.index.write(self.tailer) self.index.close() def escape(self, s): return html.escape(s.decode('UTF-8')) def add(self, pg_num, screen, data=''): self.index.write('page %02d\n' % (pg_num, self.fext, pg_num, pg_num)) pygame.image.save(screen, "%s/page-%02d.tga" % (self.dirname, pg_num)) os.system('convert "%s/page-%02d.tga" "%s/page-%02d.jpg"' % (self.dirname, pg_num, self.dirname, pg_num)) if self.html: os.system('convert -scale 200x150 "%s/page-%02d.tga" "%s/small-%02d.jpg"' % (self.dirname, pg_num, self.dirname, pg_num)) os.unlink("%s/page-%02d.tga" % (self.dirname, pg_num)) # make HTML page if self.html: f = open('%s/page-%02d.%s' % (self.dirname, pg_num, self.fext), 'w') pgp, pgm = pg_num+1, pg_num-1 if pg_num >= len(pages): pgp = pg_num if pg_num <= 1: pgm = 1 f.write(self.template % dict( FILENAME=filename, CURRENT=pg_num, CONTENT=self.escape(data), PREV=pgm, NEXT=pgp, FEXT=self.fext )) f.close() class save_php(save): fext = "php" template = PHP_HEADER + save.template + PHP_TAILER header = PHP_HEADER + save.header tailer = save.tailer + PHP_TAILER def show_pages(pages, dirname, pg_num=0): global TIME0 # show pages while True: if pg_num < 0: pg_num = 0 if pg_num >= len(pages): pg_num = len(pages)-1 if save_presentation: sys.exit(0) cpage, defaults = pages[pg_num] p = render_page(colors(defaults.get_last(1, "back")), dirname) pauses = 0 lineout = '' n = 1 old_cursor_position = (0, 0) for line, params in cpage: if 'cont' in params: p.xspace, p.y = old_cursor_position p.params = pwt.params p.params['prefix'] = '' p.params['icon'] = '' n -= 1 else: # Try to set line defaults try: for k, v in defaults.line[n].args: try: if not p.param_set[k]: p.params[k] = param_convert(k, v) except KeyError: p.params[k] = param_convert(k, v) except KeyError: pass # no defaults for this line if 'bimage' in p.params: p.bimage() # Try to set params set for this line for k, v in params.args: if (not save_presentation) and (k == 'pause'): pauses += 1 print("PAUSE", pauses) if pauses > 1: a = effect.fadeover(size) a.copyfrom(anim) a.begin(screen0, pg_num) a.do() anim.copyfrom(a) else: anim.begin(screen0, pg_num) anim.do() pause(pg_num) continue p.params[k] = param_convert(k, v) p.param_set[k] = True if k == 'mark': p.params['marker'] = p.y if k == 'again': p.y = p.params['marker'] if k == 'image': # show images #p.font = ptfont.get(pwt.params['font'],pwt.params['size']) p.image(v) if k == 'alias': p.save_alias(v) if k == 'call': p.load_alias(v) # parse tabs pwt = copy.copy(p) pwt.params = copy.copy(p.params) try: t = 0 while line[t] == '\t': t += 1 except IndexError: pass if t > 0: line = line[t:] try: if 'prefix' in defaults.tabs[t]: try: pwt.params['prefix'] += defaults.tabs[t].get_last( 'prefix') except TypeError: pwt.params['prefix'] = defaults.tabs[t].get_last( 'prefix') if 'size' in defaults.tabs[t]: pwt.params['size'] = defaults.tabs[t].get_last('size') if 'font' in defaults.tabs[t]: pwt.params['font'] = defaults.tabs[t].get_last('font') if 'fore' in defaults.tabs[t]: pwt.params['fore'] = defaults.tabs[t].get_last('fore') if 'vgap' in defaults.tabs[t]: pwt.vgap = defaults.tabs[t].get_last('vgap') if 'icon' in defaults.tabs[t]: pwt.params['icon'] = defaults.tabs[t].get_last('icon') except KeyError: print("TAB NOT FOUND:", t) sys.exit(1) # set font p.font = ptfont.get(pwt.params['font'], pwt.params['size']) pwt.font = p.font # is there an vgap? pwt.before() # show line if line != None: print(n, p.y, p.params['size'], t, p.params['align'], end=' ') print(p.params['font'], p.params['fore'], end=' ') print(line.encode(charset)) # if line and line[0]=='\\': # line = line[1:] line = re.compile(r'\\(.)').sub('\\1', line) pwt.echo(line.replace('\t', ' ')) old_cursor_position = (pwt.xspace, pwt.y) lineout += line+'\n' p.after() p.newline(pwt) n += 1 print("PAGE:", pg_num) anim.begin(screen0, pg_num) if save_presentation: anim.begin(screen0, pg_num) anim.done() pg_num += 1 save_presentation.add(pg_num, screen0, lineout.encode(charset)) else: print("Pauses:", pauses) if pauses == 0: anim.begin(screen0, pg_num) anim.do() else: if anim_effect.startswith("effect.nothing"): a = effect.nothing(size) else: a = effect.fadeover(size) a.copyfrom(anim) a.begin(screen0, pg_num) a.do() anim.copyfrom(a) pg_num += pause(pg_num, len(pages)) if (pg_num == 1) and (TIME0 == 0): TIME0 = time.time() def usage(): print("SAL's presentation viewer") print("(c) 2005-2006 Jan ONDREJ (SAL) ") print("Licensed under GNU GPL.") print("") print("Usage: %s [options] filename.mgp" % os.path.basename(ARGV0)) print(" -h --help this help") print(" -p --page X set starting page to X") print(" --hd | --fhd use HD or FullHD resolution") print(" -s --save DIR save presentetion as HTML pages in DIR") print(" -P --php DIR save presentation as PHP pages in DIR") print(" -a effect | -n use this animation effect | no effect") print(" -t minutes presentation time") # parse command line arguments anim_effect = 'effect.any(size)' pg_num = 0 save_presentation = None if not sys.argv[1:]: usage() sys.exit() args = sys.argv[1:] args.reverse() while args: arg = args.pop() if arg[0] == '-': if arg in ('-h', '--help'): usage() sys.exit(0) elif arg=="--hd": size = default_size = 1280, 900 elif arg in ('--fhd', '--fullhd'): size = default_size = 1920, 1080 elif arg in ('-p', '--page'): pg_num = int(args.pop()) elif arg in ('-s', '--save', '-w', '--write'): save_presentation = save(args.pop()) elif arg in ('-P', '--php'): save_presentation = save_php(args.pop()) elif arg in ('-a', '--anim'): anim_effect = 'effect.%s(size)' % args.pop() elif arg=='-n': anim_effect = 'effect.nothing(size)' elif arg in ('-t', '--time'): TIMER = 60*int(args.pop()) else: filename = arg pages, dirname = load_pages(filename) # pygame.init() pygame.display.init() pygame.font.init() #pygame.key.set_repeat(1000, 100) screen = pygame.display.set_mode(size) # ,pygame.DOUBLEBUF) print(pygame.image.get_extended()) pygame.display.set_caption('Presentation: '+filename) ptfont = ptfont_t() screen0 = pygame.Surface(size) anim = eval(anim_effect) show_pages(pages, dirname, pg_num) pygame.quit()