From 7dd51d08b694fcc1f07347221ea7845a2580f87c Mon Sep 17 00:00:00 2001 From: Manfred Stock Date: Thu, 15 Jul 2021 17:40:15 +0200 Subject: [PATCH 1/3] Don't use temporary file for SVG data when rendering with Inkscape This was already suggested in #22. In addition to potentially being a little more efficient, this also avoids the problem of files referenced from the SVG not being found, since the SVG is now rendered while in the artwork directory, so relative paths inside the SVG are still correct. Please note that the pipe functionality of Inkscape requires a relatively new version of Inkscape, i.e. the version from Debian Buster is not sufficient (the Buster backport from Debian should be though). Unfortunately, the same does not work when using ImageMagick, since it seems like they use different delegates/libraries to render the SVG based on how it is passed, i.e. when passed as file, it got rendered with Inkscape on my machine, when passing it as blob, it seemed to be some internal library or another delegate which did not seem to support the same feature set as Inkscape, which resulted in inferior output. Therefore, a temporary file is still used for ImageMagick. However, the issue of included images that could be solved for Inkscape with these changes still persists, since at least when using the Inkscape delegate, ImageMagick seems to create a temporary symbolic link in /tmp, which prevents that the Inkscape delegate finds included images. --- renderlib.py | 34 ++++++++++++++++------------------ svgtemplate.py | 9 +-------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/renderlib.py b/renderlib.py index 57c9366..0d749f1 100644 --- a/renderlib.py +++ b/renderlib.py @@ -11,6 +11,7 @@ from svgtemplate import SVGTemplate from lxml import etree from urllib.request import urlopen from wand.image import Image +from tempfile import NamedTemporaryFile # Frames per second. Increasing this renders more frames, the avconf-statements would still need modifications fps = 25 @@ -83,14 +84,18 @@ def ensureFilesRemoved(pattern): for f in glob.glob(pattern): os.unlink(f) -def renderFrame(infile, task, outfile): +def renderFrame(svg, task, outfile): width = 1920 height = 1080 + outfile = os.path.abspath(outfile) 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) + with NamedTemporaryFile(dir=task.workdir, suffix='.svg') as svgfile: + svgfile.write(svg.svgstr.encode('utf-8')) + svgfile.flush() + with Image(filename=svgfile.name) as img: + with img.convert('png') as converted: + converted.save(filename=outfile) elif args.resvg: # invoke inkscape to convert the generated svg-file into a png inside the .frames-directory cmd = 'resvg --background white --width={1} --height={2} "{4}" "{3}" 2>&1 >/dev/null'.format(task.workdir, width, height, outfile, infile) @@ -98,11 +103,10 @@ def renderFrame(infile, task, outfile): if errorReturn != '': print("resvg exited with error\n" + errorReturn) # sys.exit(42) - else: # invoke inkscape to convert the generated svg-file into a png inside the .frames-directory - cmd = 'inkscape --export-background=white --export-background-opacity=0 --export-width={1} --export-height={2} --export-filename="{3}" "{4}" --pipe 2>&1 >/dev/null'.format(task.workdir, width, height, os.path.abspath(outfile), os.path.abspath(infile)) - errorReturn = subprocess.check_output(cmd, shell=True, universal_newlines=True, stderr=subprocess.STDOUT, cwd=task.workdir) + cmd = 'inkscape --export-background=white --export-background-opacity=0 --export-width={1} --export-height={2} --export-filename="{3}" --pipe 2>&1 >/dev/null'.format(task.workdir, width, height, outfile) + errorReturn = subprocess.check_output(cmd, shell=True, universal_newlines=True, input=svg.svgstr, stderr=subprocess.STDOUT, cwd=task.workdir) if errorReturn != '': print("inkscape exited with error\n" + errorReturn) # sys.exit(42) @@ -135,26 +139,20 @@ def cachedRenderFrame(frame, frameNr, task, cache): elif not skip_rendering: cache[frame] = frameNr - svgfile = '{0}/.frames/{1:04d}.svg'.format(task.workdir, frameNr) - - with SVGTemplate(task, svgfile) as svg: + outfile = '{0}/.frames/{1:04d}.png'.format(task.workdir, frameNr) + with SVGTemplate(task) as svg: svg.replacetext() svg.transform(frame) - svg.write() - - outfile = '{0}/.frames/{1:04d}.png'.format(task.workdir, frameNr) - renderFrame(svgfile, task, outfile) + renderFrame(svg, task, outfile) # increment frame-number frameNr += 1 def rendertask_image(task): - svgfile = '{0}/image.svg'.format(task.workdir) - with SVGTemplate(task, svgfile) as svg: + with SVGTemplate(task) as svg: svg.replacetext() - svg.write() - renderFrame(svgfile, task, task.outfile) + renderFrame(svg, task, task.outfile) def rendertask_video(task): # iterate through the animation sequence frame by frame diff --git a/svgtemplate.py b/svgtemplate.py index 4391214..410934c 100644 --- a/svgtemplate.py +++ b/svgtemplate.py @@ -12,21 +12,14 @@ cssutils.ser.prefs.lineSeparator = ' ' cssutils.log.setLevel(logging.FATAL) class SVGTemplate: - def __init__(self, task, outfile): + def __init__(self, task): self.task = task - self.outfile = outfile 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) - with builtins.open(self.outfile, 'w') as fp: - # write the generated svg-text into the output-file - fp.write(self.svgstr) - def replacetext(self): for key in self.task.parameters.keys(): self.svgstr = self.svgstr.replace(key, xmlescape(str(self.task.parameters[key]))) From edf8624da4f82333a7465e85b86cd108000f690f Mon Sep 17 00:00:00 2001 From: Manfred Stock Date: Mon, 15 Nov 2021 22:20:37 +0100 Subject: [PATCH 2/3] Stringify SVG as ASCII with entity encoding It seems that when processing a SVG file passed via a pipe, Inkscape emits errors/warnings about an XML parser error if the SVG file is UTF-8 encoded. It still does seem to successfully process the SVG file and generates the expected output, however, these messages are at least confusing. Not passing the encoding parameter to etree.tostring() makes it output ASCII with non-ASCII characters encoded as XML entities, so this can then be correctly parsed again, and this does not seem to 'confuse' Inkscape. --- svgtemplate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svgtemplate.py b/svgtemplate.py index 410934c..57fea9d 100644 --- a/svgtemplate.py +++ b/svgtemplate.py @@ -42,7 +42,7 @@ class SVGTemplate: # if '$subtitle' in task.parameters and task.parameters['$subtitle'] == '': # child = svg.findall(".//*[@id='subtitle']")[0] # child.getparent().remove(child) - self.svgstr = etree.tostring(svg, encoding='unicode') + self.svgstr = etree.tostring(svg).decode('UTF-8') def __exit__(self, exception_type, exception_value, traceback): pass From af310d7e3eefd218aa7218ef5a46ab29d388b195 Mon Sep 17 00:00:00 2001 From: Manfred Stock Date: Thu, 22 Jul 2021 22:42:02 +0200 Subject: [PATCH 3/3] Add --audio-streams flag to configure number of audio streams for .ts output Since videos with translations appear to require at least as many audio streams in the intro/outro as there are audio streams in the main video, this allows to create intro/outro files for videos with more than one translation (so far, the code already created 2 streams). --- make.py | 4 ++++ renderlib.py | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/make.py b/make.py index 6c0834c..20ea5a0 100755 --- a/make.py +++ b/make.py @@ -56,6 +56,10 @@ parser.add_argument('--resvg', action="store_true", default=False, help=''' Render frames using resvg instead of Inkscape. Usage: ./make.py yourproject/ --resvg ''') +parser.add_argument('--audio-streams', action="store", default=2, type=int, help=''' + Number of audio streams to generate. + Usage: ./make.py yourproject/ --audio-streams 4 + ''') if len(sys.argv) < 2: parser.print_help() diff --git a/renderlib.py b/renderlib.py index 0d749f1..3f28b38 100644 --- a/renderlib.py +++ b/renderlib.py @@ -176,16 +176,18 @@ def rendertask_video(task): cmd = 'cd {0} && '.format(task.workdir) cmd += 'ffmpeg -f image2 -i .frames/%04d.png ' if task.audiofile is None: - cmd += '-ar 48000 -ac 1 -f s16le -i /dev/zero -ar 48000 -ac 1 -f s16le -i /dev/zero ' + audio_input = '-ar 48000 -ac 1 -f s16le -i /dev/zero ' else: - cmd += '-i {0} -i {0} '.format(task.audiofile) + audio_input = '-i {0} '.format(task.audiofile) + cmd += audio_input * args.audio_streams cmd += '-map 0:0 -c:v mpeg2video -q:v 2 -aspect 16:9 ' if task.audiofile is None: - cmd += '-map 1:0 -map 2:0 ' + audio_map = '-map {0}:0 ' else: - cmd += '-map 1:0 -c:a copy -map 2:0 -c:a copy ' + audio_map = '-map {0}:0 -c:a copy ' + cmd += ''.join(audio_map.format(index + 1) for index in range(args.audio_streams)) cmd += '-shortest -f mpegts "{0}"'.format(task.outfile) elif task.outfile.endswith('.mov'): cmd = 'cd {0} && '.format(task.workdir)