Last active
October 13, 2015 23:18
-
-
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 :)
This file contains hidden or 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
| 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 :) |
This file contains hidden or 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
| 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. | |
| 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 :) |
This file contains hidden or 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
| <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> |
This file contains hidden or 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/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) | |
This file contains hidden or 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
| <!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, { | |
| lookup: { ids: {},labels:{}}, | |
| code: function() { | |
| var svgDoc = {/*bonsai_content*/}; | |
| 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); | |
| } | |
| }); | |
| console.log("Dump of indexed SVG label attributes:" + objToString(stage.options.lookup.labels)); | |
| console.log("Dump of indexed SVG id attributes:" + objToString(stage.options.lookup.ids)); | |
| 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); | |
| } | |
| stage.options.lookup.ids[node.id] = group; | |
| if (node.label !== '') { | |
| stage.options.lookup.labels[node.label] = group; | |
| } | |
| group.addTo(parent); | |
| } | |
| function addPath(node,parent){ | |
| path = new Path(node.path).attr({ | |
| fillColor: node.attr.fillColor, | |
| 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); | |
| } | |
| stage.options.lookup.ids[node.id] = path; | |
| if (node.label !== '') { | |
| stage.options.lookup.labels[node.label] = path; | |
| } | |
| path.addTo(parent); | |
| } | |
| function addFlowRoot(node,parent){ | |
| } | |
| function addFlowPara(node,parent){ | |
| } | |
| function addFlowRegion(node,parent){ | |
| } | |
| function objToString (obj) { | |
| var str = ''; | |
| for (var p in obj) { | |
| if (obj.hasOwnProperty(p)) { | |
| str += p + '::' + obj[p] + '\n'; | |
| } | |
| } | |
| return "\n" + str; | |
| } | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment