Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, PHP, and whatever else I happen to feel like hacking at.

Write an Inkscape extension: create multiple duplicates

Jerry Stratton, January 3, 2010

While making maps for Gods & Monsters last night, I needed to duplicate the same item thirty times at exact pixel intervals apart. As far as I can tell, there is no way to do this directly in Inkscape; the recommended way is to make tiled clones, and then disconnect the clones from each other. Even that doesn’t tile in terms of pixels, however, but in relation to the bounding box. Normally what I do when I need actual copies spaced specifically is make one copy, move it where I want it; duplicate both of those copies, move the two new ones where I want them; duplicate all four, etc., and then when I’m done remove the extras. That’s a little tedious, though, and, because I’m easily distracted, prone to error.1 I decided to look into making an Inkscape extension to do what I wanted.

Inkscape extensions consist of two parts: a configuration file that defines how and where the extension appears in the GUI, and a script file that does whatever the extension is supposed to do.

Configuration

The configuration file is a simple XML file. It needs to end in .inx and it needs to go in your personal extensions directory. The example says “~/.inkscape/extensions” but it looks like this has changed, in Inkscape .47, to “~/.config/inkscape/extensions/”.

For example, the following .inx file, saved as “duplicates.inx”, will create a new submenu in Inkscape’s Extensions menu called “Mine”.

[toggle code]

  • <inkscape-extension>
    • <_name>Multiple Copies</_name>
    • <id>com.hoboes.filter.duplicates</id>
    • <dependency type="executable" location="extensions">inkex.py</dependency>
    • <dependency type="executable" location="extensions">simpletransform.py</dependency>
    • <param name="number" type="int" min="1" max="1000" _gui-text="How many copies would you like?">2</param>
    • <param name="horizontal" type="float" min="-10000.0" max="10000.0" _gui-text="X Offset">0.0</param>
    • <param name="vertical" type="float" min="-10000.0" max="10000.0" _gui-text="Y Offset">0.0</param>
    • <effect>
      • <object-type>all</object-type>
      • <effects-menu>
        • <submenu _name="Mine" />
      • </effects-menu>
    • </effect>
    • <script>
      • <command reldir="extensions" interpreter="python">duplicates.py</command>
    • </script>
  • </inkscape-extension>
  • The submenu will contain the _name defined in the .inx file; here that’s “Multiple Copies”.
  • The ID needs to be unique; if you have a personal domain, that’s a good choice.
  • There is a list of dependencies. Inkscape will make sure that those dependencies exist before adding this item to the submenu.
  • There is a list of parameters. Inkscape will create a dialog box that asks for these parameters. For integers, the default minimum is 0 and the default maximum is 10.
  • Most extensions seem to just be set to “all” object-types. Other object-types include “path” and “rect”.
  • The <script>’s <command> defines the script file that will perform the effect, and also includes the name of the interpreter. Here, I’ll be using Python.

You can play around with adding new parameters to make the dialogue box ask for the things you want it to ask for. Other parameter types are “string” and “boolean”. See INX extension descriptor format for more about Inkscape’s .inx files.

Script

There’s one important dependency that I left out so that we could play around with the dialog box. That’s our own script. Add one more dependency:

  • <dependency type="executable" location="extensions">duplicates.py</dependency>

Once you add that, Inkscape will no longer display your menu item until you add duplicates.py to your extensions folder. Save this as duplicates.py:

[toggle code]

  • #!/usr/bin/python
  • import sys, copy
  • sys.path.append('/Applications/Inkscape.app/Contents/Resources/extensions')
  • import inkex, simpletransform
  • class DuplicateMultiple(inkex.Effect):
    • def __init__(self):
      • inkex.Effect.__init__(self)
      • self.OptionParser.add_option('-n', '--number-of-copies', action='store', type='int', dest='number', default=2, help='How many copies would you like?')
      • self.OptionParser.add_option('-x', '--horizontal', action='store', type='float', dest='horizontal', default=0, help='Horizontal distance?')
      • self.OptionParser.add_option('-y', '--vertical', action='store', type='float', dest='vertical', default=0, help='Vertical distance?')
    • def effect(self):
      • transformation = 'translate(' + str(self.options.horizontal) + ', ' + str(self.options.vertical) + ')'
      • transform = simpletransform.parseTransform(transformation)
      • if self.selected:
        • for id, node in self.selected.iteritems():
          • counter = self.options.number
          • while counter > 0:
            • newNode = copy.deepcopy(node)
            • simpletransform.applyTransformToNode(transform, newNode)
            • self.current_layer.append(newNode)
            • counter = counter - 1
            • node = newNode
  • effect = DuplicateMultiple()
  • effect.affect()

You might have to change the location of Inkscape.app! The above path assumes that you’re using Mac OS X and Inkscape is in your main Applications folder. If you’re putting it in a personal Applications folder, it might be:

  • sys.path.append('/Users/username/Applications/Inkscape.app/Contents/Resources/extensions')

We need to tell Python where Inkscape’s built-in extensions folder is so that Python can import “inkex” and “simpletransform”.

The extension script is fairly simple:

  1. Create a class that inherits from inkex.Effect.
  2. Add an __init__ method to set up the same options you set up in the .inx file. You also have to call the base class’s __init__ method as well2.
  3. Add an effect method to do whatever you want the extension to do.
  4. Instantiate the effect.
  5. Call the “affect” method on the effect.

