Created
November 29, 2020 11:28
-
-
Save rindPHI/a4c362166d879e2acbe21897d59c01c0 to your computer and use it in GitHub Desktop.
Python script to convert an Inkscape SVG file with LaTeX Beamer-like layer names (e.g., "My layer [3,5-7,10-]") to a series of PDF files for use in, e.g., Beamer presentations, or as a step to create an animated gif.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
import xml.etree.ElementTree as ET | |
import re | |
import sys | |
import os | |
import threading | |
from pathlib import Path | |
from subprocess import call | |
########################################################################### | |
# This script extracts "layers" for usage in LaTeX presentations # | |
# from inkscape SVG files. For this, append a LaTeX overlay specification # | |
# to the label of the layer, e.g., "1,2-5,3,17-", which you surround by # | |
# either angle or square brackets. Then, call this script with the name # | |
# of the SVG file as argument (works also with a path). It will export # | |
# multiple PDF files "...-step-NNN.pdf" in the same directory as the SVG # | |
# file. Requires the inkscape binary in the path. # | |
########################################################################### | |
ns = {'svg': 'http://www.w3.org/2000/svg', | |
'inkscape': 'http://www.inkscape.org/namespaces/inkscape'} | |
class OverlayRange: | |
def __init__(self, start, end): | |
self.start = int(start) | |
self.end = int(end) | |
def __str__(self): | |
if (self.end == -1): | |
return "range(" + str(self.start) + "-" + ")" | |
else: | |
return "range(" + str(self.start) + "-" + str(self.end) + ")" | |
def max(self): | |
if (self.end > self.start): | |
return self.end | |
else: | |
return self.start | |
def setMax(self, max): | |
if (self.end == -1): | |
self.end = max | |
def visibleAt(self, step): | |
return (self.start <= step and step <= self.end) | |
@classmethod | |
def fromStartOnly(cls, start): | |
return cls(start, -1) | |
def parseOverlayRange(string): | |
arr = string.split('-') | |
if (len(arr) == 1): | |
return OverlayRange(arr[0], arr[0]) | |
elif (len(arr) == 2 and arr[1] == ''): | |
return OverlayRange.fromStartOnly(arr[0]) | |
elif (len(arr) == 2): | |
return OverlayRange(arr[0], arr[1]) | |
class Layer: | |
def __init__(self, label, element): | |
self.label = label | |
self.element = element | |
self.ranges = [] | |
def addRange(self, therange): | |
self.ranges.append(therange) | |
def maxRange(self): | |
return max(map(lambda r: r.max(), self.ranges)) | |
def visibleAt(self, step): | |
for therange in self.ranges: | |
if (therange.visibleAt(step)): | |
return True | |
return False | |
def toNS(elem, namespace): | |
return '{' + ns.get(namespace) + '}' + elem | |
if (len(sys.argv) < 2): | |
print('Expecting input SVG file as argument') | |
inputfile = sys.argv[1] | |
outprefix = inputfile.split('.svg')[0] | |
tree = ET.parse(inputfile) | |
root = tree.getroot() | |
layers = [] | |
for elem in tree.iter(): | |
if (elem.tag != toNS('g', 'svg')): | |
continue | |
label = elem.get(toNS('label', 'inkscape')) | |
if (label == None): | |
continue | |
groupmode = elem.get(toNS('groupmode', 'inkscape')) | |
if (groupmode == None or groupmode != 'layer'): | |
continue | |
regex = r"^([^(<\[)]*)\W+(?:<|\[)([0-9]+(?:-(?:[0-9]+)?)?(?:,[0-9]+(?:-(?:[0-9]+)?)?)*)(?:>|\])$" | |
m = re.search(regex, label) | |
if (m == None): | |
continue | |
layer = Layer(m.group(1), elem) | |
for theRange in m.group(2).split(','): | |
layer.addRange(parseOverlayRange(theRange)) | |
layers.append(layer) | |
maxLayer = max(map(lambda l: l.maxRange(), layers)) | |
print("Maximum overlay number: " + str(maxLayer)) | |
for layer in layers: | |
for therange in layer.ranges: | |
therange.setMax(maxLayer) | |
threads = list() | |
for i in range(1,maxLayer+1): | |
print("Animation step " + str(i)) | |
for layer in layers: | |
if (layer.visibleAt(i)): | |
print (" Layer " + layer.label + " visible") | |
layer.element.set('style', 'display:inline') | |
elif (not layer.visibleAt(i)): | |
print (" Layer " + layer.label + " hidden") | |
layer.element.set('style', 'display:none') | |
layerOutPrefix = outprefix + '-step-' + str('{:03d}'.format(i)) | |
svgoutfile = layerOutPrefix + '.svg' | |
pdfoutfile = layerOutPrefix + '.pdf' | |
if Path(svgoutfile).exists(): | |
print("Step SVG file " + svgoutfile + " already exists, won't override but cancel") | |
quit() | |
print(" Exporting layer " + str(i) + " to file " + pdfoutfile) | |
tree.write(svgoutfile) | |
t = threading.Thread(target=(lambda: call(['inkscape', '-z', '-C', '--export-pdf=' + pdfoutfile, svgoutfile]))) | |
threads.append(t) | |
t.start() | |
print("Waiting for inkscape threads to terminate...") | |
for step, thread in enumerate(threads): | |
thread.join() | |
svgoutfile = outprefix + '-step-' + str('{:03d}'.format(step+1)) + '.svg' | |
os.remove(svgoutfile) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment