diff --git a/fossgis14/__init__.py b/fossgis14/__init__.py index a9023bb..7d9bf41 100644 --- a/fossgis14/__init__.py +++ b/fossgis14/__init__.py @@ -1,5 +1,6 @@ -#!/usr/bin/python -# -*- coding: UTF-8 -*- +#!/usr/bin/python3 + +from renderlib import * # URL to Schedule-XML scheduleUrl = 'http://www.fossgis.de/konferenz/2014/programm/schedule.de.xml' @@ -157,7 +158,7 @@ def debug(): def tasks(queue): # iterate over all events extracted from the schedule xml-export - for event in events(): + for event in events(scheduleUrl, titlemap): # generate a task description and put them into the queue queue.put(Rendertask( diff --git a/make.py b/make.py index 30835da..8008204 100755 --- a/make.py +++ b/make.py @@ -1,44 +1,16 @@ #!/usr/bin/python3 -# -*- coding: UTF-8 -*- import sys -import glob import os -import re -import math import time import shutil -import errno -from urllib.request import urlopen 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 - -class Rendertask: - def __init__(self, infile, sequence, parameters={}, outfile=None, workdir='.'): - self.infile = infile - self.sequence = sequence - self.parameters = parameters - self.outfile = outfile - self.workdir = workdir - - def fromtupel(tuple): - return Rendertask(tuple[0], tuple[2], tuple[3], tuple[1]) - - def ensure(input): - if isinstance(input, tuple): - return Rendertask.fromtupel(input) - elif isinstance(input, Rendertask): - return input - else: - return None +import renderlib # Project-Name if len(sys.argv) < 2: @@ -47,191 +19,26 @@ if len(sys.argv) < 2: projectname = sys.argv[1].strip('/') try: - sys.path.append(projectname) - project = __import__(projectname) + project = renderlib.loadProject(projectname) except ImportError: print("you must specify a project-name as first argument, eg. './make.py sotmeu14'. The supplied value '{0}' seems not to be a valid project (there is no '{0}/__init__.py').".format(projectname)) sys.exit(1) -# 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) - -# 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) - -cssutils.ser.prefs.lineSeparator = ' ' -cssutils.log.setLevel(logging.FATAL) +renderlib.debug = ('--debug' in sys.argv) def render(infile, outfile, sequence, parameters={}, workdir=os.path.join(projectname, 'artwork')): - return rendertask(Rendertask(infile=infile, outfile=outfile, sequence=sequence, parameters=parameters, workdir=workdir)) - -def rendertask(task): - # in debug mode we have no thread-worker which prints its progress - if debug: - print("generating {0} from {1}".format(task.outfile, task.infile)) - - # make sure a .frames-directory exists in out workdir - ensurePathExists(os.path.join(task.workdir, '.frames')) - - # open and parse the input file - with open(os.path.join(task.workdir, task.infile), 'r') as fp: - svgstr = fp.read() - for key in task.parameters.keys(): - svgstr = svgstr.replace(key, xmlescape(str(task.parameters[key]))) - - svg = etree.fromstring(svgstr.encode('utf-8')) - - # frame-number counter - frameNr = 0 - - # iterate through the animation seqence frame by frame - # frame is a ... tbd - for frame in task.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(task.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] = str(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, encoding='unicode') ) - - # 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(task.workdir, frameNr), shell=True, universal_newlines=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(task.workdir, task.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(task.workdir, task.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(task.workdir, '.frames')) - - # remove the generated svg - ensureFilesRemoved(os.path.join(task.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 = urlopen(project.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 = [] - if event.find('persons') is not None: - 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 { - 'id': int(event.get('id')), - 'title': project.titlemap[id] if id in project.titlemap else event.find('title').text, - 'subtitle': event.find('subtitle').text if event.find('subtitle') is not None else '', - 'persons': personnames, - 'personnames': ', '.join(personnames) - } - -# expose helper-methods method to project -project.events = events -project.render = render -project.rendertask = rendertask -project.Rendertask = Rendertask - -project.fps = fps - -# 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; - -def easeLinear(t, b, c, d): - t=float(t)/d - return t*c+b - -# expose easings to project # HACKYYYYY -project.easeOutCubic = easeOutCubic -project.easeInCubic = easeInCubic -project.easeOutQuad = easeOutQuad -project.easeLinear = easeLinear + task = renderlib.Rendertask(infile=infile, outfile=outfile, sequence=sequence, parameters=parameters, workdir=workdir) + return renderlib.rendertask(task) # debug-mode selected by --debug switch -if debug: +if renderlib.debug: print("!!! DEBUG MODE !!!") + # expose debug-render method + project.render = render + # call into project which calls render as needed project.debug() @@ -286,7 +93,7 @@ def worker(): # loop until all tasks are done (when the thread fetches a sentinal from the queue) while True: # fetch a task from the queue - task = Rendertask.ensure(tasks.get()) + task = renderlib.Rendertask.ensure(tasks.get()) # if it is a stop-sentinal break out of the loop if task == None: @@ -301,7 +108,7 @@ def worker(): task.workdir = workdir # render with these arguments - rendertask(task) + renderlib.rendertask(task) # print that we're finished tprint('finished {0}, {1} tasks left'.format(task.outfile, max(0, tasks.qsize() - num_worker_threads))) diff --git a/renderlib.py b/renderlib.py new file mode 100644 index 0000000..9019e57 --- /dev/null +++ b/renderlib.py @@ -0,0 +1,182 @@ +#!/usr/bin/python3 + +import os +import sys +import glob +import math +import shutil +import errno +from lxml import etree +from xml.sax.saxutils import escape as xmlescape +import cssutils +import logging +import subprocess +from urllib.request import urlopen + +# Frames per second. Increasing this renders more frames, the avconf-statements would still need modifications +fps = 25 +debug = False + +cssutils.ser.prefs.lineSeparator = ' ' +cssutils.log.setLevel(logging.FATAL) + +def loadProject(projectname): + sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), projectname)) + return __import__(projectname) + +# 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; + +def easeLinear(t, b, c, d): + t=float(t)/d + return t*c+b + +class Rendertask: + def __init__(self, infile, sequence, parameters={}, outfile=None, workdir='.'): + self.infile = infile + self.sequence = sequence + self.parameters = parameters + self.outfile = outfile + self.workdir = workdir + + def fromtupel(tuple): + return Rendertask(tuple[0], tuple[2], tuple[3], tuple[1]) + + def ensure(input): + if isinstance(input, tuple): + return Rendertask.fromtupel(input) + elif isinstance(input, Rendertask): + return input + else: + return None + +# 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 rendertask(task): + # in debug mode we have no thread-worker which prints its progress + if debug: + print("generating {0} from {1}".format(task.outfile, task.infile)) + + # make sure a .frames-directory exists in out workdir + ensurePathExists(os.path.join(task.workdir, '.frames')) + + # open and parse the input file + with open(os.path.join(task.workdir, task.infile), 'r') as fp: + svgstr = fp.read() + for key in task.parameters.keys(): + svgstr = svgstr.replace(key, xmlescape(str(task.parameters[key]))) + + svg = etree.fromstring(svgstr.encode('utf-8')) + + # frame-number counter + frameNr = 0 + + # iterate through the animation seqence frame by frame + # frame is a ... tbd + for frame in task.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(task.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] = str(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, encoding='unicode') ) + + # 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(task.workdir, frameNr), shell=True, universal_newlines=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(task.workdir, task.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(task.workdir, task.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(task.workdir, '.frames')) + + # remove the generated svg + ensureFilesRemoved(os.path.join(task.workdir, '.gen.svg')) + + +# Download the Events-Schedule and parse all Events out of it. Yield a tupel for each Event +def events(scheduleUrl, titlemap={}): + print("downloading pentabarf schedule") + + # download the schedule + response = 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 = [] + if event.find('persons') is not None: + 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 { + 'id': int(event.get('id')), + 'title': titlemap[id] if id in titlemap else event.find('title').text, + 'subtitle': event.find('subtitle').text if event.find('subtitle') is not None else '', + 'persons': personnames, + 'personnames': ', '.join(personnames) + }