On the other hand, it has quite extensive and somewhat hidden scripting capabilities, including a “shell mode” 🌐 (
inkscape --shell) that allows you to edit SVG using high-level commands, a.k.a. “verbs” and4 “actions”!
Unfortunately, it seems that this “shell mode” (or verbs/action interface in general) is inaccessible from the GUI (and even incompatible with “normal” editing5), and there is no way to send actions to a running Inkscape instance.
Fortunately, verbs and actions are perfectly invokable and composable from within Python extensions (or rather, via the inkscape command line, which presents one way extensions can act on the document). And it is this route that I took and found pretty good.
The fun part is that inkscape only checks the presence of the python file on start-up, but doesn’t preload or cache the extension in any way - meaning that each invocation of the extension runs with the up-to-date code, which in turn means it’s quite easy to sort of “live code” ad-hoc extensions (and poor man’s macros) simply by having an editor open alongside inkscape.
The following presents a “skeleton” extension for running custom sequences of command (which was not exactly easy to tease out from the docs6), and should be fairly easy to edit and build on top of it for custom “extensions”.
TODO: explain the basic relation of inkex and how python extensions are otherwise structured.
#!/usr/bin/env python3 import inkex import inkex.command from inkex.utils import TemporaryDirectory from lxml import etree import os class MacroExtension(inkex.EffectExtension): def effect(self): svg = self.document.getroot() # The following gets you all selected objects - actual SVG nodes. objects = [svg.getElement(path) for path in svg.selected] # Or, you can use the "paint/z order" functions. # Unfortunately, I found they worked rather erratically... ## objects = [svg.getElement(path) for path in svg.selected.paint_order()] ??? ## objects = [svg.getElement(path) for path in svg.get_z_selected()] ??? actions =  for node_id in [o.get('id') for o in objects]: # Important: The temporary SVG file starts with nothing selected. actions.append("select-by-id:" + node_id) # See `inkscape --verb-list` and `inkscape --action-list` actions.append("!WHATEVER COMMAND!") # Important: The selection does not clear itself... actions.append("select-clear") result = self._inkscape_actions(svg, actions) print(result) # This actually commits the changes! def _inkscape_actions(self, svg, actions): with TemporaryDirectory(prefix='inkscape-macros') as dirname: svg_file = self._write_svg(svg, dirname, 'input.svg') actions.extend(('FileSave', 'FileQuit')) inkex.command.inkscape( svg_file, "--with-gui", actions=';'.join(actions) ) with open(svg_file, 'rb') as fhl: return fhl.read().decode('utf-8') def _write_svg(self, svg, *filename): filename = os.path.join(*filename) with open(filename, 'wb') as fhl: svg = etree.ElementTree(svg) svg.write(fhl) return filename if __name__ == '__main__': MacroExtension().run()
TODO: Add a skeleton
Resources on Extensions
- https://inkscape-extensions-guide.readthedocs.io 🌐
- https://inkscape.gitlab.io/ 🌐
- https://medium.com/@xaviju/inkscape-extensions-by-non-developers-for-non-developers-a-primer-b272dda360fe 🌐
Apparently unfinished attempt: https://wiki.inkscape.org/wiki/index.php/Inkscape/macrogoodness 🌐 ↩
Lonely reddit thread: https://www.reddit.com/r/Inkscape/comments/aztj6g/scripts_or_macros/ 🌐 ↩
From what I gathered, both are essentially just “commands”, but “verbs” are the older variant that do not support parameters, and “actions” are the “new verbs”, except not all have been converted yet…? ↩
I’m not 100% on this, because some actions explicitly require an open GUI, but running
inkscape --shell --with-guionly ever produced a window that renders once and then freezes on my machine. ↩
As an example: ↩