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
IMPORTANT - ALL LAYERS MUST BE HIDDEN AND THEN DISPLAYED IN THE LAYERS LIST TO
GET THE NEEDED STYLE ATTRIBUTE WHICH IS USED BY BANSAI!
* 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 :)
Update 20121216 22:42
Separated out the Bansai code into its own "static" closure. This to make it more reusable.
Update 20121214 16:37
Added indexing of all added Bonsai objects. This makes it possible to reference them later in
your script like this:
var tree = stage.options.lookup.labels['tree1'];
or
var tree = stage.options.lookup.ids['g7816'];
And you get a reference back to that instance.
This means that you can preserve your Inkscape layer/shape/group naming in you script. Neat.... :)
You JS console will also output all the contents of these two indexes.
--------------------------------------------------------------------------------------------------
Update 20121214 15:23
Externalized the template HTML code to a separate file. This should be placed, along with the
other files, in the Inkscape/share/extensions/ folder.
--------------------------------------------------------------------------------------------------
Update 20121213 10:51
Added check to make sure that bounding boxes are never "None" but instead empty arrays.
Update 20121213 10:21
I'm such a dork. I forgot to add transform support for Path elements.
Fixed now though :)
<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 = []
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, 'bansai-'+time_stamp+'.html')
content = self.templateOutput('larscwallin.inx.bansai.template.html','{/*bonsai_content*/}')
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)
box = list(box) if box != None else []
group = {
'id':id,
'name':label,
'svg':'g',
'transform':transform,
'box':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)
transform = simpletransform.parseTransform(node.get('transform',''))
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':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']) if path['box'] != None else []
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,templateName = '',placeholder = ''):
if(placeholder == ''):
inkex.debug('Bonsai.templateOutput: Mandatory argument "placeholder" missing. Aborting.')
return False
if(templateName == ''):
inkex.debug('Bonsai.templateOutput: Mandatory argument "templateName" missing. Aborting.')
return False
FILE = open(templateName,'r')
if(FILE):
template = FILE.read()
FILE.close()
if(len(template) > 0):
content = ','.join([str(el) for el in self.json_output])
tplResult = string.replace(template,placeholder,content);
return tplResult
else:
inkex.debug('Bonsai.templateOutput: Empty template file "'+templateName+'". Aborting.')
return False
else:
inkex.debug('Bonsai.templateOutput: Unable to open template file "'+templateName+'". Aborting.')
return False
def saveToFile(self,content,filename):
FILE = open(filename,'w')
if(FILE):
FILE.write(content)
FILE.close()
return True
else:
inkex.debug('Bonsai.templateOutput: Unable to open output file "'+filename+'". Aborting.')
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)
<!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() {
bansai = {
ids: {},
labels:{},
createBackgroundLayer:function(){return new Group().attr({x:0, y:0}).addTo(stage)},
stage:null,
init:function(stage,svgDoc){
backgroundLayer = bansai.createBackgroundLayer();
bansai.stage = stage;
svgDoc.elements.forEach(function(el) {
if(el.svg === 'g'){
bansai.addGroup(el,backgroundLayer);
}else{
bansai.addPath(el,backgroundLayer);
}
});
console.log("Dump of indexed SVG label attributes:" + bansai.objToString(bansai.labels));
console.log("Dump of indexed SVG id attributes:" + bansai.objToString(bansai.ids));
},
addGroup:function(node,parent){
var group = new Group();
node.elements.forEach(function(el){
if(el.svg === 'g'){
bansai.addGroup(el,group);
}else if(el.svg === 'path'){
bansai.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);
}
bansai.ids[node.id] = group;
if (node.label !== '') {
bansai.labels[node.label] = group;
}
group.addTo(parent);
},
addPath:function(node,parent){
path = new Path(node.path).attr({
fillColor: node.attr.fillColor,
fillOpacity: node.attr.fillOpacity,
strokeColor: node.attr.strokeColor,
strokeWidth: node.attr.strokeWidth
});
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]);
path.attr('matrix',m);
}
bansai.ids[node.id] = path;
if (node.label !== '') {
bansai.labels[node.label] = path;
}
path.addTo(parent);
},
addFlowRoot:function (node,parent){
},
addFlowPara:function (node,parent){
},
addFlowRegion:function(node,parent){
},
objToString:function(obj) {
var str = '';
for (var p in obj) {
if (obj.hasOwnProperty(p)) {
str += p + '::' + obj[p] + '\n';
}
}
return "\n" + str;
}
};
var svgDoc = {/*bonsai_content*/};
bansai.init(stage,svgDoc);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment