Ʌ: Tech


Inkscape does not support “macros”.123 This is rather unfortunate, because it’s a great and powerful piece of software otherwise.

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...

        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'))
            with open(svg_file, 'rb') as fhl:

    def _write_svg(self, svg, *filename):
        filename = os.path.join(*filename)
        with open(filename, 'wb') as fhl:
            svg = etree.ElementTree(svg)
        return filename

if __name__ == '__main__':

(Code partly lifted from inkex.command’s source 🌐 and from pathops 🌐)

TODO: Add a skeleton .inx file.

Resources on Extensions

  1. Apparently unfinished attempt: 🌐 

  2. As of 2020.12.01 “Open” feature request: 🌐 

  3. Lonely reddit thread: 🌐 

  4. 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…? 

  5. I’m not 100% on this, because some actions explicitly require an open GUI, but running inkscape --shell --with-gui only ever produced a window that renders once and then freezes on my machine. 

  6. As an example: Screenshot of inkex docs simply stating, sic: "It's not known what this function does..."