Write an Inkscape extension: create multiple duplicates
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
-
for id, node in self.selected.iteritems():
-
def __init__(self):
- 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:
- Create a class that inherits from inkex.Effect.
- 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.
- Add an effect method to do whatever you want the extension to do.
- Instantiate the effect.
- 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
-
for id, node in self.selected.iteritems():
- #inkex.debug(newNode.attrib)
-
def __init__(self):
- effect = DuplicateMultiple()
- effect.affect()
And the full .inx file:
See, for example, learning to write an Inkscape extension when all I needed was to duplicate a line thirty times.
↑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.
↑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.
↑
- Inkscape
- “Inkscape is an Open Source vector graphics editor. Supported features include shapes, paths, text, markers, clones, alpha blending, transforms, gradients, patterns, and grouping. Inkscape also supports Creative Commons meta-data, node editing, layers, complex path operations, bitmap tracing, text-on-path, flowed text, direct XML editing, and more. It imports formats such as JPEG, PNG, TIFF, and others and exports PNG as well as multiple vector-based formats.”
- Inkscape Extension subsystem
- “The Extension system is a way to add functionality to Inkscape. The design of the extension system is similar to the bridge design pattern, breaking apart the functionality that is being provided, and the implementation of that functionality. The term extension is used to describe all of this, the functionality and the implementation.”
- Inkscape extensions source
- There are many built-in extensions here, with their .inx and .py files, that you can look at for examples.
- INX extension descriptor format
- “In order for Inkscape to make use of an external script or program, you must describe that script to inkscape using an INX file.”
- Python modules for extensions
- Describes the main modules you’ll be using to act on Inkscape documents and nodes.
- PythonEffectTutorial
- If you want to create a new layer that says “Hello World”, this tutorial will show you how.
More Inkscape
- Updated Inkscape extension
- Inkscape extensions on Mac OS X are a little easier in the latest versions.
- Get element by id in Inkscape
- A Google search couldn’t find this before, so I’m going to nudge it along. The way to get an Inkscape node by Id in an Effect is with self.getElementById('Id').
- Using the color picker in Inkscape extensions
- It was pretty easy, looking at the built-in extensions, to switch colors from one to another if I wanted to type their values by hand. But using the color parameter type took a bit of sleuthing.
Hello, thanks for the tutorial! I have a question, though: what if I would like to create a big number of copies and I would like to automatically group them together in a single big group? Is it possible to do so? I don't know python at all, so it's quite difficult for me to do that on my own :)
Alessandro Roncone in Italy at 10:24 a.m. March 29th, 2014
noISX
My guess is that the way to group items is to create an empty group, and then add the new (or existing) items to the group.
This item on the inkscape wiki might have ideas.
And then in the loop, something like:
Instead of “self.current_layer.append (newNode)”.
But that’s just a wild guess.
Jerry Stratton in Round Rock, TX at 6:12 p.m. March 29th, 2014
VekeO
Thanks, I will try it out!
Alessandro Roncone in Italy at 8:35 p.m. May 24th, 2014
pQX1k
Great work thanks. I wrote a little extension that allows you to type and execute lines of python from directly within inkscape. I think it would be useful to anyone who's writing scripts, as it makes scripting quite quick. http://www.smanohar.com/inkscape.php
example
sanjay in uk at 7:45 p.m. September 3rd, 2016
hQikt
Hi Jerry! When I dumped windows for Ubuntu (one reason being cost of updates), I had to also dump CorelDraw - my favourite graphics prog. I tried Inkscape, and altho it was a "steep learning curve", now I absolutely love it.
Frustrating tho, that I couldn't see how to transfer all the macros I'd written for Corel to Inkscape. Nothing I googled gave me a clue as to how to actually write a script that would work.
Until I found your site! It was still a struggle, but you helped me see the light and now I get it!! Well, I'm getting there. I've done a couple of scripts that are proving really helpful, and I can see my way to writing more!
Thanks so much for your help. :oD
I think it would be great if there was an "idiot's guide to scripting inkscape" for those of us that are struggling like I did.
Thanks again,
Terry
Terry in England at 11:13 a.m. December 20th, 2016
Tp08Y