From 8eeca3485d67733cafed88ba301b40b34e098354 Mon Sep 17 00:00:00 2001 From: Daniel Molkentin Date: Thu, 1 Aug 2019 23:20:32 +0200 Subject: [PATCH] refactor renderlib to allow for single frame rendering --- forumoe19/__init__.py | 19 +++-- renderlib.py | 180 ++++++++++++++++++++++++------------------ 2 files changed, 117 insertions(+), 82 deletions(-) diff --git a/forumoe19/__init__.py b/forumoe19/__init__.py index b85bffc..906ec4c 100644 --- a/forumoe19/__init__.py +++ b/forumoe19/__init__.py @@ -124,28 +124,35 @@ def tasks(queue, args, idlist, skiplist): queue.put(Rendertask( infile = 'intro.svg', outfile = str(event['id'])+".ts", - sequence = introFrames, parameters = { '$ID': event['id'], '$TITLE': event['title'], '$SUBTITLE': event['subtitle'], '$SPEAKER': event['personnames'] } - )) + ).animated(introFrames)) # place a task for the outro into the queue if not "out" in skiplist: queue.put(Rendertask( infile = 'outro.svg', - outfile = 'outro.ts', - sequence = outroFrames - )) + outfile = 'outro.ts' + ).animated(outroFrames)) for person in persons(scheduleUrl, personmap, taglinemap): queue.put(Rendertask( infile = 'insert.svg', outfile = "insert_{}.mkv".format(person['person'].replace("/", "_")), - sequence = bbFrames, + parameters = { + '$PERSON': person['person'], + '$TAGLINE': person['tagline'], + } + ).animated(bbFrames)) + + for person in persons(scheduleUrl, personmap, taglinemap): + queue.put(Rendertask( + infile = 'insert.svg', + outfile = "insert_{}.png".format(person['person'].replace("/", "_")), parameters = { '$PERSON': person['person'], '$TAGLINE': person['tagline'], diff --git a/renderlib.py b/renderlib.py index d29221f..ff9e8f0 100644 --- a/renderlib.py +++ b/renderlib.py @@ -40,20 +40,31 @@ def easeDelay(easer, delay, t, b, c, d, *args): class Rendertask: - def __init__(self, infile, sequence, parameters={}, outfile=None, workdir='.'): + def __init__(self, infile, parameters={}, outfile=None, workdir='.', sequence=None): if isinstance(infile, list): self.infile = infile[0] # self.audiofile = infile[1] else: self.infile = infile self.audiofile = None - self.sequence = sequence self.parameters = parameters self.outfile = outfile self.workdir = workdir + self.sequence = sequence # deprecated, use animated() + + def animated(self, sequence): + atask = self + atask.sequence = sequence + return atask + + def is_animated(self): + return self.sequence != None def fromtupel(tuple): - return Rendertask(tuple[0], tuple[2], tuple[3], tuple[1]) + task = Rendertask(tuple[0], tuple[2], tuple[1]) + if len(tuple) > 3: + task = task.animated(tuple[3]) + return task def ensure(input): if isinstance(input, tuple): @@ -63,7 +74,6 @@ class Rendertask: else: return None - # try to create all folders needed and skip, they already exist def ensurePathExists(path): try: @@ -79,39 +89,62 @@ def ensureFilesRemoved(pattern): os.unlink(f) -def rendertask(task): - global args - # in debug mode we have no thread-worker which prints its progress - if debug: - print("generating {0} from {1}".format(task.outfile, task.infile)) - - if args.skip_frames and 'only_rerender_frames_after' not in task.parameters: - if os.path.isdir(os.path.join(task.workdir, '.frames')): - shutil.rmtree(os.path.join(task.workdir, '.frames')) - - # make sure a .frames-directory exists in out workdir - ensurePathExists(os.path.join(task.workdir, '.frames')) - - # open and parse the input file +def svgTemplate_open(task): 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]))) + return fp.read() - parser = etree.XMLParser(huge_tree=True) - svg = etree.fromstring(svgstr.encode('utf-8'), parser) +def svgTemplate_replacetext(svgstr, task): + for key in task.parameters.keys(): + svgstr = svgstr.replace(key, xmlescape(str(task.parameters[key]))) + return svgstr + +def svgTemplate_transform(svgstr, frame, task): + parser = etree.XMLParser(huge_tree=True) + svg = etree.fromstring(svgstr.encode('utf-8'), parser) + # apply the replace-pairs to the input text, by finding the specified xml-elements by their id and modify their 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] = str(value) + elif type == 'text': + el.text = str(value) # if '$subtitle' in task.parameters and task.parameters['$subtitle'] == '': # child = svg.findall(".//*[@id='subtitle']")[0] # child.getparent().remove(child) + return etree.tostring(svg, encoding='unicode') - # frame-number counter - frameNr = 0 +def svgTemplate_write(svgstr, task): + # open the output-file (named ".gen.svg" in the workdir) + outfile = os.path.join(task.workdir, '.gen.svg') + with open(outfile, 'w') as fp: + # write the generated svg-text into the output-file + fp.write(svgstr) + return outfile - # iterate through the animation seqence frame by frame - # frame is a ... tbd - cache = {} - for frame in task.sequence(task.parameters): +def renderFrame(infile, task, outfile): + width = 1920 + height = 1080 + infile = '{0}/.gen.svg'.format(task.workdir) + if args.imagemagick: + # invoke imagemagick to convert the generated svg-file into a png inside the .frames-directory + with Image(filename=infile) as img: + with img.convert('png') as converted: + converted.save(filename=outfile) + else: + # invoke inkscape to convert the generated svg-file into a png inside the .frames-directory + cmd = 'cd {0} && inkscape --export-background=white --export-background-opacity=0 --export-width={1} --export-height={2} --export-png="{3}" "{4}" 2>&1 >/dev/null'.format(task.workdir, width, height, outfile, infile) + errorReturn = subprocess.check_output(cmd, shell=True, universal_newlines=True, stderr=subprocess.STDOUT) + if errorReturn != '': + print("inkscape exitted with error\n" + errorReturn) + # sys.exit(42) + +def cachedRenderFrame(frame, frameNr, task, cache): skip_rendering = False # skip first n frames, to speed up rerendering during debugging if 'only_rerender_frames_after' in task.parameters: @@ -135,58 +168,35 @@ def rendertask(task): framedir = task.workdir + "/.frames/" shutil.copyfile("{0}/{1:04d}.png".format(framedir, cache[frame]), "{0}/{1:04d}.png".format(framedir, frameNr)) - frameNr += 1 - continue + return elif not skip_rendering: cache[frame] = frameNr - # 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 + svgstr = svgTemplate_open(task) + svgstr = svgTemplate_replacetext(svgstr, task) + svgstr = svgTemplate_transform(svgstr, frame, task) + svgfile = svgTemplate_write(svgstr, task) - 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] = str(value) - - elif type == 'text': - el.text = str(value) - - if not skip_rendering: - # open the output-file (named ".gen.svg" in the workdir) - with open(os.path.join(task.workdir, '.gen.svg'), 'w') as fp: - # write the generated svg-text into the output-file - fp.write(etree.tostring(svg, encoding='unicode')) - - if task.outfile.endswith('.ts') or task.outfile.endswith('.mov') or task.outfile.endswith('.mkv'): - width = 1920 - height = 1080 - else: - width = 1024 - height = 576 - - if args.imagemagick: - # invoke imagemagick to convert the generated svg-file into a png inside the .frames-directory - infile = '{0}/.gen.svg'.format(task.workdir) - outfile = '{0}/.frames/{1:04d}.png'.format(task.workdir, frameNr) - with Image(filename=infile) as img: - with img.convert('png') as converted: - converted.save(filename=outfile) - else: - # invoke inkscape to convert the generated svg-file into a png inside the .frames-directory - cmd = 'cd {0} && inkscape --export-background=white --export-background-opacity=0 --export-width={2} --export-height={3} --export-png=$(pwd)/.frames/{1:04d}.png $(pwd)/.gen.svg 2>&1 >/dev/null'.format(task.workdir, frameNr, width, height) - errorReturn = subprocess.check_output(cmd, shell=True, universal_newlines=True, stderr=subprocess.STDOUT) - if errorReturn != '': - print("inkscape exitted with error\n" + errorReturn) - # sys.exit(42) + outfile = '{0}/.frames/{1:04d}.png'.format(task.workdir, frameNr) + renderFrame(svgfile, task, outfile) # increment frame-number frameNr += 1 + +def rendertask_image(task): + svgstr = svgTemplate_open(task) + svgstr = svgTemplate_replacetext(svgstr, task) + svgfile = svgTemplate_write(svgstr, task) + renderFrame(svgfile, task, task.outfile) + +def rendertask_video(task): + # iterate through the animation sequence frame by frame + # frame is a ... tbd + cache = {} + for frameNr, frame in enumerate(task.sequence(task.parameters)): + cachedRenderFrame(frame, frameNr, task, cache) + if args.only_frame: task.outfile = '{0}.frame{1:04d}.png'.format(task.outfile, args.only_frame) @@ -231,15 +241,33 @@ def rendertask(task): if r != 0: sys.exit() +def rendertask(task): + global args + # in debug mode we have no thread-worker which prints its progress + if debug: + print("generating {0} from {1}".format(task.outfile, task.infile)) + + if args.skip_frames and 'only_rerender_frames_after' not in task.parameters: + if os.path.isdir(os.path.join(task.workdir, '.frames')): + shutil.rmtree(os.path.join(task.workdir, '.frames')) + + # make sure a .frames-directory exists in out workdir + ensurePathExists(os.path.join(task.workdir, '.frames')) + + if task.is_animated(): + rendertask_video(task) + else: + rendertask_image(task) + if not debug: print("cleanup") # 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 downloadSchedule(scheduleUrl): print("downloading schedule") @@ -322,7 +350,7 @@ def events(scheduleUrl, titlemap={}): 'personnames': ', '.join(personnames), 'room': room.attrib['name'], 'track': event.find('track').text, - #'url': event.find('url').text + #'url': event.find('url').text }