diff --git a/sendezentrum14/artwork/bkd-sz-484a4f.jpg b/sendezentrum14/artwork/bkd-sz-484a4f.jpg new file mode 100644 index 0000000..55cb326 Binary files /dev/null and b/sendezentrum14/artwork/bkd-sz-484a4f.jpg differ diff --git a/sendezentrum14/artwork/overlay-1024x576.png b/sendezentrum14/artwork/overlay-1024x576.png new file mode 100644 index 0000000..fedf5ec Binary files /dev/null and b/sendezentrum14/artwork/overlay-1024x576.png differ diff --git a/sendezentrum14/artwork/overlay-720x576.png b/sendezentrum14/artwork/overlay-720x576.png new file mode 100644 index 0000000..9a5d4db Binary files /dev/null and b/sendezentrum14/artwork/overlay-720x576.png differ diff --git a/sendezentrum14/artwork/overlay.svg b/sendezentrum14/artwork/overlay.svg new file mode 100644 index 0000000..1e6fbac --- /dev/null +++ b/sendezentrum14/artwork/overlay.svg @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/sendezentrum14/artwork/pause.svg b/sendezentrum14/artwork/pause.svg new file mode 100644 index 0000000..faea6fa --- /dev/null +++ b/sendezentrum14/artwork/pause.svg @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + $text + diff --git a/sendezentrum14/artwork/sz.png b/sendezentrum14/artwork/sz.png new file mode 100644 index 0000000..0f159da Binary files /dev/null and b/sendezentrum14/artwork/sz.png differ diff --git a/sendezentrum14/make.py b/sendezentrum14/make.py new file mode 100755 index 0000000..3e0faaf --- /dev/null +++ b/sendezentrum14/make.py @@ -0,0 +1,469 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import sys +import glob +import os +import re +import math +import time +import shutil +import errno +import urllib2 +from lxml import etree +from xml.sax.saxutils import escape as xmlescape +import cssutils +import logging +import tempfile +import threading +import multiprocessing +from threading import Thread, Lock +import subprocess +from Queue import Queue + +# URL to Schedule-XML +scheduleUrl = 'http://www.fossgis.de/konferenz/2014/programm/schedule.de.xml' + +# For (really) too long titles +titlemap = { + #708: "Neue WEB-Anwendungen des LGRB Baden-Württemberg im Überblick" +} + +# 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) + +def abspannFrames(): + # 9 Sekunden + + # 3 Sekunden Fadein Logo + frames = int(3*fps) + for i in range(0, frames): + yield ( + ('logo', 'style', 'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)), + ('box', 'style', 'opacity', 0) + ) + + # 3 Sekunde Fadein Box + frames = 3*fps + for i in range(0, frames): + yield ( + ('logo', 'style', 'opacity', 1), + ('box', 'style', 'opacity', "%.4f" % easeOutQuad(i, 0, 1, frames)), + ('box', 'attr', 'transform', 'translate(0,%.4f)' % easeOutQuad(i, 94, -94, frames) ) + #('box', 'attr', 'transform', 'translate(%.4f,0)' % easeOutQuad(i, 960, -960, frames) ) + ) + + # 3 Sekunden stehen bleiben + frames = 3*fps + for i in range(0, frames): + yield ( + ('logo', 'style', 'opacity', 1), + ('box', 'style', 'opacity', 1) + ) + + +def vorspannFrames(): + # 7 Sekunden + + # 0.5 Sekunden stehen bleiben + frames = int(math.ceil(0.5*fps)) + for i in range(0, frames): + yield ( + ('logo', 'style', 'opacity', 0), + ('box', 'style', 'opacity', 0) + ) + + # 1.5 Sekunden Fadein Logo + frames = int(math.ceil(1.5*fps)) + for i in range(0, frames): + yield ( + ('logo', 'style', 'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)), + ('box', 'style', 'opacity', 0) + ) + + # 3 Sekunde Fadein Box + frames = 3*fps + for i in range(0, frames): + yield ( + ('logo', 'style', 'opacity', 1), + ('box', 'style', 'opacity', "%.4f" % easeOutQuad(i, 0, 1, frames)), + ('box', 'attr', 'transform', 'translate(0,%.4f)' % easeOutQuad(i, 198, -198, frames) ) + #('box', 'attr', 'transform', 'translate(%.4f,0)' % easeOutQuad(i, 960, -960, frames) ) + ) + + # 3 Sekunden stehen bleiben + frames = 3*fps + for i in range(0, frames): + yield ( + ('logo', 'style', 'opacity', 1), + ('box', 'style', 'opacity', 1) + ) + +def pauseFrames(): + # 12 Sekunden + + texts = { + 'text1': "0.0", + 'text2': "0.0", + 'text3': "0.0" + } + + for name in texts.keys(): + # 2 Sekunden einfaden + frames = 2*fps + for i in range(0, frames): + texts[name] = "%.4f" % easeOutQuad(i, 0, 1, frames) + + yield ( + ('text1', 'style', 'opacity', texts['text1']), + ('text2', 'style', 'opacity', texts['text2']), + ('text3', 'style', 'opacity', texts['text3']) + ) + + # 2 Sekunden ausfaden + frames = 2*fps + for i in range(0, frames): + texts[name] = "%.4f" % easeOutQuad(i, 1, -1, frames) + + yield ( + ('text1', 'style', 'opacity', texts['text1']), + ('text2', 'style', 'opacity', texts['text2']), + ('text3', 'style', 'opacity', texts['text3']) + ) + + texts[name] = "0.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 + errorReturn = subprocess.check_output('cd {0} && inkscape --export-png=.frames/{1:04d}.png .gen.svg 2>&1 >/dev/null'.format(workdir, frameNr), shell=True) + if errorReturn != '': + print "inkscape exitted with error\n"+errorReturn + sys.exit(42) + + # increment 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} && ffmpeg -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(scheduleUrl) + + # 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 !!!" + + render( + 'vorspann.svg', + '../intro.dv', + vorspannFrames, + { + '$id': 667, + '$title': 'OpenJUMP - Überblick, Neuigkeiten, Zusammenarbeit/Schnittstellen mit proprietärer Software', + '$subtitle': 'Even more news about OpenJUMP', + '$personnames': 'Matthias S.' + } + ) + + render( + 'abspann.svg', + '../outro.dv', + abspannFrames + ) + + render('pause.svg', + '../pause.dv', + pauseFrames + ) + + sys.exit(0) + + + +# threaded task queue +tasks = Queue() + +# 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() + +# List of running threads +threads = [] + +# generate and start the threads +for i in range(num_worker_threads): + t = Thread(target=worker) + t.daemon = True + t.start() + threads.append(t) + +# wait until they finished doing the work +# we're doing it the manual way because tasks.join() would wait until all tasks are done, +# even if the worker threads crash due to broken svgs, Ctrl-C termination or whatnot +while True: + if tasks.empty() == True: + break + + # sleep while the workers work + time.sleep(1) + + # check if all worker-threads are still alive + thread_count = len(filter(lambda t: t.is_alive(), threads)) + + # exit otherwise + if thread_count != num_worker_threads: + tprint("{0} of {1} threads have died, ending".format(num_worker_threads - thread_count, num_worker_threads)) + sys.exit(23) + +print "all worker threads ended"