See Python modules for extensions for more of the things you can have Inkscape do to nodes or to documents. This is how I knew that inkex.Effect has a “selected” property of the currently-selected objects. One useful property that isn’t listed there is “current_layer”, which is, as you might suspect, the currently-selected layer. That’s most likely where you’re going to want to create any new things.

I then loop through all selected items, make a copy of each item however many times I chose, and append the new item to the current layer. I’m using applyTransformToNode from Inkscape’s simpletransform module. It takes a string of the transformation: translate, rotate, scale, etc. Here, the translation is going to look something like “translate(xx, yy)” where “xx” is the horizontal offset chosen in the dialog box, and “yy” is the vertical offset.

If you want to create a new layer instead of putting the duplicates on the current layer, get the document’s svg xml and make a layer off of that.

  • #get the document
  • svg = self.document.getroot()
  • #Create a new layer.
  • layer = inkex.etree.SubElement(svg, 'g')
  • layer.set(inkex.addNS('label', 'inkscape'), 'LAYER NAME')
  • layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')

You can see an example of that in PythonEffectTutorial.

As far as I can tell, this is all just XML, so anything you can do at one level you can do at another. If you want to put the new layer in as a sublayer of the current layer instead of in a “sublayer” of the root level, use self.current_layer as the parent instead of the svg root:

  • #put copies in a separate sublayer of the current layer
  • layer = inkex.etree.SubElement(self.current_layer, 'g')
  • layer.set(inkex.addNS('label', 'inkscape'), 'DUPLICATES LAYER')
  • layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')

Then, instead of “self.current_layer.append(newNode)”, use “layer.append(newNode)”3.

Debugging

  • Errors will be in your personal Inkscape configuration directory. In .47, this is “~/.config/inkscape/extension-errors.log”.
  • You can use inkex.debug() to display a window with variable values while debugging. You may also find inkex.debug(dir(something)) useful to show all of the properties/methods on an object.
  • You may find that grepping *.py in Inkscape.app/Contents/Resources/extensions will help you find how an existing extension does something similar. This, for example, is how I discovered that “path” and “rect” can also be used for the object-type element.
  • If you make changes to the .inx file, you’ll need to quit Inkscape for the changes to take effect. Changes to the script file take effect immediately, even if the dialog box is open.

Further

There are other kinds of extensions than Effect. For example, there is a CharDataEffect (that you can use by importing chardataeffect) that lets you modify strings of selected text in Inkscape. Look in the source code for extensions with names that make it obvious that they affect text, such as flipcase.

You’ve probably noticed that your dialog box already has “Needs Live Preview” on it. Inkscape adds this for you, and handles it for you, automatically—you don’t need to do anything to get this functionality. If you do need to turn it off, though, you can add needs-live-preview="false" to the <effects> element:

  • <effect needs-live-preview="false">
March 15, 2014: Updated Inkscape extension

I noticed on the Inkscape forum that a couple of parts of this tutorial are out of date.

First, the line that includes “/Applications/Inkscape.app/Contents/Resources/extensions” is no longer necessary. The path is now known to all extensions, and by removing this line you can make your extensions more portable. For one thing, you no longer have to worry about the location of the Inkscape app.

Second, the .inx files are now full-fledged XML. This means that they need a real declaration at the top. Replace the “inkscape-extension” root element with:

  • <?xml version="1.0" encoding="UTF-8"?>
  • <inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">

This makes the full code:

[toggle code]

  • #!/usr/bin/python
  • import copy
  • import inkex, simpletransform
  • class DuplicateMultiple(inkex.Effect):
    • def __init__(self):
      • inkex.Effect.__init__(self)
      • self.OptionParser.add_option('-n', '--number-of-copies', action='store', type='int', dest='number', default=2, help='How many copies would you like?')
      • self.OptionParser.add_option('-x', '--horizontal', action='store', type='float', dest='horizontal', default=0, help='Horizontal distance?')
      • self.OptionParser.add_option('-y', '--vertical', action='store', type='float', dest='vertical', default=0, help='Vertical distance?')
    • def effect(self):
      • transformation = 'translate(' + str(self.options.horizontal) + ', ' + str(self.options.vertical) + ')'
      • transform = simpletransform.parseTransform(transformation)
      • if self.selected:
        • for id, node in self.selected.iteritems():
          • counter = self.options.number
          • while counter > 0:
            • newNode = copy.deepcopy(node)
            • #newNode.set('style', 'fill:none;stroke:#ff0000;stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1')
            • simpletransform.applyTransformToNode(transform, newNode)
            • self.current_layer.append(newNode)
            • counter = counter - 1
            • node = newNode
      • #inkex.debug(newNode.attrib)
  • effect = DuplicateMultiple()
  • effect.affect()

And the full .inx file:

  1. See, for example, learning to write an Inkscape extension when all I needed was to duplicate a line thirty times.

  2. I don’t know why it doesn’t use “super”, but rather calls the base class’s __init__ directly. All of the built-in extensions do this, too. Judging from the error I got when I experimented (“TypeError: super() argument 1 must be type, not classobj”), I suspect that they’re using old-style classes.

  3. In the example, I’ve hardcoded the new layer to be called “DUPLICATES LAYER”. You would want to find some way of making the name unique; while Inkscape is fine having two layers with the same name, it will be confusing for anyone using the document. You might use a “string” parameter to get the name, for example, or if you’re the kind of person who labels your objects, use the object label as the base name of the new layer.

  1. <- Date/time formsets
  2. A present for Palm ->