intro-outro-generator/eh14/make.py
2014-04-18 21:16:26 +02:00

448 lines
12 KiB
Python
Executable file

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import sys
import glob
import os
import re
import math
import shutil
import errno
import unicodedata
import urllib2
#import xml.etree.ElementTree as etree
from lxml import etree
from xml.sax.saxutils import escape as xmlescape
import cssutils
import logging
import textwrap
import tempfile
import threading
import multiprocessing
from threading import Thread, Lock
from Queue import Queue
# Frames per second. Increasing this renders more frames, the avconf-statements would still need modifications
fps = 25
# using --debug skips the threading, the network fetching of the schedule and
# just renders one type of video
debug = ('--debug' in sys.argv)
# using --offline only skips the network fetching and use a local schedule.de.xml
offline = ('--offline' in sys.argv)
# set charset of output-terminal
reload(sys)
sys.setdefaultencoding('utf-8')
# t: current time, b: begInnIng value, c: change In value, d: duration
# copied from jqueryui
def easeOutCubic(t, b, c, d):
t=float(t)/d-1
return c*((t)*t*t + 1) + b
def easeInCubic(t, b, c, d):
t=float(t)/d
return c*(t)*t*t + b;
def easeOutQuad(t, b, c, d):
t=float(t)/d
return -c *(t)*(t-2) + b;
# try to create all folders needed and skip, they already exist
def ensurePathExists(path):
try:
os.makedirs(path)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise
# remove the files matched by the pattern
def ensureFilesRemoved(pattern):
for f in glob.glob(pattern):
os.unlink(f)
# Normalizes string, converts to lowercase, removes non-alpha characters,
#and converts spaces to hyphens.
def slugify(value):
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
value = unicode(re.sub('[-\s]+', '-', value))
return value
# create a filename from the events' id and a slugified version of the title
def vorspannFilename(id, title):
return u'{0:04d}-{1}.dv'.format(id, slugify(unicode(title)))
# svg does not have a method for automatic line breaking, that rsvg is capable of
# so we do it in python as good as we can
def vorspannTitle(title):
return '</tspan><tspan x="120" dy="35">'.join(textwrap.wrap(title, 50))
def vorspannUrl(id):
return 'fossgis.de/konferenz/2014/programm/events/'+str(id)+'.de.html'
def abspannFrames():
# 5 Sekunden
# 2 Sekunden Fadein Lizenz
frames = 2*fps
for i in range(0, frames):
yield (
('lizenz','style', 'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
)
# 3 Sekunden stehenlassen
frames = 3*fps
for i in range(0, frames):
yield (
('lizenz','style', 'opacity', 1),
)
def vorspannFrames():
# 5 Sekunden
# 1 Sekunde Title Fadein
frames = 1*fps
for i in range(0, frames):
yield (
('title', 'style', 'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
('text', 'style', 'opacity', 0),
('lizenz','style', 'opacity', 0),
)
# 2 Sekunde Text Fadein
frames = 2*fps
for i in range(0, frames):
yield (
('title', 'style', 'opacity', 1),
('text', 'style', 'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
('lizenz','style', 'opacity', 0),
)
# 1 Sekunde Lizenz Fadein
frames = 1*fps
for i in range(0, frames):
yield (
('title', 'style', 'opacity', 1),
('text', 'style', 'opacity', 1),
('lizenz','style', 'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
)
# 1 Sekunde stehen bleiben
frames = 1*fps
for i in range(0, frames):
yield (
('title', 'style', 'opacity', 1),
('text', 'style', 'opacity', 1),
('lizenz','style', 'opacity', 1),
)
def pauseFrames():
# 7 Sekunden
# 3 Sekunde Text FadeIn
frames = 3*fps
for i in range(0, frames):
yield (
('text','style', 'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
)
# 3 Sekunde Text FadeOut
frames = 3*fps
for i in range(0, frames):
yield (
('text','style', 'opacity', "%.4f" % easeInCubic(i, 1, -1, frames)),
)
# 1 Sekunde stehen lassen
frames = 1*fps
for i in range(0, frames):
yield (
('text','style', 'opacity', 0),
)
cssutils.ser.prefs.lineSeparator = ' '
cssutils.log.setLevel(logging.ERROR)
def render(infile, outfile, sequence, parameters={}, workdir='artwork'):
# in debug mode we have no thread-worker which prints its progress
if debug:
print "generating {0} from {1}".format(outfile, infile)
# make sure a .frames-directory exists in out workdir
ensurePathExists(os.path.join(workdir, '.frames'))
# open and parse the input file
with open(os.path.join(workdir, infile), 'r') as fp:
svgstr = fp.read()
for key in parameters.keys():
svgstr = svgstr.replace(key, xmlescape(str(parameters[key])))
svg = etree.fromstring(svgstr)
# find all images and force them to absolute file-urls
namespaces = {'xlink': 'http://www.w3.org/1999/xlink', 'svg': 'http://www.w3.org/2000/svg'}
for el in svg.findall(".//svg:image[@xlink:href]", namespaces=namespaces):
el.attrib['{http://www.w3.org/1999/xlink}href'] = 'file:///' + os.path.realpath(workdir) + '/' + el.attrib['{http://www.w3.org/1999/xlink}href']
# frame-number counter
frameNr = 0
# iterate through the animation seqence frame by frame
# frame is a ... tbd
for frame in sequence():
# print a line for each and every frame generated
if debug:
print "frameNr {0:2d} => {1}".format(frameNr, frame)
# open the output-file (named ".gen.svg" in the workdir)
with open(os.path.join(workdir, '.gen.svg'), 'w') as fp:
# apply the replace-pairs to the input text, by finding the specified xml-elements by thier id and modify thier css-parameter the correct value
for replaceinfo in frame:
(id, type, key, value) = replaceinfo
for el in svg.findall(".//*[@id='"+id.replace("'", "\\'")+"']"):
if type == 'style':
style = cssutils.parseStyle( el.attrib['style'] if 'style' in el.attrib else '' )
style[key] = unicode(value)
el.attrib['style'] = style.cssText
elif type == 'attr':
el.attrib[key] = value
# write the generated svg-text into the output-file
fp.write( etree.tostring(svg) )
# invoke inkscape to convert the generated svg-file into a png inside the .frames-directory
os.system('cd {0} && inkscape --export-png=.frames/{1:04d}.png .gen.svg >/dev/null 2>&1'.format(workdir, frameNr))
# incrwement frame-number
frameNr += 1
# remove the dv we are about to (re-)generate
ensureFilesRemoved(os.path.join(workdir, outfile))
# invoke avconv aka ffmpeg and renerate a lossles-dv from the frames
# if we're not in debug-mode, suppress all output
os.system('cd {0} && avconv -ar 48000 -ac 2 -f s16le -i /dev/zero -f image2 -i .frames/%04d.png -target pal-dv -aspect 16:9 -shortest "{1}"'.format(workdir, outfile) + ('' if debug else '>/dev/null 2>&1'))
# as before, in non-debug-mode the thread-worker does all progress messages
if debug:
print "cleanup"
# remove the .frames-dir with all frames in it
shutil.rmtree(os.path.join(workdir, '.frames'))
# remove the generated svg
ensureFilesRemoved(os.path.join(workdir, '.gen.svg'))
# Download the Events-Schedule and parse all Events out of it. Yield a tupel for each Event
def events():
print "downloading pentabarf schedule"
# use --offline to skip networking
if offline:
# parse the offline-version
schedule = etree.parse('schedule.de.xml').getroot()
else:
# download the schedule
response = urllib2.urlopen('http://eh14.easterhegg.eu/pages/fahrplan/schedule.xml')
# read xml-source
xml = response.read()
# parse into ElementTree
schedule = etree.fromstring(xml)
# iterate all days
for day in schedule.iter('day'):
# iterate all rooms
for room in day.iter('room'):
# iterate events on that day in this room
for event in room.iter('event'):
# aggregate names of the persons holding this talk
personnames = []
for person in event.find('persons').iter('person'):
personnames.append(person.text)
# yield a tupel with the event-id, event-title and person-names
yield ( int(event.get('id')), event.find('title').text, event.find('subtitle').text or '', ', '.join(personnames) )
# debug-mode selected by --debug switch
if debug:
print "!!! DEBUG MODE !!!"
title = 'Sleep? Ain\'t nobody got time for that!'
subtitle = 'Physiologie von Schlaf und Wachzustand'
render(
'vorspann.svg',
os.path.join('..', str(5725)+".dv"),
vorspannFrames,
{
'$id': 5725,
'$title': title,
'$subtitle': subtitle,
'$personnames': 'Christina'
}
)
# render(
# 'abspann.svg',
# '../outro.dv',
# abspannFrames
# )
render('pause.svg',
'../pause.dv',
pauseFrames
)
sys.exit(0)
# threaded task queue
tasks = Queue()
# for too long titles ;)
titlemap = {
#708: "Neue WEB-Anwendungen des LGRB Baden-Württemberg im Überblick"
}
# iterate over all events extracted from the schedule xml-export
for (id, title, subtitle, personnames) in events():
if id in titlemap:
title = titlemap[id]
# generate a task description and put them into the queue
tasks.put((
'vorspann.svg',
str(id)+".dv",
vorspannFrames,
{
'$id': id,
'$title': title,
'$subtitle': subtitle,
'$personnames': personnames
}
))
# place a task for the outro into the queue
tasks.put((
'abspann.svg',
'outro.dv',
abspannFrames
))
# place the pause-sequence into the queue
tasks.put((
'pause.svg',
'pause.dv',
pauseFrames
))
# one working thread per cpu
num_worker_threads = multiprocessing.cpu_count()
print "{0} tasks in queue, starting {1} worker threads".format(tasks.qsize(), num_worker_threads)
# put a sentinel for each thread into the queue to signal the end
for _ in range(num_worker_threads):
tasks.put(None)
# this lock ensures, that only one thread at a time is writing to stdout
# and avoids output from multiple threads intermixing
printLock = Lock()
def tprint(str):
# aquire lock
printLock.acquire()
# print thread-name and message
print threading.current_thread().name+': '+str
# release lock
printLock.release()
# thread worker
def worker():
# generate a tempdir for this worker-thread and use the artwork-subdir as temporary folder
tempdir = tempfile.mkdtemp()
workdir = os.path.join(tempdir, 'artwork')
# save the current working dir as output-dir
outdir = os.getcwd()
# print a message that we're about to initialize our environment
tprint("initializing worker in {0}, writing result to {1}".format(tempdir, outdir))
# copy the artwork-dir into the tempdir
shutil.copytree('artwork', workdir)
# loop until all tasks are done (when the thread fetches a sentinal from the queue)
while True:
# fetch a task from the queue
task = tasks.get()
# if it is a stop-sentinal break out of the loop
if task == None:
break
# print that we're about to render a task
tprint('rendering {0}'.format(task[1]))
# render options
opts = (
# argument 0 is the input file. prepend the workdir
os.path.join(workdir, task[0]),
# argument 1 is the output file. prepend the outdir
os.path.join(outdir, task[1]),
# argument 2 is the frame generator, nothing to do here
task[2],
# argument 3 are the extra parameters
task[3] if len(task) > 3 else {},
# argument 4 is the workdir path
workdir
)
# render with these arguments
render(*opts)
# print that we're finished
tprint('finished {0}, {1} tasks left'.format(task[1], max(0, tasks.qsize() - num_worker_threads)))
# mark the task as finished
tasks.task_done()
# all tasks from the queue done, clean up
tprint("cleaning up worker")
# remove the tempdir
shutil.rmtree(tempdir)
# mark the sentinal as done
tasks.task_done()
# generate and start the threads
for i in range(num_worker_threads):
t = Thread(target=worker)
t.daemon = True
t.start()
# wait until they finished doing the work
tasks.join()
print "all worker threads ended"