diff --git a/.gitignore b/.gitignore
index 87676a5..b5f2439 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
*.mp4
*.dv
*.ts
+*.mkv
*.mov
+/*/*.png
/*.png
*.pyc
schedule.de.xml
diff --git a/cccamp19/config.ini b/cccamp19/config.ini
index 838379d..c1bbba0 100644
--- a/cccamp19/config.ini
+++ b/cccamp19/config.ini
@@ -1,4 +1,5 @@
[default]
+schedule = https://fahrplan.events.ccc.de/camp/2019/Fahrplan/schedule.xml
template = cccamp19_talks_intro_1080p.mov
[title]
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/make-ffmpeg.py b/make-ffmpeg.py
index 0b13287..6d048f3 100755
--- a/make-ffmpeg.py
+++ b/make-ffmpeg.py
@@ -9,25 +9,21 @@ import argparse
import shlex
from PIL import ImageFont
from configparser import ConfigParser
+import json
# Parse arguments
parser = argparse.ArgumentParser(
description='C3VOC Intro-Outro-Generator - Variant which renders only using video filters in ffmpeg',
- usage="./make-ffmpeg.py yourproject/ https://url/to/schedule.xml",
+ usage="./make-ffmpeg.py yourproject/",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('project', action="store", metavar='Project folder', type=str, help='''
Path to your project folder
''')
-parser.add_argument('schedule', action="store", metavar='Schedule-URL', type=str, nargs='?', help='''
- URL or Path to your schedule.xml
- ''')
-
parser.add_argument('--debug', action="store_true", default=False, help='''
Run script in debug mode and render with placeholder texts,
- not parsing or accessing a schedule. Schedule-URL can be left blank when
- used with --debug
+ not parsing or accessing a schedule.
This argument must not be used together with --id
Usage: ./make-ffmpeg.py yourproject/ --debug
''')
@@ -71,30 +67,6 @@ def error(str):
parser.print_help()
sys.exit(1)
-
-if not (os.path.exists(os.path.join(args.project, 'config.ini'))):
- error("config.ini file in Project Path is missing")
-
-if not args.project:
- error("The Project Path is a required argument")
-
-if not args.debug and not args.schedule:
- error("Either specify --debug or supply a schedule")
-
-if args.debug:
- persons = ['Thomas Roth', 'Dmitry Nedospasov', 'Josh Datko']
- events = [{
- 'id': 'debug',
- 'title': 'wallet.fail',
- 'subtitle': 'Hacking the most popular cryptocurrency hardware wallets',
- 'persons': persons,
- 'personnames': ', '.join(persons),
- 'room': 'Borg',
- }]
-
-else:
- events = list(renderlib.events(args.schedule))
-
parser = ConfigParser()
parser.read(os.path.join(os.path.dirname(args.project), 'config.ini'))
template = parser['default']['template']
@@ -131,6 +103,31 @@ font_tt = os.path.join(os.path.dirname(args.project), text_font)
fileformat = os.path.splitext(template)[1]
infile = os.path.join(os.path.dirname(args.project), template)
+schedule = parser['default']['schedule']
+
+if not (os.path.exists(os.path.join(args.project, 'config.ini'))):
+ error("config.ini file in Project Path is missing")
+
+if not args.project:
+ error("The Project Path is a required argument")
+
+if not args.debug and not schedule:
+ error("Either specify --debug or supply a schedule in config.ini")
+
+if args.debug:
+ persons = ['Thomas Roth', 'Dmitry Nedospasov', 'Josh Datko']
+ events = [{
+ 'id': 'debug',
+ 'title': 'wallet.fail',
+ 'subtitle': 'Hacking the most popular cryptocurrency hardware wallets',
+ 'persons': persons,
+ 'personnames': ', '.join(persons),
+ 'room': 'Borg',
+ }]
+
+else:
+ events = list(renderlib.events(schedule))
+
def describe_event(event):
return "#{}: {}".format(event['id'], event['title'])
@@ -202,6 +199,9 @@ def enqueue_job(event):
event_title = str(event['title'])
event_personnames = str(event['personnames'])
+ event_title = event_title.replace('"', '')
+ event_title = event_title.replace('\'', '')
+ event_personnames = event_personnames.replace('"', '')
t = fit_title(event_title)
s = fit_speaker(event_personnames)
@@ -220,7 +220,8 @@ def enqueue_job(event):
videofilter += "drawtext=enable='between(n,{0},{1})':fontfile={2}:fontsize={3}:fontcolor={4}:x={5}:y={6}:text={7}".format(text_in, text_out, font_tt, text_fontsize, text_fontcolor, text_x, text_y, text_text)
if fileformat == '.mov':
- cmd = 'ffmpeg -y -i "{0}" -vf "{1}" -vcodec prores_ks -pix_fmt yuva444p10le -profile:v 4444 -shortest -movflags faststart -f mov "{2}"'.format(infile, videofilter, outfile)
+ cmd = 'ffmpeg -y -i "{0}" -vf "{1}" -shortest -c:v qtrle -movflags faststart -f mov "{2}"'.format(infile, videofilter, outfile)
+ #cmd = 'ffmpeg -y -i "{0}" -vf "{1}" -vcodec prores_ks -pix_fmt yuva444p10le -profile:v 4444 -shortest -movflags faststart -f mov "{2}"'.format(infile, videofilter, outfile)
else:
cmd = 'ffmpeg -y -i "{0}" -vf "{1}" -map 0:0 -c:v mpeg2video -q:v 2 -aspect 16:9 -map 0:1 -c:a mp2 -b:a 384k -shortest -f mpegts "{2}"'.format(infile, videofilter, outfile)
if args.debug:
diff --git a/renderlib.py b/renderlib.py
index d29221f..0f99adf 100644
--- a/renderlib.py
+++ b/renderlib.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 expandtab
import os
@@ -7,11 +6,9 @@ import re
import glob
import shutil
import errno
-from lxml import etree
-from xml.sax.saxutils import escape as xmlescape
-import cssutils
-import logging
import subprocess
+from svgtemplate import SVGTemplate
+from lxml import etree
from urllib.request import urlopen
from wand.image import Image
@@ -20,10 +17,6 @@ fps = 25
debug = True
args = None
-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)
@@ -40,20 +33,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 +67,6 @@ class Rendertask:
else:
return None
-
# try to create all folders needed and skip, they already exist
def ensurePathExists(path):
try:
@@ -78,40 +81,24 @@ def ensureFilesRemoved(pattern):
for f in glob.glob(pattern):
os.unlink(f)
+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 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
- 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])))
-
- parser = etree.XMLParser(huge_tree=True)
- svg = etree.fromstring(svgstr.encode('utf-8'), parser)
-
- # if '$subtitle' in task.parameters and task.parameters['$subtitle'] == '':
- # child = svg.findall(".//*[@id='subtitle']")[0]
- # child.getparent().remove(child)
-
- # frame-number counter
- frameNr = 0
-
- # iterate through the animation seqence frame by frame
- # frame is a ... tbd
- cache = {}
- for frame in task.sequence(task.parameters):
+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 +122,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
+ with SVGTemplate(task) as svg:
+ svg.replacetext()
+ svg.transform(frame)
+ svgfile = svg.write()
- 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):
+ with SVGTemplate(task) as svg:
+ svg.replacetext()
+ svgfile = svg.write()
+ 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 +195,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 +304,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
}
diff --git a/suselabs18/__init__.py b/suselabs18/__init__.py
new file mode 100644
index 0000000..0b4687e
--- /dev/null
+++ b/suselabs18/__init__.py
@@ -0,0 +1,129 @@
+#!/usr/bin/python
+
+from renderlib import *
+from easing import *
+
+# URL to Schedule-XML
+scheduleUrl = 'https://live.ber.c3voc.de/releases/suselabs18/schedule.xml'
+
+def bounce(i, min, max, frames):
+ if i == frames - 1:
+ return 0
+
+ if i <= frames/2:
+ return easeInOutQuad(i, min, max, frames/2)
+ else:
+ return max - easeInOutQuad(i - frames/2, min, max, frames/2)
+
+def introFrames(parameters):
+ # 3 Sekunde Text Fadein
+ frames = 1*fps
+ for i in range(0, frames):
+ yield (
+ ('textblock', 'style', 'opacity', "%.4f" % easeLinear(i, 0, 1, frames)),
+ ('fadeout', 'style', 'opacity', "%.4f" % 0),
+ )
+
+ # 4 Sekunden stehen lassen
+ frames = 4*fps
+ for i in range(0, frames):
+ yield (
+ ('fadeout', 'style', 'opacity', "%.4f" % 0),
+ )
+
+ # 1 Sekunde Fade to black layer
+ frames = 1*fps
+ for i in range(0, frames):
+ yield (
+ ('fadeout', 'style', 'opacity', "%.4f" % easeOutCubic(i, 0, 1, frames)),
+ )
+
+def pauseFrames(parameters):
+ frames = 3*fps
+ colors = ['#21A4D4', '#73BA25', '#6DA741', '#35B9AB', '#00A489', '#173F4F']
+ yield (
+ ('pause_bg', 'style', 'fill', "%s" % '#173F4F'),
+ ('pause_bg', 'attr', 'opacity', '%.4f' % 1.0),
+ )
+ for i in range(0, len(colors)):
+ z = 0
+ for z in range(0,frames):
+ yield (
+ ('pause_bg_alt', 'style', 'fill', "%s" % colors[i]),
+ ('pause_bg_alt', 'attr', 'opacity', '%.4f' % easeLinear(z, 0.0, 1.0, frames)),
+ )
+ yield (
+ ('pause_bg', 'style', 'fill', "%s" % colors[i]),
+ ('pause_bg', 'attr', 'opacity', '%.4f' % 1.0),
+ )
+
+def outroFrames(p):
+ # 5 Sekunden stehen bleiben
+ frames = 5*fps
+ for i in range(0, frames):
+ yield []
+
+def debug():
+ render(
+ 'intro.svg',
+ '../intro.ts',
+ introFrames,
+ {
+ '$ID': 4711,
+ '$TITLE': "Long Long Long title is LONG",
+ '$SUBTITLE': 'Long Long Long Long subtitle is LONGER',
+ '$SPEAKER': 'Long Name of Dr. Dr. Prof. Dr. Long Long'
+ }
+ )
+
+ render(
+ 'pause.svg',
+ '../pause.ts',
+ pauseFrames
+ )
+
+ render(
+ 'outro.svg',
+ '../outro.ts',
+ outroFrames
+ )
+
+def tasks(queue, args, idlist, skiplist):
+ # iterate over all events extracted from the schedule xml-export
+ for event in events(scheduleUrl):
+ if not (idlist==[]):
+ if 000000 in idlist:
+ print("skipping id (%s [%s])" % (event['title'], event['id']))
+ continue
+ if int(event['id']) not in idlist:
+ print("skipping id (%s [%s])" % (event['title'], event['id']))
+ continue
+
+ # generate a task description and put it into the queue
+ 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']
+ }
+ ))
+
+ # 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
+ ))
+
+ # place the pause-sequence into the queue
+ if not "pause" in skiplist:
+ queue.put(Rendertask(
+ infile = 'pause.svg',
+ outfile = 'pause.ts',
+ sequence = pauseFrames
+ ))
diff --git a/suselabs18/artwork/intro.svg b/suselabs18/artwork/intro.svg
new file mode 100644
index 0000000..fee1795
--- /dev/null
+++ b/suselabs18/artwork/intro.svg
@@ -0,0 +1,3378 @@
+
+
+
+
\ No newline at end of file
diff --git a/suselabs18/artwork/outro.svg b/suselabs18/artwork/outro.svg
new file mode 100644
index 0000000..0be25ef
--- /dev/null
+++ b/suselabs18/artwork/outro.svg
@@ -0,0 +1,436 @@
+
+
+
+
\ No newline at end of file
diff --git a/suselabs18/artwork/pause.svg b/suselabs18/artwork/pause.svg
new file mode 100644
index 0000000..e601797
--- /dev/null
+++ b/suselabs18/artwork/pause.svg
@@ -0,0 +1,180 @@
+
+
+
+
\ No newline at end of file
diff --git a/svgtemplate.py b/svgtemplate.py
new file mode 100644
index 0000000..f6797b0
--- /dev/null
+++ b/svgtemplate.py
@@ -0,0 +1,54 @@
+# vim: tabstop=4 shiftwidth=4 expandtab
+import builtins
+import cssutils
+import logging
+import os
+from lxml import etree
+from xml.sax.saxutils import escape as xmlescape
+
+cssutils.ser.prefs.lineSeparator = ' '
+cssutils.log.setLevel(logging.FATAL)
+
+class SVGTemplate:
+ def __init__(self, task):
+ self.task = task
+
+ def __enter__(self):
+ with builtins.open(os.path.join(self.task.workdir, self.task.infile), 'r') as fp:
+ self.svgstr = fp.read()
+ return self
+
+ def write(self):
+ # open the output-file (named ".gen.svg" in the workdir)
+ outfile = os.path.join(self.task.workdir, '.gen.svg')
+ with builtins.open(outfile, 'w') as fp:
+ # write the generated svg-text into the output-file
+ fp.write(self.svgstr)
+ return outfile
+
+ def replacetext(self):
+ for key in self.task.parameters.keys():
+ self.svgstr = self.svgstr.replace(key, xmlescape(str(self.task.parameters[key])))
+
+ def transform(self, frame):
+ parser = etree.XMLParser(huge_tree=True)
+ svg = etree.fromstring(self.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)
+ self.xmlstr = etree.tostring(svg, encoding='unicode')
+
+ def __exit__(self, exception_type, exception_value, traceback):
+ pass