Skip to content

Instantly share code, notes, and snippets.

@RavuAlHemio
Created July 27, 2013 19:34
Show Gist options
  • Save RavuAlHemio/6096000 to your computer and use it in GitHub Desktop.
Save RavuAlHemio/6096000 to your computer and use it in GitHub Desktop.
Gresenter: the two-window PDF presentation viewer
#!/usr/bin/env python2
# Gresenter: the two-window PDF presentation viewer
# One window: presentation (projector)
# Other window: presentation and notes (two different PDF files) side by side
# (laptop screen)
#
# Released into the public domain.
# http://creativecommons.org/publicdomain/zero/1.0/
from gi.repository import Gtk, Gdk, Poppler, Pango
class DumbPdfWidget(Gtk.DrawingArea):
def __init__(self, doc):
Gtk.DrawingArea.__init__(self)
self.connect('draw', self._do_draw)
self._doc = doc
self._bgcolor = Gdk.RGBA(0, 0, 0, 1)
self._black = False
self._white = False
if self._doc.get_n_pages() < 1:
raise ValueError("Document has less than one page.")
self._pgnum = 0
def _do_draw(self, widget, cr):
# f0tch p0ge
pg = self._doc.get_page(self._pgnum)
# fetch sizes
(ww, wh) = (self.get_allocated_width(), self.get_allocated_height())
(pw, ph) = pg.get_size()
# calculate aspect
wf = ww / pw
hf = wh / ph
# fit scaling
scale = min(wf, hf)
xoff = 0
yoff = 0
if wf > hf:
xoff = (ww - scale*pw) / 2
else:
yoff = (wh - scale*ph) / 2
# draw the background first
cr.set_source_rgba(*list(self._bgcolor))
cr.rectangle(0, 0, ww, wh)
cr.fill()
# transform
cr.translate(xoff, yoff)
cr.scale(scale, scale)
if self._black:
cr.set_source_rgba(0, 0, 0, 1)
cr.rectangle(0, 0, pw, ph)
cr.fill()
else:
# blank to white in any case
cr.set_source_rgba(1, 1, 1, 1)
cr.rectangle(0, 0, pw, ph)
cr.fill()
if not self._white:
# draw page
pg.render(cr)
def set_bgcolor(self, newclr):
self._bgcolor = newclr
self.queue_draw()
def get_bgcolor(self):
return self._bgcolor
def page_advance(self):
if self._pgnum >= self._doc.get_n_pages()-1:
return
self._pgnum += 1
self.queue_draw()
def page_regress(self):
if self._pgnum < 1:
return
self._pgnum -= 1
self.queue_draw()
def page_first(self):
self._pgnum = 0
self.queue_draw()
def page_last(self):
self._pgnum = self._doc.get_n_pages()-1
self.queue_draw()
def page_n(self, n):
if n < 0:
self.page_first()
elif n >= self._doc.get_n_pages():
self.page_last()
else:
self._pgnum = n
self.queue_draw()
def toggle_white(self):
self._white = not self._white
self._black = False
self.queue_draw()
def toggle_black(self):
self._black = not self._black
self._white = False
self.queue_draw()
def status(self):
return (self._pgnum, self._doc.get_n_pages(), self._black, self._white)
class GresenterWindow:
def _destruction(self, window):
Gtk.main_quit()
def _keypress(self, wdgt, evt):
self._app.keypress(evt.keyval)
def __init__(self, app, title, docpath):
self._app = app
self._win = Gtk.Window()
self._win.set_title(title)
self._win.connect('destroy', self._destruction)
self._win.connect('key-press-event', self._keypress)
self._doc = Poppler.Document.new_from_file(docpath, None)
self._pv = DumbPdfWidget(self._doc)
self._win.add(self._pv)
def show(self):
self._win.show_all()
def hide(self):
self._win.hide()
def page_advance(self):
self._pv.page_advance()
def page_regress(self):
self._pv.page_regress()
def page_first(self):
self._pv.page_first()
def page_last(self):
self._pv.page_last()
def page_n(self, n):
self._pv.page_n(n)
def toggle_white(self):
self._pv.toggle_white()
def toggle_black(self):
self._pv.toggle_black()
def status(self):
return self._pv.status()
class GresenterAdvancedWindow(GresenterWindow):
def __init__(self, app, title, prespath, notepath):
self._app = app
self._gotostr = ""
self._presdoc = Poppler.Document.new_from_file(prespath, None)
self._notedoc = Poppler.Document.new_from_file(notepath, None)
prespgs = self._presdoc.get_n_pages()
notepgs = self._notedoc.get_n_pages()
if prespgs != notepgs:
raise ValueError("different number of pages (pres {0}, note {1})".format(prespgs, notepgs))
self._win = Gtk.Window()
self._win.set_title(title)
self._win.connect('destroy', self._destruction)
self._win.connect('key-press-event', self._keypress)
self._pv = DumbPdfWidget(self._presdoc)
self._notepv = DumbPdfWidget(self._notedoc)
self._statlbl = Gtk.Label("hmm")
self._statlbl.override_font(Pango.FontDescription.from_string("Sans 24"))
self._bigbox = Gtk.VBox()
self._paneful = Gtk.HPaned()
self._paneful.pack1(self._notepv, True, False)
self._paneful.pack2(self._pv, True, False)
self._bigbox.pack_start(self._paneful, True, True, 0)
self._bigbox.pack_start(self._statlbl, False, False, 0)
self._win.add(self._bigbox)
self._updateStatus()
def _updateStatus(self):
(pgnum, pgcount, blk, wht) = self._app.pres_status()
txt = "{0}/{1}".format(pgnum+1, pgcount)
if blk:
txt += " BLACK"
if wht:
txt += " WHITE"
if len(self._gotostr) > 0:
txt += ", Go to: " + self._gotostr
self._statlbl.set_text(txt)
def page_advance(self):
GresenterWindow.page_advance(self)
self._notepv.page_advance()
self._updateStatus()
def page_regress(self):
GresenterWindow.page_regress(self)
self._notepv.page_regress()
self._updateStatus()
def page_first(self):
GresenterWindow.page_first(self)
self._notepv.page_first()
self._updateStatus()
def page_last(self):
GresenterWindow.page_last(self)
self._notepv.page_last()
self._updateStatus()
def page_n(self, n):
GresenterWindow.page_n(self, n)
self._notepv.page_n(n)
self._updateStatus()
def toggle_white(self):
self._updateStatus()
def toggle_black(self):
self._updateStatus()
def pressed_digit(self, n):
self._gotostr += "{0:d}".format(n)
self._updateStatus()
def pressed_enter(self):
self._app.page_n(int(self._gotostr) - 1)
self._gotostr = ""
self._updateStatus()
def pressed_escape(self):
self._gotostr = ""
self._updateStatus()
def pressed_backspace(self):
self._gotostr = self._gotostr[:-1]
self._updateStatus()
class GresenterApp:
def __init__(self, prespath, notepath):
self._preswin = GresenterWindow(self, "presentation", prespath)
self._notewin = GresenterAdvancedWindow(self, "notes", prespath, notepath)
def show(self):
self._preswin.show()
self._notewin.show()
def keypress(self, keyval):
if keyval in (Gdk.KEY_b, Gdk.KEY_B, Gdk.KEY_period, Gdk.KEY_KP_Decimal):
self._preswin.toggle_black()
self._notewin.toggle_black()
elif keyval in (Gdk.KEY_w, Gdk.KEY_W):
self._preswin.toggle_white()
self._notewin.toggle_white()
elif keyval in (Gdk.KEY_Page_Up, Gdk.KEY_Up, Gdk.KEY_Left):
self._preswin.page_regress()
self._notewin.page_regress()
elif keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Down, Gdk.KEY_Right):
self._preswin.page_advance()
self._notewin.page_advance()
elif keyval == Gdk.KEY_Home:
self._preswin.page_first()
self._notewin.page_first()
elif keyval == Gdk.KEY_End:
self._preswin.page_last()
self._notewin.page_last()
elif keyval >= Gdk.KEY_0 and keyval <= Gdk.KEY_9:
self._notewin.pressed_digit(keyval - Gdk.KEY_0)
elif keyval >= Gdk.KEY_KP_0 and keyval <= Gdk.KEY_KP_9:
self._notewin.pressed_digit(keyval - Gdk.KEY_KP_0)
elif keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
self._notewin.pressed_enter()
elif keyval == Gdk.KEY_BackSpace:
self._notewin.pressed_backspace()
elif keyval == Gdk.KEY_Escape:
# do not support escape as "quit"
self._notewin.pressed_escape()
def page_n(self, n):
self._preswin.page_n(n)
self._notewin.page_n(n)
def pres_status(self):
return self._preswin.status()
if __name__ == '__main__':
import sys, os.path
if len(sys.argv) != 3:
print >> sys.stderr, "Usage: {0} SLIDES NOTES".format(sys.argv[0])
sys.exit(1)
slides = "file://" + os.path.abspath(sys.argv[1])
notes = "file://" + os.path.abspath(sys.argv[2])
ga = GresenterApp(slides, notes)
ga.show()
Gtk.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment