Skip to content

Instantly share code, notes, and snippets.

@larscwallin
Last active October 13, 2015 23:18
Show Gist options
  • Select an option

  • Save larscwallin/4271123 to your computer and use it in GitHub Desktop.

Select an option

Save larscwallin/4271123 to your computer and use it in GitHub Desktop.
Bansai! exports SVG from wonderous Inkscape to BonsaiJS template code. Still in Alpha but works as a proof of concept :)
Bansai! version 0.00001
* What is Bansai?
This is a script extension for Inkscape.It was inspired by the splendid BonsaiJS library. As well as
my lazy disposition, which does not like "coding" graphics ;)
So in short Bansai lets you select one or elements in Inkscape, and export them to BonsaiJS JSON notation.
* What is supported by this version?
A little, and still a lot :) At the moment I have had time to implement support for
- Path elements
- Group elements (also nested)
- Transformation matrix
These initial features can get you pretty far as most, or all, SVG shapes can be described using
one or more of the path types available.
* What is NOT supported by this version?
A lot of course, such as
- Filters
- Gradients
- Fonts
- "use" references
Thanks to Bonsai its really easy for anyone with a basic knowledge of Python,
JavaScript and SVG to help with development :)
<inkscape-extension>
<_name>Bansai!</_name>
<id>com.larscwallin.bansai</id>
<dependency type="executable" location="extensions">larscwallin.inx.bansai.py</dependency>
<dependency type="executable" location="extensions">inkex.py</dependency>
<param name="where" type="string" _gui-text="Where to save the resulting JS file?"></param>
<param name="reposition" type="boolean" _gui-text="Reposition each selected element to 0,0?">false</param>
<param name="viewresult" type="boolean" _gui-text="Do you wish to view the result?">true</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu _name="Export"/>
</effects-menu>
</effect>
<script>
<command reldir="extensions" interpreter="python">larscwallin.inx.bansai.py</command>
</script>
</inkscape-extension>
#!/usr/bin/env python
import inkex
import simpletransform
import simplepath
import os.path
from simplestyle import *
from time import gmtime, strftime
import string
import webbrowser
import threading
from optparse import OptionParser
# This line below is only needed if you don't put the script directly into
# the installation directory
# sys.path.append('/usr/share/inkscape/extensions')
"""
Bansai! version 0.00001
* What is Bansai?
This is a script extension for Inkscape.It was inspired by the splendid BonsaiJS library. As well as
my lazy disposition, which does not like "coding" graphics ;)
So in short Bansai lets you select one or elements in Inkscape, and export them to BonsaiJS JSON notation.
* What is supported by this version?
A little, and still a lot :) At the moment I have had time to implement support for
- Path elements
- Group elements (also nested)
- Transformation matrix
These initial features can get you pretty far as most, or all, SVG shapes can be described using
one or more of the path types available.
* What is NOT supported by this version?
A lot of course, such as
- Filters
- Gradients
- Fonts
- "use" references
Thanks to Bonsai its really easy for anyone with a basic knowledge of Python,
JavaScript and SVG to help with development :)
"""
class SVGElement():
id=''
label=''
box=[]
path=[]
fill=''
fill_opacity=1
stroke=''
stroke_width=0
stroke_opacity=1
transform=''
x=0
y=0
width=0
height=0
nodeRef = None
def __init__(self,node = None):
self.nodeRef = node
class SVGPath(SVGElement):
def __init__(self,node = None):
SVGElement.__init__(self)
class SVGRect(SVGElement):
def __init__(self,node = None):
SVGElement.__init__(self)
class SVGArc(SVGElement):
cx=0
cy=0
rx=0
ry=0
def __init__(self,node = None):
SVGElement.__init__(self)
class SVGGroup(SVGElement):
items=[]
def __init__(self,node = None):
SVGElement.__init__(self)
"""
This is where the actual fun stuff starts
"""
# Effect main class
class Bansai(inkex.Effect):
json_output = []
debug_tab =' '
svg_doc = None
svg_doc_width = ''
svg_doc_height = ''
svg_file = ''
bonsaistyle = False
reposition = True
parsing_context = ''
parse_stack = []
# Proof of concept. This should of course be loaded from file later...
template = """<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
</head>
<body>
<script src='http://cdnjs.cloudflare.com/ajax/libs/bonsai/0.4.1/bonsai.min.js'></script>
<div id='stage'></div>
<script>
stage = document.getElementById('stage');
bonsai.run(stage, {
code: function() {
var svgDoc = /*larscwallin_inx_elements_to_json_output*/;
var backgroundLayer = new Group().attr({x:0, y:0}).addTo(stage);
svgDoc.elements.forEach(function(el) {
if(el.svg === 'g'){
addGroup(el,backgroundLayer);
}else{
addPath(el,backgroundLayer);
}
});
function addGroup(node,parent){
var group = new Group();
node.elements.forEach(function(el){
if(el.svg === 'g'){
addGroup(el,group);
}else if(el.svg === 'path'){
addPath(el,group);
}
});
if(node.transform!==''){
m = new Matrix();
m.scale(node.transform[0][0],node.transform[1][1]);
m.rotate(node.transform[1][0]);
m.translate(node.transform[0][2],node.transform[1][2]);
group.attr('matrix',m);
}
group.addTo(parent);
}
function addPath(el,parent){
path = new Path(el.path).attr({
fillColor: el.attr.fillColor,
strokeColor: el.attr.strokeColor,
strokeWidth: el.attr.strokeWidth
}).addTo(parent);
}
function addFlowRoot(node,parent){
}
function addFlowPara(node,parent){
}
function addFlowRegion(node,parent){
}
}
});
</script>
</body>
</html>
"""
def __init__(self):
"""
Constructor.
"""
"""
First we grab the input parameters that we got from the Inkscape plugin system (see the .inx file)
"""
inkex.Effect.__init__(self)
self.OptionParser.add_option('--where', action = 'store',
type = 'string', dest = 'where', default = '',
help = 'Where to save the resulting file?')
self.OptionParser.add_option('--reposition', action = 'store',
type = 'inkbool', dest = 'reposition', default = False,
help = 'Reposition elements to 0,0?')
self.OptionParser.add_option('--bonsaistyle', action = 'store',
type = 'inkbool', dest = 'bonsaistyle', default = False,
help = 'Should the output be BonsaiJS specific?')
self.OptionParser.add_option('--viewresult', action = 'store',
type = 'inkbool', dest = 'viewresult', default = True,
help = 'Do you want to view the result?')
def effect(self):
"""
Effect behaviour.
Overrides base class method
"""
self.svg_file = self.args[-1]
self.svg_doc = self.document.xpath('//svg:svg',namespaces=inkex.NSS)[0]
self.svg_doc_width = inkex.unittouu(self.svg_doc.get('width'))
self.svg_doc_height = inkex.unittouu(self.svg_doc.get('height'))
self.where = self.options.where
self.reposition = self.options.reposition
self.bonsaistyle = self.options.bonsaistyle
self.viewresult = self.options.viewresult
filename = ''
success = False
self.getselected()
if(self.selected.__len__() > 0):
#inkex.debug(self.debug_tab + 'Elements selected\n');
self.json_output.append({
'defs':{
'filters':[],
'fonts':[],
'gradients':[]
},
'elements':[]
})
parent = self.json_output[0]['elements']
selected = []
layers = self.document.xpath('//svg:svg/svg:g/*',namespaces=inkex.NSS)
# Iterate through all selected elements
for element in self.selected.values():
selected.append(element.get('id'))
#inkex.debug(self.debug_tab + 'selected ' + element.get('id'))
for element in layers:
#inkex.debug(self.debug_tab + 'Looping element ' + element.get('id'))
if(element.get('id') in selected):
self.parseElement(element,parent)
#inkex.debug(self.debug_tab + 'found ' + element.get('id'))
self.debug_tab = self.debug_tab[:-4]
else:
#inkex.debug(self.debug_tab + 'No elements were selected')
layers = self.document.xpath('//svg:svg/svg:g[@style!="display:none"]',namespaces=inkex.NSS)
self.json_output.append({
'svg':'document',
'id':'',
'name':'',
'transform':'',
'box':[
0,
self.svg_doc_width,
0,
self.svg_doc_height
],
'defs':{
'filters':[],
'fonts':[],
'gradients':[]
},
'elements':[]
})
parent = self.json_output[0]['elements']
# Iterate through all selected elements
for element in layers:
self.parseElement(element,parent)
self.debug_tab = self.debug_tab[:-4]
#inkex.debug(self.debug_tab + '\nDone iterating.\n')
#inkex.debug(self.debug_tab + ','.join([str(el) for el in self.json_output]))
if(self.where!=''):
# The easiest way to name rendered elements is by using their id since we can trust that this is always unique.
time_stamp = strftime('%a%d%b%Y%H%M', gmtime())
filename = os.path.join(self.where, 'elements-to-json-'+time_stamp+'.html')
content = self.templateOutput()
success = self.saveToFile(content,filename)
if(success and self.viewresult):
self.viewOutput(filename)
else:
inkex.debug('Unable to write to file "' + filename + '"')
#inkex.debug(self.debug_tab + ','.join([str(el) for el in self.json_output]))
def parseGroup(self,node,parent):
#inkex.debug(self.debug_tab + 'Parsing group' + node.get('id'))
self.debug_tab += ' '
self.parsing_context = 'g'
id = node.get('id')
transform = simpletransform.parseTransform(node.get('transform',''))
label = str(node.get(inkex.addNS('label', 'inkscape'),''))
elements = node.xpath('./*',namespaces=inkex.NSS)
box = simpletransform.computeBBox(elements)
group = {
'id':id,
'name':label,
'svg':'g',
'transform':transform,
'box':list(box),
'elements':[]
}
parent.append(group)
self.parse_stack.append(group)
#inkex.debug('Loop through all grouped elements')
for child in elements:
self.parseElement(child,group["elements"])
self.debug_tab = self.debug_tab[:-4]
self.parsing_context = ''
self.parse_stack.pop()
def parseElement(self,node,parent):
type = node.get(inkex.addNS('type', 'sodipodi'))
if(type == None):
#remove namespace data {....}
tag_name = node.tag
tag_name = tag_name.split('}')[1]
else:
tag_name = str(type)
id = node.get('id')
#inkex.debug(self.debug_tab + 'Got "' + tag_name + '" element ' + id);
if(tag_name == 'g'):
self.parseGroup(node,parent)
elif(tag_name == 'path'):
self.parsePath(node,parent)
elif(tag_name == 'arc'):
self.parsePath(node,parent)
elif(tag_name == 'rect'):
self.parseRect(node,parent)
def parseStyleAttribute(self,str):
rules = str.split(';')
parsed_set = {}
result = ''
for rule in rules:
parts = rule.split(':')
parsed_set[parts[0]] = parts[1]
result = parsed_set
return result
def parsePath(self,node,parent):
#self.parsing_context = 'path'
style = node.get('style')
style = self.parseStyleAttribute(style)
path_array = simplepath.parsePath(node.get('d'))
#inkex.debug(style)
## if(self.bonsaistyle):
## inkex.debug('Bonsai style!')
## for attr in style:
## inkex.debug('Attr ' + attr)
## camelAttr = attr.split('-')
## if(len(camelAttr) == 2):
## attr = camelAttr[0] + camelAttr[1].title()
## inkex.debug('Camel version ' + attr)
path = {
'id':node.get('id'),
'svg':'path',
'label':str(node.get(inkex.addNS('label', 'inkscape'),'')),
'box':list(simpletransform.computeBBox([node])),
'transform':node.get('transform',''),
'path':path_array,
'd':node.get('d',''),
'attr':{
'fillColor':style['fill'],
'fillOpacity':style.get('fill-opacity','1'),
'strokeColor':style.get('stroke',''),
'strokeWidth':style.get('stroke-width','0'),
'strokeOpacity':style.get('stroke-opacity','1')
}
}
#inkex.debug('Path resides in group ' + self.parse_stack[len(self.parse_stack)-1]['id'])
if(self.reposition):
path['path'] = self.movePath(path,0,0,'tl')
else:
path['path'] = simplepath.formatPath(path_array)
path['box'] = list(path['box'])
parent.append(path)
"""
movePath changes a paths bounding box x,y extents to a new, absolute, position.
In other words, this function does not use translate for repositioning.
Note: The origin parameter is not currently used but will soon let you choose
which origin point (top left, top right, bottom left, bottom right, center)
to use.
"""
def movePath(self,node,x,y,origin):
path = node.get('path')
box = node.get('box')
#inkex.debug(box)
offset_x = (box[0] - x)
offset_y = (box[2] - (y))
#inkex.debug('Will move path "'+id+'" from x, y ' + str(box[0]) + ', ' + str(box[2]))
#inkex.debug('to x, y ' + str(x) + ', ' + str(y))
#inkex.debug('The x offset is ' + str(offset_x))
#inkex.debug('The y offset is = ' + str(offset_y))
for cmd in path:
params = cmd[1]
i = 0
while(i < len(params)):
if(i % 2 == 0):
#inkex.debug('x point at ' + str( round( params[i] )))
params[i] = (params[i] - offset_x)
#inkex.debug('moved to ' + str( round( params[i] )))
else:
#inkex.debug('y point at ' + str( round( params[i]) ))
params[i] = (params[i] - offset_y)
#inkex.debug('moved to ' + str( round( params[i] )))
i = i + 1
#inkex.debug(simplepath.formatPath(path))
return simplepath.formatPath(path)
def parseRect(self,node,parent):
#self.parsing_context = 'rect'
style = node.get('style')
style = self.parseStyleAttribute(style)
rect = {
'id':node.get('id',''),
'svg':'rect',
'label':str(node.get(inkex.addNS('label', 'inkscape'),'')),
'x': node.get('x',0),
'y': node.get('y',0),
'width':node.get('width',0),
'height':node.get('height',0),
'box':[],
'fill':style.get('fill',''),
'fill-opacity':style.get('fill-opacity',''),
'stroke':style.get('stroke',''),
'stroke-width':style.get('stroke-width',''),
'stroke-opacity':style.get('stroke-opacity',''),
'transform':node.get('transform','')
}
if(self.reposition):
self.x = 0
self.y = 0
parent.append(rect)
def parseArc(self,node,parent):
#self.parsing_context = 'arc'
style = node.get('style')
style = self.parseStyleAttribute(style)
arc = {
'id':node.get('id',''),
'svg':'arc',
'label':str(node.get(inkex.addNS('label', 'inkscape'),'')),
'cx': node.get(inkex.addNS('cx', 'sodipodi'),''),
'cy':node.get(inkex.addNS('cy', 'sodipodi'),''),
'rx':node.get(inkex.addNS('rx', 'sodipodi'),''),
'ry':node.get(inkex.addNS('ry', 'sodipodi'),''),
'path':simplepath.parsePath(node.get('d')),
'd':node.get('d',''),
'box':list(simpletransform.computeBBox([node])),
'fill':style.get('fill',''),
'fill-opacity':style.get('fill-opacity',''),
'stroke':style.get('stroke',''),
'stroke-width':style.get('stroke-width',''),
'stroke-opacity':style.get('stroke-opacity',''),
'transform':node.get('transform','')
}
if(self.reposition):
arc['path'] = self.movePath(node,0,0,'tl')
else:
arc['path'] = arc['d']
parent.append(arc)
def parseDef(self,node,parent):
pass
def parseFont(self,node,parent):
pass
def parseGlyph(self,node,parent):
pass
def pathToObject(self,node):
pass
def repositionGroupedElements(self, box, elements):
pass
def viewOutput(self,url):
vwswli = VisitWebSiteWithoutLockingInkscape()
vwswli.url = url
vwswli.start()
def templateOutput(self):
content = ','.join([str(el) for el in self.json_output])
tplContent = string.replace(self.template,'/*larscwallin_inx_elements_to_json_output*/',content);
return tplContent
def saveToFile(self,content,filename):
FILE = open(filename,'w')
if(FILE):
FILE.write(content)
FILE.close()
return True
else:
return False
class VisitWebSiteWithoutLockingInkscape(threading.Thread):
url = ''
def __init__(self):
threading.Thread.__init__ (self)
def run(self):
webbrowser.open(self.url)
# Create effect instance and apply it.
effect = Bansai()
effect.affect(output=False)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment