Last active
October 23, 2023 15:06
-
-
Save impiaaa/645cb4a2d4a86f74807c52ed376388ef to your computer and use it in GitHub Desktop.
FLA to SVG converter
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
from xml.dom import minidom | |
import sys | |
from xml.sax.saxutils import quoteattr, escape | |
import math, numpy | |
class Scanner: | |
def __init__(self, data): | |
self.data = data | |
self.pos = 0 | |
def scanChar(self): | |
c = self.data[self.pos] | |
self.pos += 1 | |
return c | |
def scanDecimal(self): | |
start = self.pos | |
end = start | |
while True: | |
if end >= len(self.data): | |
break | |
if not self.data[end].isdigit() and self.data[end] != '.' and self.data[end] != '-': | |
break | |
end += 1 | |
if start == end: | |
return None | |
else: | |
self.pos = end | |
return float(self.data[start:end]) | |
def hasData(self): | |
return self.pos < len(self.data) | |
def scanHex(self): | |
start = self.pos | |
if self.data[start] != '#': | |
return None | |
start += 1 | |
end = start | |
while self.data[end] != '.': end += 1 | |
wholePart = int(self.data[start:end], 16) | |
sign = -1 if wholePart&0x80000000 else 1 | |
if sign == -1: wholePart = (~wholePart)+1 | |
start = end+1 | |
end = start | |
while end < len(self.data) and self.data[end] in '0123456789ABCDEFabcdef': end += 1 | |
fractionalPart = int(self.data[start:end], 16) | |
self.pos = end | |
return sign*(wholePart+(fractionalPart/(16**(end-start)))) | |
def scanNumber(self): | |
self.skipWhitespace() | |
if self.data[self.pos] == '#': | |
return self.scanHex() | |
else: | |
return self.scanDecimal() | |
def skipWhitespace(self): | |
while self.pos < len(self.data) and self.data[self.pos].isspace(): self.pos += 1 | |
class Edge: pass | |
class PathCommand: | |
def __init__(self, cmd, x, y, x1=None, y1=None): | |
self.cmd = cmd | |
self.x = x | |
self.y = y | |
self.x1 = x1 | |
self.y1 = y1 | |
def __str__(self): | |
if self.cmd == 'Q': | |
return "{0} {1},{2} {3},{4}".format(self.cmd, self.x1, self.y1, self.x, self.y) | |
else: | |
return "{0} {1},{2}".format(self.cmd, self.x, self.y) | |
def matrixToTransform(el): | |
if 'a' in el.attributes and 'd' in el.attributes: | |
if 'b' not in el.attributes: el.attributes['b'] = '0' | |
if 'c' not in el.attributes: el.attributes['c'] = '0' | |
return 'matrix({0[a].value},{0[b].value},{0[c].value},{0[d].value},{0[tx].value},{0[ty].value})'.format(el.attributes) | |
if 'tx' in el.attributes and 'ty' in el.attributes: | |
return 'translate({0[tx].value},{0[ty].value})'.format(el.attributes) | |
def reversePath(path): | |
last = None | |
for command in reversed(path): | |
if last is None: | |
yield PathCommand('M', command.x, command.y) | |
else: | |
yield PathCommand(last.cmd, command.x, command.y, last.x1, last.y1) | |
last = command | |
def distance(x1, y1, x2, y2): | |
return math.sqrt((x1-x2)**2 + (y1-y2)**2) | |
def handleGradient(fout, style, nodeId, osbPaint=None): | |
if style.tagName.startswith('RadialGradient'): | |
fout.write('<radialGradient') | |
elif style.tagName.startswith('LinearGradient'): | |
fout.write('<linearGradient') | |
else: | |
raise Exception("Unknown gradient type {0}".format(style.tagName)) | |
fout.write(' id="{0}"'.format(nodeId)) | |
matrix = style.getElementsByTagName('matrix') | |
if len(matrix) > 0: | |
Matrix = matrix[0].getElementsByTagName('Matrix')[0] | |
fout.write(' gradientUnits="userSpaceOnUse"') | |
if 1: | |
fout.write(' gradientTransform="{0}"'.format(matrixToTransform(Matrix))) | |
if style.tagName.startswith('LinearGradient'): | |
fout.write(' x1="0" y1="0"') | |
fout.write(' x2="100%" y2="0"') | |
else: | |
fout.write(' cx="0" cy="0"') | |
fout.write(' r="100%"') | |
else: | |
a = float(Matrix.attributes['a'].value) if 'a' in Matrix.attributes else 0 | |
b = float(Matrix.attributes['b'].value) if 'b' in Matrix.attributes else 0 | |
c = float(Matrix.attributes['c'].value) if 'c' in Matrix.attributes else 0 | |
d = float(Matrix.attributes['d'].value) if 'd' in Matrix.attributes else 0 | |
tx = float(Matrix.attributes['tx'].value) if 'tx' in Matrix.attributes else 0 | |
ty = float(Matrix.attributes['ty'].value) if 'ty' in Matrix.attributes else 0 | |
mat = [[a*1000, b*1000, tx], [c*1000, d*1000, ty], [0, 0, 1]] | |
x1, y1, w = numpy.matmul(mat, [0, 0, 1]) | |
x2, y2, w = numpy.matmul(mat, [1, 0, 1]) | |
if style.tagName.startswith('LinearGradient'): | |
fout.write(' x1="{0}" y1="{1}"'.format(x1, y1)) | |
fout.write(' x2="{0}" y2="{1}"'.format(x2, y2)) | |
else: | |
r = distance(x1, y1, x2, y2) | |
fout.write(' cx="{0}" cy="{1}"'.format(x1, y1)) | |
fout.write(' r="{1}"'.format(r)) | |
if osbPaint is not None: | |
fout.write(' osb:paint="{0}"'.format(osbPaint)) | |
fout.write('>\n') | |
for GradientEntry in style.getElementsByTagName('GradientEntry'): | |
fout.write('<stop offset="{0}" style="'.format(GradientEntry.attributes['ratio'].value)) | |
if 'color' in GradientEntry.attributes: fout.write('stop-color:{0};'.format(GradientEntry.attributes['color'].value)) | |
if 'alpha' in GradientEntry.attributes: fout.write('stop-opacity:{0};'.format(GradientEntry.attributes['alpha'].value)) | |
fout.write('" />\n') | |
if style.tagName.startswith('RadialGradient'): | |
fout.write('</radialGradient>\n') | |
elif style.tagName.startswith('LinearGradient'): | |
fout.write('</linearGradient>\n') | |
def handleFillStyle(fout, style, fillType): | |
global gradientId | |
if style.tagName == 'SolidColor': | |
if 'color' in style.attributes: return fillType+':'+style.attributes['color'].value+';' | |
else: return 'fill:none;' | |
elif style.tagName == 'RadialGradient' or style.tagName == 'LinearGradient': | |
#handleGradient(fout, style, 'gradient{0}'.format(gradientId)) | |
#result = fillType+':url(#gradient{0});'.format(gradientId) | |
#gradientId += 1 | |
#return result | |
return fillType+':#0000FF;' | |
else: | |
print("can't handle", style.tagName, fillType, "style type") | |
return '' | |
def handleFrame(fout, DOMFrame): | |
#fout.write('<g>\n') | |
for element in DOMFrame.getElementsByTagName('elements')[0].childNodes: | |
if element.nodeType == doc.TEXT_NODE: continue | |
if element.tagName == 'DOMShape': | |
fout.write('<g transform="translate(0, {0})">\n'.format(gridPosY)) | |
fillStyles = {} | |
fills = element.getElementsByTagName('fills') | |
if len(fills) > 0: | |
for FillStyle in fills[0].childNodes: | |
if FillStyle.nodeType == doc.TEXT_NODE: continue | |
assert FillStyle.tagName == 'FillStyle' | |
styleIndex = FillStyle.attributes['index'].value | |
style = [node for node in FillStyle.childNodes if node.nodeType != doc.TEXT_NODE][0] | |
fillStyles[styleIndex] = handleFillStyle(fout, style, 'fill') | |
strokeStyles = {} | |
strokes = element.getElementsByTagName('strokes') | |
if len(strokes) > 0: | |
for StrokeStyle in strokes[0].childNodes: | |
if StrokeStyle.nodeType == doc.TEXT_NODE: continue | |
assert StrokeStyle.tagName == 'StrokeStyle' | |
styleIndex = StrokeStyle.attributes['index'].value | |
SolidStroke = [node for node in StrokeStyle.childNodes if node.nodeType != doc.TEXT_NODE][0] | |
assert SolidStroke.tagName == 'SolidStroke' | |
strokeStyleStr = 'stroke-width:'+SolidStroke.attributes['weight'].value+';' | |
if 'sharpCorners' in SolidStroke.attributes: strokeStyleStr += 'stroke-linejoin:'+{'true': 'miter', 'false': 'round'}[SolidStroke.attributes['sharpCorners'].value]+';' | |
fillStyle = [node for node in SolidStroke.getElementsByTagName('fill')[0].childNodes if node.nodeType != doc.TEXT_NODE][0] | |
strokeStyleStr += handleFillStyle(fout, fillStyle, 'stroke') | |
strokeStyles[styleIndex] = strokeStyleStr | |
edges = [] | |
for EdgeEl in element.getElementsByTagName('edges')[0].childNodes: | |
if EdgeEl.nodeType == doc.TEXT_NODE: continue | |
assert EdgeEl.tagName == 'Edge' | |
if 'edges' not in EdgeEl.attributes: continue | |
s = Scanner(EdgeEl.attributes["edges"].value) | |
edge = Edge() | |
if "fillStyle0" in EdgeEl.attributes: | |
edge.fillStyle0 = EdgeEl.attributes["fillStyle0"].value | |
else: | |
edge.fillStyle0 = None | |
if "fillStyle1" in EdgeEl.attributes: | |
edge.fillStyle1 = EdgeEl.attributes["fillStyle1"].value | |
else: | |
edge.fillStyle1 = None | |
if "strokeStyle" in EdgeEl.attributes: | |
edge.strokeStyle = EdgeEl.attributes["strokeStyle"].value | |
else: | |
edge.strokeStyle = None | |
edge.d = [] | |
x = y = None | |
clockwise = 0.0 | |
while s.hasData(): | |
command = s.scanChar() | |
if command == "!": | |
# move to | |
newX, newY = s.scanNumber()/20, s.scanNumber()/20 | |
if x is None and y is None: | |
firstX, firstY = newX, newY | |
else: | |
clockwise += (newX-x)*(newY+y) | |
if x != newX or y != newY: | |
x, y = newX, newY | |
edge.d.append(PathCommand('M', x, y)) | |
elif command == "|": | |
# line to | |
newX, newY = s.scanNumber()/20, s.scanNumber()/20 | |
clockwise += (newX-x)*(newY+y) | |
x, y = newX, newY | |
edge.d.append(PathCommand('L', x, y)) | |
elif command == "[": | |
# quadratic spline | |
cx, cy = s.scanNumber()/20, s.scanNumber()/20 | |
newX, newY = s.scanNumber()/20, s.scanNumber()/20 | |
clockwise += (newX-x)*(newY+y) | |
x, y = newX, newY | |
edge.d.append(PathCommand('Q', x, y, cx, cy)) | |
elif not command.isspace(): | |
print("unknown command", command, EdgeEl.toxml()) | |
#clockwise += (firstX-x)*(firstY+y) | |
edge.clockwise = clockwise > 0 | |
#edge.d.append('z') | |
edges.append(edge) | |
for styleIndex, fillStyle in fillStyles.items(): | |
fout.write('<path style="fill-rule:evenodd;') | |
fout.write(fillStyle) | |
fout.write('" d="') | |
x = y = None | |
startX = startY = None | |
fillEdges = edges[:] | |
while len(fillEdges) > 0: | |
edge = fillEdges.pop(0) | |
if edge.fillStyle0 == edge.fillStyle1: | |
# pure stroke | |
continue | |
if edge.fillStyle0 == styleIndex: | |
path = edge.d | |
elif edge.fillStyle1 == styleIndex: | |
path = list(reversePath(edge.d)) | |
if edge.fillStyle0 == styleIndex or edge.fillStyle1 == styleIndex: | |
if startX is None: startX = path[0].x | |
if startY is None: startY = path[0].y | |
if path[0].x == x and path[0].y == y: path = path[1:] | |
fout.write(' '.join(map(str, path))+' ') | |
x, y = path[-1].x, path[-1].y | |
fillEdges.sort(key=lambda edge2: distance(edge2.d[0].x, edge2.d[0].y, x, y) if edge2.fillStyle0 == styleIndex else distance(edge2.d[-1].x, edge2.d[-1].y, x, y)) | |
if x == startX and y == startY: fout.write('z') | |
fout.write('" />\n') | |
for styleIndex, strokeStyle in strokeStyles.items(): | |
fout.write('<path style="fill:none;') | |
fout.write(strokeStyle) | |
fout.write('" d="') | |
x = y = None | |
startX = startY = None | |
fillEdges = edges[:] | |
while len(fillEdges) > 0: | |
edge = fillEdges.pop(0) | |
if edge.strokeStyle == styleIndex: | |
path = edge.d | |
if startX is None: startX = path[0].x | |
if startY is None: startY = path[0].y | |
if path[0].x == x and path[0].y == y: path = path[1:] | |
fout.write(' '.join(map(str, path))+' ') | |
x, y = path[-1].x, path[-1].y | |
fillEdges.sort(key=lambda edge2: distance(edge2.d[0].x, edge2.d[0].y, x, y)) | |
if x == startX and y == startY: fout.write('z') | |
fout.write('" />\n') | |
fout.write('</g>\n') | |
elif element.tagName == 'DOMStaticText': | |
fout.write('<text transform="{0}">\n'.format(matrixToTransform(element.getElementsByTagName("matrix")[0].getElementsByTagName("Matrix")[0]))) | |
for DOMTextRun in element.getElementsByTagName('textRuns')[0].childNodes: | |
if DOMTextRun.nodeType == doc.TEXT_NODE: continue | |
assert DOMTextRun.tagName == 'DOMTextRun' | |
fout.write('<tspan style="') | |
textAttrs = DOMTextRun.getElementsByTagName('textAttrs')[0] | |
DOMTextAttrs = textAttrs.getElementsByTagName('DOMTextAttrs')[0] | |
for textAttrName, textAttrValue in DOMTextAttrs.attributes.items(): | |
if textAttrName == 'alignment': | |
if textAttrValue == 'center': | |
fout.write('text-anchor:middle;text-align:center;') | |
elif textAttrValue == 'left': # TODO: bidi | |
fout.write('text-anchor:start;text-align:start;') | |
elif textAttrValue == 'right': | |
fout.write('text-anchor:end;text-align:end;') | |
elif textAttrName == 'size': fout.write('font-size:{0};'.format(textAttrValue)) | |
elif textAttrName == 'face': fout.write('font-family:{0};'.format(textAttrValue)) | |
elif textAttrName == 'fillColor': fout.write('fill:{0};'.format(textAttrValue)) | |
fout.write('">') | |
fout.write(escape(DOMTextRun.getElementsByTagName('characters')[0].firstChild.data)) | |
fout.write('</tspan>\n') | |
fout.write('</text>\n') | |
else: | |
print("can't handle", element.tagName, "element") | |
#fout.write('</g>\n') | |
fout = open(sys.argv[1]+".svg", 'w') | |
doc = minidom.parse(sys.argv[1]) | |
head = doc.firstChild | |
if head.tagName == 'DOMDocument': | |
extendedSwatchLists = head.getElementsByTagName('extendedSwatchLists')[0].childNodes | |
swatchLists = head.getElementsByTagName('swatchLists')[0].childNodes | |
timelines = head.getElementsByTagName('timelines')[0] | |
fout.write('<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" width="{0[width].value}" height="{0[height].value}" version="1.1">\n'.format(head.attributes)) | |
fout.write('<sodipodi:namedview pagecolor="{0[backgroundColor].value}" inkscape:snap-bbox="{0[objectsSnapTo].value}" />\n'.format(head.attributes)) | |
elif head.tagName == 'DOMSymbolItem': | |
extendedSwatchLists = [] | |
swatchLists = [] | |
timelines = head.getElementsByTagName('timeline')[0] | |
fout.write('<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" version="1.1">\n'.format(head.attributes)) | |
swatchId = 0 | |
fout.write('<defs>\n') | |
for swatchList in swatchLists: | |
if swatchList.nodeType == doc.TEXT_NODE: continue | |
assert swatchList.tagName == 'swatchList' | |
for swatches in swatchList.childNodes: | |
if swatches.nodeType == doc.TEXT_NODE: continue | |
assert swatches.tagName == 'swatches' | |
for swatchItem in swatches.childNodes: | |
if swatchItem.nodeType == doc.TEXT_NODE: continue | |
assert swatchItem.tagName == 'SolidSwatchItem' | |
fout.write('<linearGradient osb:paint="solid" id="swatch{0}">\n'.format(swatchId)) | |
if 'color' in swatchItem.attributes: | |
fout.write('<stop offset="0" style="stop-color:{0}" />\n'.format(swatchItem.attributes['color'].value)) | |
fout.write('</linearGradient>\n') | |
swatchId += 1 | |
for swatchList in extendedSwatchLists: | |
if swatchList.nodeType == doc.TEXT_NODE: continue | |
assert swatchList.tagName == 'swatchList' | |
for swatches in swatchList.childNodes: | |
if swatches.nodeType == doc.TEXT_NODE: continue | |
assert swatches.tagName == 'swatches' | |
for swatchItem in swatches.childNodes: | |
if swatchItem.nodeType == doc.TEXT_NODE: continue | |
handleGradient(fout, swatchItem, 'swatch{0}'.format(swatchId), "gradient") | |
swatchId += 1 | |
fout.write('</defs>\n') | |
timelineId = 0 | |
frameId = 0 | |
layerId = 0 | |
gradientId = 0 | |
for DOMTimeline in timelines.childNodes: | |
if DOMTimeline.nodeType == doc.TEXT_NODE: continue | |
assert DOMTimeline.tagName == 'DOMTimeline' | |
#fout.write('<g id="timeline{0}" inkscape:label={1} inkscape:groupmode="layer">\n'.format(timelineId, quoteattr(DOMTimeline.attributes['name'].value))) | |
layers = timelines.getElementsByTagName('layers')[0] | |
frameNames = None | |
for DOMLayer in layers.childNodes: | |
if DOMLayer.nodeType == doc.TEXT_NODE: continue | |
assert DOMLayer.tagName == 'DOMLayer' | |
fout.write('<g id="layer{0}" inkscape:label={1} inkscape:groupmode="layer">\n'.format(layerId, quoteattr(DOMLayer.attributes['name'].value))) | |
frames = DOMLayer.getElementsByTagName('frames')[0] | |
gridPosX = 0 | |
if frameNames is not None: | |
frameIter = iter(frames.childNodes) | |
for (startIndex, endIndex), name in frameNames: | |
fout.write('<g id="frame{0}" inkscape:label={1} transform="translate({2}, 0)">\n'.format(frameId, quoteattr(name), gridPosX)) | |
gridPosX += 100 | |
gridPosY = 0 | |
for i in range(startIndex, endIndex): | |
while True: | |
DOMFrame = next(frameIter) | |
if DOMFrame.nodeType != doc.TEXT_NODE: break | |
assert DOMFrame.tagName == 'DOMFrame' | |
assert i == int(DOMFrame.attributes['index'].value) | |
handleFrame(fout, DOMFrame) | |
gridPosY += 100 | |
fout.write('</g>\n') | |
frameId += 1 | |
else: | |
if DOMLayer.attributes['name'].value == "Label": | |
frameNames = [] | |
frameNameKeys = [] | |
for DOMFrame in frames.childNodes: | |
if DOMFrame.nodeType == doc.TEXT_NODE: continue | |
assert DOMFrame.tagName == 'DOMFrame' | |
if 'name' in DOMFrame.attributes: | |
name = DOMFrame.attributes['name'].value | |
else: | |
name = None | |
if DOMLayer.attributes['name'].value == "Label": | |
startIndex = int(DOMFrame.attributes['index'].value) | |
if 'duration' in DOMFrame.attributes: | |
endIndex = startIndex+int(DOMFrame.attributes['duration'].value) | |
else: | |
endIndex = startIndex+1 | |
frameNames.append(((startIndex, endIndex), name)) | |
frameNameKeys.append(startIndex) | |
fout.write('<g id="frame{0}"'.format(frameId)) | |
if name is not None: | |
fout.write(' inkscape:label={0}'.format(quoteattr(name))) | |
fout.write(' transform="translate({0}, 0)">\n'.format(gridPosX)) | |
gridPosX += 100 | |
handleFrame(fout, DOMFrame) | |
fout.write('</g>\n') | |
frameId += 1 | |
#break | |
fout.write('</g>\n') | |
layerId += 1 | |
#fout.write('</g>\n') | |
timelineId += 1 | |
fout.write('</svg>') | |
fout.close() | |
from xml.dom import minidom | |
import sys | |
from xml.sax.saxutils import quoteattr, escape | |
import math, numpy | |
class Scanner: | |
def __init__(self, data): | |
self.data = data | |
self.pos = 0 | |
def scanChar(self): | |
c = self.data[self.pos] | |
self.pos += 1 | |
return c | |
def scanDecimal(self): | |
start = self.pos | |
end = start | |
while True: | |
if end >= len(self.data): | |
break | |
if not self.data[end].isdigit() and self.data[end] != '.' and self.data[end] != '-': | |
break | |
end += 1 | |
if start == end: | |
return None | |
else: | |
self.pos = end | |
return float(self.data[start:end]) | |
def hasData(self): | |
return self.pos < len(self.data) | |
def scanHex(self): | |
start = self.pos | |
if self.data[start] != '#': | |
return None | |
start += 1 | |
end = start | |
while self.data[end] != '.': end += 1 | |
wholePart = int(self.data[start:end], 16) | |
sign = -1 if wholePart&0x80000000 else 1 | |
if sign == -1: wholePart = (~wholePart)+1 | |
start = end+1 | |
end = start | |
while end < len(self.data) and self.data[end] in '0123456789ABCDEFabcdef': end += 1 | |
fractionalPart = int(self.data[start:end], 16) | |
self.pos = end | |
return sign*(wholePart+(fractionalPart/(16**(end-start)))) | |
def scanNumber(self): | |
self.skipWhitespace() | |
if self.data[self.pos] == '#': | |
return self.scanHex() | |
else: | |
return self.scanDecimal() | |
def skipWhitespace(self): | |
while self.pos < len(self.data) and self.data[self.pos].isspace(): self.pos += 1 | |
class Edge: pass | |
class PathCommand: | |
def __init__(self, cmd, x, y, x1=None, y1=None): | |
self.cmd = cmd | |
self.x = x | |
self.y = y | |
self.x1 = x1 | |
self.y1 = y1 | |
def __str__(self): | |
if self.cmd == 'Q': | |
return "{0} {1},{2} {3},{4}".format(self.cmd, self.x1, self.y1, self.x, self.y) | |
else: | |
return "{0} {1},{2}".format(self.cmd, self.x, self.y) | |
def matrixToTransform(el): | |
if 'a' in el.attributes and 'd' in el.attributes: | |
if 'b' not in el.attributes: el.attributes['b'] = '0' | |
if 'c' not in el.attributes: el.attributes['c'] = '0' | |
return 'matrix({0[a].value},{0[b].value},{0[c].value},{0[d].value},{0[tx].value},{0[ty].value})'.format(el.attributes) | |
if 'tx' in el.attributes and 'ty' in el.attributes: | |
return 'translate({0[tx].value},{0[ty].value})'.format(el.attributes) | |
def reversePath(path): | |
last = None | |
for command in reversed(path): | |
if last is None: | |
yield PathCommand('M', command.x, command.y) | |
else: | |
yield PathCommand(last.cmd, command.x, command.y, last.x1, last.y1) | |
last = command | |
def distance(x1, y1, x2, y2): | |
return math.sqrt((x1-x2)**2 + (y1-y2)**2) | |
def handleGradient(fout, style, nodeId, osbPaint=None): | |
if style.tagName.startswith('RadialGradient'): | |
fout.write('<radialGradient') | |
elif style.tagName.startswith('LinearGradient'): | |
fout.write('<linearGradient') | |
else: | |
raise Exception("Unknown gradient type {0}".format(style.tagName)) | |
fout.write(' id="{0}"'.format(nodeId)) | |
matrix = style.getElementsByTagName('matrix') | |
if len(matrix) > 0: | |
Matrix = matrix[0].getElementsByTagName('Matrix')[0] | |
fout.write(' gradientUnits="userSpaceOnUse"') | |
if 1: | |
fout.write(' gradientTransform="{0}"'.format(matrixToTransform(Matrix))) | |
if style.tagName.startswith('LinearGradient'): | |
fout.write(' x1="0" y1="0"') | |
fout.write(' x2="100%" y2="0"') | |
else: | |
fout.write(' cx="0" cy="0"') | |
fout.write(' r="100%"') | |
else: | |
a = float(Matrix.attributes['a'].value) if 'a' in Matrix.attributes else 0 | |
b = float(Matrix.attributes['b'].value) if 'b' in Matrix.attributes else 0 | |
c = float(Matrix.attributes['c'].value) if 'c' in Matrix.attributes else 0 | |
d = float(Matrix.attributes['d'].value) if 'd' in Matrix.attributes else 0 | |
tx = float(Matrix.attributes['tx'].value) if 'tx' in Matrix.attributes else 0 | |
ty = float(Matrix.attributes['ty'].value) if 'ty' in Matrix.attributes else 0 | |
mat = [[a*1000, b*1000, tx], [c*1000, d*1000, ty], [0, 0, 1]] | |
x1, y1, w = numpy.matmul(mat, [0, 0, 1]) | |
x2, y2, w = numpy.matmul(mat, [1, 0, 1]) | |
if style.tagName.startswith('LinearGradient'): | |
fout.write(' x1="{0}" y1="{1}"'.format(x1, y1)) | |
fout.write(' x2="{0}" y2="{1}"'.format(x2, y2)) | |
else: | |
r = distance(x1, y1, x2, y2) | |
fout.write(' cx="{0}" cy="{1}"'.format(x1, y1)) | |
fout.write(' r="{1}"'.format(r)) | |
if osbPaint is not None: | |
fout.write(' osb:paint="{0}"'.format(osbPaint)) | |
fout.write('>\n') | |
for GradientEntry in style.getElementsByTagName('GradientEntry'): | |
fout.write('<stop offset="{0}" style="'.format(GradientEntry.attributes['ratio'].value)) | |
if 'color' in GradientEntry.attributes: fout.write('stop-color:{0};'.format(GradientEntry.attributes['color'].value)) | |
if 'alpha' in GradientEntry.attributes: fout.write('stop-opacity:{0};'.format(GradientEntry.attributes['alpha'].value)) | |
fout.write('" />\n') | |
if style.tagName.startswith('RadialGradient'): | |
fout.write('</radialGradient>\n') | |
elif style.tagName.startswith('LinearGradient'): | |
fout.write('</linearGradient>\n') | |
def handleFillStyle(fout, style, fillType): | |
global gradientId | |
if style.tagName == 'SolidColor': | |
if 'color' in style.attributes: return fillType+':'+style.attributes['color'].value+';' | |
else: return 'fill:none;' | |
elif style.tagName == 'RadialGradient' or style.tagName == 'LinearGradient': | |
#handleGradient(fout, style, 'gradient{0}'.format(gradientId)) | |
#result = fillType+':url(#gradient{0});'.format(gradientId) | |
#gradientId += 1 | |
#return result | |
return fillType+':#0000FF;' | |
else: | |
print("can't handle", style.tagName, fillType, "style type") | |
return '' | |
def handleFrame(fout, DOMFrame): | |
#fout.write('<g>\n') | |
for element in DOMFrame.getElementsByTagName('elements')[0].childNodes: | |
if element.nodeType == doc.TEXT_NODE: continue | |
if element.tagName == 'DOMShape': | |
fout.write('<g transform="translate(0, {0})">\n'.format(gridPosY)) | |
fillStyles = {} | |
fills = element.getElementsByTagName('fills') | |
if len(fills) > 0: | |
for FillStyle in fills[0].childNodes: | |
if FillStyle.nodeType == doc.TEXT_NODE: continue | |
assert FillStyle.tagName == 'FillStyle' | |
styleIndex = FillStyle.attributes['index'].value | |
style = [node for node in FillStyle.childNodes if node.nodeType != doc.TEXT_NODE][0] | |
fillStyles[styleIndex] = handleFillStyle(fout, style, 'fill') | |
strokeStyles = {} | |
strokes = element.getElementsByTagName('strokes') | |
if len(strokes) > 0: | |
for StrokeStyle in strokes[0].childNodes: | |
if StrokeStyle.nodeType == doc.TEXT_NODE: continue | |
assert StrokeStyle.tagName == 'StrokeStyle' | |
styleIndex = StrokeStyle.attributes['index'].value | |
SolidStroke = [node for node in StrokeStyle.childNodes if node.nodeType != doc.TEXT_NODE][0] | |
assert SolidStroke.tagName == 'SolidStroke' | |
strokeStyleStr = 'stroke-width:'+SolidStroke.attributes['weight'].value+';' | |
if 'sharpCorners' in SolidStroke.attributes: strokeStyleStr += 'stroke-linejoin:'+{'true': 'miter', 'false': 'round'}[SolidStroke.attributes['sharpCorners'].value]+';' | |
fillStyle = [node for node in SolidStroke.getElementsByTagName('fill')[0].childNodes if node.nodeType != doc.TEXT_NODE][0] | |
strokeStyleStr += handleFillStyle(fout, fillStyle, 'stroke') | |
strokeStyles[styleIndex] = strokeStyleStr | |
edges = [] | |
for EdgeEl in element.getElementsByTagName('edges')[0].childNodes: | |
if EdgeEl.nodeType == doc.TEXT_NODE: continue | |
assert EdgeEl.tagName == 'Edge' | |
if 'edges' not in EdgeEl.attributes: continue | |
s = Scanner(EdgeEl.attributes["edges"].value) | |
edge = Edge() | |
if "fillStyle0" in EdgeEl.attributes: | |
edge.fillStyle0 = EdgeEl.attributes["fillStyle0"].value | |
else: | |
edge.fillStyle0 = None | |
if "fillStyle1" in EdgeEl.attributes: | |
edge.fillStyle1 = EdgeEl.attributes["fillStyle1"].value | |
else: | |
edge.fillStyle1 = None | |
if "strokeStyle" in EdgeEl.attributes: | |
edge.strokeStyle = EdgeEl.attributes["strokeStyle"].value | |
else: | |
edge.strokeStyle = None | |
edge.d = [] | |
x = y = None | |
clockwise = 0.0 | |
while s.hasData(): | |
command = s.scanChar() | |
if command == "!": | |
# move to | |
newX, newY = s.scanNumber()/20, s.scanNumber()/20 | |
if x is None and y is None: | |
firstX, firstY = newX, newY | |
else: | |
clockwise += (newX-x)*(newY+y) | |
if x != newX or y != newY: | |
x, y = newX, newY | |
edge.d.append(PathCommand('M', x, y)) | |
elif command == "|": | |
# line to | |
newX, newY = s.scanNumber()/20, s.scanNumber()/20 | |
clockwise += (newX-x)*(newY+y) | |
x, y = newX, newY | |
edge.d.append(PathCommand('L', x, y)) | |
elif command == "[": | |
# quadratic spline | |
cx, cy = s.scanNumber()/20, s.scanNumber()/20 | |
newX, newY = s.scanNumber()/20, s.scanNumber()/20 | |
clockwise += (newX-x)*(newY+y) | |
x, y = newX, newY | |
edge.d.append(PathCommand('Q', x, y, cx, cy)) | |
elif not command.isspace(): | |
print("unknown command", command, EdgeEl.toxml()) | |
#clockwise += (firstX-x)*(firstY+y) | |
edge.clockwise = clockwise > 0 | |
#edge.d.append('z') | |
edges.append(edge) | |
for styleIndex, fillStyle in fillStyles.items(): | |
fout.write('<path style="fill-rule:evenodd;') | |
fout.write(fillStyle) | |
fout.write('" d="') | |
x = y = None | |
startX = startY = None | |
fillEdges = edges[:] | |
while len(fillEdges) > 0: | |
edge = fillEdges.pop(0) | |
if edge.fillStyle0 == edge.fillStyle1: | |
# pure stroke | |
continue | |
if edge.fillStyle0 == styleIndex: | |
path = edge.d | |
elif edge.fillStyle1 == styleIndex: | |
path = list(reversePath(edge.d)) | |
if edge.fillStyle0 == styleIndex or edge.fillStyle1 == styleIndex: | |
if startX is None: startX = path[0].x | |
if startY is None: startY = path[0].y | |
if path[0].x == x and path[0].y == y: path = path[1:] | |
fout.write(' '.join(map(str, path))+' ') | |
x, y = path[-1].x, path[-1].y | |
fillEdges.sort(key=lambda edge2: distance(edge2.d[0].x, edge2.d[0].y, x, y) if edge2.fillStyle0 == styleIndex else distance(edge2.d[-1].x, edge2.d[-1].y, x, y)) | |
if x == startX and y == startY: fout.write('z') | |
fout.write('" />\n') | |
for styleIndex, strokeStyle in strokeStyles.items(): | |
fout.write('<path style="fill:none;') | |
fout.write(strokeStyle) | |
fout.write('" d="') | |
x = y = None | |
startX = startY = None | |
fillEdges = edges[:] | |
while len(fillEdges) > 0: | |
edge = fillEdges.pop(0) | |
if edge.strokeStyle == styleIndex: | |
path = edge.d | |
if startX is None: startX = path[0].x | |
if startY is None: startY = path[0].y | |
if path[0].x == x and path[0].y == y: path = path[1:] | |
fout.write(' '.join(map(str, path))+' ') | |
x, y = path[-1].x, path[-1].y | |
fillEdges.sort(key=lambda edge2: distance(edge2.d[0].x, edge2.d[0].y, x, y)) | |
if x == startX and y == startY: fout.write('z') | |
fout.write('" />\n') | |
fout.write('</g>\n') | |
elif element.tagName == 'DOMStaticText': | |
fout.write('<text transform="{0}">\n'.format(matrixToTransform(element.getElementsByTagName("matrix")[0].getElementsByTagName("Matrix")[0]))) | |
for DOMTextRun in element.getElementsByTagName('textRuns')[0].childNodes: | |
if DOMTextRun.nodeType == doc.TEXT_NODE: continue | |
assert DOMTextRun.tagName == 'DOMTextRun' | |
fout.write('<tspan style="') | |
textAttrs = DOMTextRun.getElementsByTagName('textAttrs')[0] | |
DOMTextAttrs = textAttrs.getElementsByTagName('DOMTextAttrs')[0] | |
for textAttrName, textAttrValue in DOMTextAttrs.attributes.items(): | |
if textAttrName == 'alignment': | |
if textAttrValue == 'center': | |
fout.write('text-anchor:middle;text-align:center;') | |
elif textAttrValue == 'left': # TODO: bidi | |
fout.write('text-anchor:start;text-align:start;') | |
elif textAttrValue == 'right': | |
fout.write('text-anchor:end;text-align:end;') | |
elif textAttrName == 'size': fout.write('font-size:{0};'.format(textAttrValue)) | |
elif textAttrName == 'face': fout.write('font-family:{0};'.format(textAttrValue)) | |
elif textAttrName == 'fillColor': fout.write('fill:{0};'.format(textAttrValue)) | |
fout.write('">') | |
fout.write(escape(DOMTextRun.getElementsByTagName('characters')[0].firstChild.data)) | |
fout.write('</tspan>\n') | |
fout.write('</text>\n') | |
else: | |
print("can't handle", element.tagName, "element") | |
#fout.write('</g>\n') | |
fout = open(sys.argv[1]+".svg", 'w') | |
doc = minidom.parse(sys.argv[1]) | |
head = doc.firstChild | |
if head.tagName == 'DOMDocument': | |
extendedSwatchLists = head.getElementsByTagName('extendedSwatchLists')[0].childNodes | |
swatchLists = head.getElementsByTagName('swatchLists')[0].childNodes | |
timelines = head.getElementsByTagName('timelines')[0] | |
fout.write('<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" width="{0[width].value}" height="{0[height].value}" version="1.1">\n'.format(head.attributes)) | |
fout.write('<sodipodi:namedview pagecolor="{0[backgroundColor].value}" inkscape:snap-bbox="{0[objectsSnapTo].value}" />\n'.format(head.attributes)) | |
elif head.tagName == 'DOMSymbolItem': | |
extendedSwatchLists = [] | |
swatchLists = [] | |
timelines = head.getElementsByTagName('timeline')[0] | |
fout.write('<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" version="1.1">\n'.format(head.attributes)) | |
swatchId = 0 | |
fout.write('<defs>\n') | |
for swatchList in swatchLists: | |
if swatchList.nodeType == doc.TEXT_NODE: continue | |
assert swatchList.tagName == 'swatchList' | |
for swatches in swatchList.childNodes: | |
if swatches.nodeType == doc.TEXT_NODE: continue | |
assert swatches.tagName == 'swatches' | |
for swatchItem in swatches.childNodes: | |
if swatchItem.nodeType == doc.TEXT_NODE: continue | |
assert swatchItem.tagName == 'SolidSwatchItem' | |
fout.write('<linearGradient osb:paint="solid" id="swatch{0}">\n'.format(swatchId)) | |
if 'color' in swatchItem.attributes: | |
fout.write('<stop offset="0" style="stop-color:{0}" />\n'.format(swatchItem.attributes['color'].value)) | |
fout.write('</linearGradient>\n') | |
swatchId += 1 | |
for swatchList in extendedSwatchLists: | |
if swatchList.nodeType == doc.TEXT_NODE: continue | |
assert swatchList.tagName == 'swatchList' | |
for swatches in swatchList.childNodes: | |
if swatches.nodeType == doc.TEXT_NODE: continue | |
assert swatches.tagName == 'swatches' | |
for swatchItem in swatches.childNodes: | |
if swatchItem.nodeType == doc.TEXT_NODE: continue | |
handleGradient(fout, swatchItem, 'swatch{0}'.format(swatchId), "gradient") | |
swatchId += 1 | |
fout.write('</defs>\n') | |
timelineId = 0 | |
frameId = 0 | |
layerId = 0 | |
gradientId = 0 | |
for DOMTimeline in timelines.childNodes: | |
if DOMTimeline.nodeType == doc.TEXT_NODE: continue | |
assert DOMTimeline.tagName == 'DOMTimeline' | |
#fout.write('<g id="timeline{0}" inkscape:label={1} inkscape:groupmode="layer">\n'.format(timelineId, quoteattr(DOMTimeline.attributes['name'].value))) | |
layers = timelines.getElementsByTagName('layers')[0] | |
frameNames = None | |
for DOMLayer in layers.childNodes: | |
if DOMLayer.nodeType == doc.TEXT_NODE: continue | |
assert DOMLayer.tagName == 'DOMLayer' | |
fout.write('<g id="layer{0}" inkscape:label={1} inkscape:groupmode="layer">\n'.format(layerId, quoteattr(DOMLayer.attributes['name'].value))) | |
frames = DOMLayer.getElementsByTagName('frames')[0] | |
gridPosX = 0 | |
if frameNames is not None: | |
frameIter = iter(frames.childNodes) | |
for (startIndex, endIndex), name in frameNames: | |
fout.write('<g id="frame{0}" inkscape:label={1} transform="translate({2}, 0)">\n'.format(frameId, quoteattr(name), gridPosX)) | |
gridPosX += 100 | |
gridPosY = 0 | |
for i in range(startIndex, endIndex): | |
while True: | |
DOMFrame = next(frameIter) | |
if DOMFrame.nodeType != doc.TEXT_NODE: break | |
assert DOMFrame.tagName == 'DOMFrame' | |
assert i == int(DOMFrame.attributes['index'].value) | |
handleFrame(fout, DOMFrame) | |
gridPosY += 100 | |
fout.write('</g>\n') | |
frameId += 1 | |
else: | |
if DOMLayer.attributes['name'].value == "Label": | |
frameNames = [] | |
frameNameKeys = [] | |
for DOMFrame in frames.childNodes: | |
if DOMFrame.nodeType == doc.TEXT_NODE: continue | |
assert DOMFrame.tagName == 'DOMFrame' | |
if 'name' in DOMFrame.attributes: | |
name = DOMFrame.attributes['name'].value | |
else: | |
name = None | |
if DOMLayer.attributes['name'].value == "Label": | |
startIndex = int(DOMFrame.attributes['index'].value) | |
if 'duration' in DOMFrame.attributes: | |
endIndex = startIndex+int(DOMFrame.attributes['duration'].value) | |
else: | |
endIndex = startIndex+1 | |
frameNames.append(((startIndex, endIndex), name)) | |
frameNameKeys.append(startIndex) | |
fout.write('<g id="frame{0}"'.format(frameId)) | |
if name is not None: | |
fout.write(' inkscape:label={0}'.format(quoteattr(name))) | |
fout.write(' transform="translate({0}, 0)">\n'.format(gridPosX)) | |
gridPosX += 100 | |
handleFrame(fout, DOMFrame) | |
fout.write('</g>\n') | |
frameId += 1 | |
#break | |
fout.write('</g>\n') | |
layerId += 1 | |
#fout.write('</g>\n') | |
timelineId += 1 | |
fout.write('</svg>') | |
fout.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment