Write your own extensions for Inkscape

Tutorial – Inkscape Scripts

Article from Issue 239/2020
Author(s):

Inkscape's extensions add many useful features. Here's how to write your own.

Badly documented software is common. In fact, I'd say that for free software it is almost the norm. It is also what … er … "inspires" most of my Linux Magazine articles. I set out to do something, hit the wall of insufficient or non-existent documentation, doggedly try to do it anyway, and if I succeed, hey presto, an article.

Which brings me to Inkscape's extensions. Inkscape is a wonderful piece of software, a testament to how good free and open source, community-built applications can be. However, when I began exploring it, I found the documentation of the extension engine to be staggeringly bad. For starters, a link to a Python Effect Tutorial in the Inkscape wiki leads to an empty page ("[extensions under review]") that was last "modified" in 2008!

To add insult to injury, Inkscape was recently updated to version 1.0, after being in the 0.9x circle of hell for years. Much rejoicing was to be had. Alas, with the overhaul of looks and features came an overhaul of the scripting API, and you probably guessed what happened with the docs – nothing. The little documentation there is on the API is still for the old version and is completely obsolete.

The documentation written by third parties regarding Inkscape's extension system is just as bad. Again, it has been made obsolete by Inkscape 1.0, and it is mainly listings of code devoid of comments, which you are expected to understand, or tutorials left halfway complete, as if most authors gave up just when they got past their own particular "Hello World" example.

It would be a pity to let Inkscape's extensions feature go to waste because of its deficient documentation however, so let's do something about it. Let's learn how it works, not by printing "Hello World" (which is pointless anyway), but by drawing a circle.

Circle

An Inkscape extension is made up by two files. The first is an .inx file, which is an XML file (see Listing 1) that, in its most basic form, contains a description of the extension, where it will live in Inkscape's menus, and a link to the executable file. The second file can be in other languages, but it is usually a Python script (see Listing 2).

Listing 1

draw_circle.inx (I)

01 <?xml version="1.0" encoding="UTF-8"?>
02 <inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
03     <name>Draw Circle</name>
04     <id>org.linuxmagazine.inkscape.effects.drawcircle01</id>
05     <effect>
06         <effects-menu>
07             <submenu name="Render"/>
08         </effects-menu>
09     </effect>
10     <script>
11         <command location="inx" interpreter="python">draw_circle.py</command>
12     </script>
13 </inkscape-extension>

Listing 2

draw_circle.py (I)

01 #!/usr/bin/env python
02 # coding=utf-8
03
04 import inkex
05 from inkex import Circle
06
07 class DrawCircle (inkex.EffectExtension):
08     def effect (self):
09         parent = self.svg.get_current_layer ()
10         style = {'stroke': '#000000', 'stroke-width': 1, 'fill': '#FF0000'}
11         circle_attribs = {'style': str(inkex.Style(style)),
12                            inkex.addNS('label', 'inkscape'): "mycircle",
13                            'cx': '100', 'cy': '100',
14                            'r': '100'}
15         parent.add(Circle (**circle_attribs))
16
17 if __name__ == '__main__':
18     DrawCircle ().run ()

The .inx file draw_circle.inx is pretty straightforward. Lines 1 and 2 tell Inkscape routine stuff like the version, character encoding, and type of XML being used. The <name> tag (line 3) establishes what will show up in Inkscape's menus, and <id> gives your extension an identifier that you must make sure is unique so it doesn't clash with any other extension.

As you will see later, if your extension requires parameters, you would put them in here also, but, for the time being, let's just get a circle with a fixed radius and fixed location drawn.

More interesting stuff happens between the <effect></effect> tags (lines 5 to 9). Here you establish where your own extension will go under Inkscape's Extensions menu. In this case, line 7 tells Inkscape you want it in the Render submenu.

The <script></script> tags tell Inkscape where the actual script is located and its name. You will probably read conflicting accounts of how to write this tag online. This is because it is one of the things that has changed in Inkscape 1.0. Line 11 of Listing 1 tells the latest version of Inkscape that draw_circle.py is the name of the script and that the path is relative to the .inx file (location"=inx"). As you will be saving them both to the same directory, there is no need to specify a path along with the script name. The <command> tag also informs Inkscape of the interpreter it needs to run the script, in this case Python.

Speaking of Python scripts, check out Listing 2. This is the program itself, and, again, it is not hard. Lines 1 and 2 are housework – what interpreter to use and the character encoding for the file itself.

Importing inkex (line 4) brings in Inkscape's Python extension module. The inkex family of classes has sub-modules for shapes, text, and other Inkscape elements. On line 5 you import the Circle element, because that is what you're going to draw.

To find out the other things you can import, you can look into the .py files under /usr/share/inkscape/extensions/inkex/elements/. Yes, you have to read the code. There is no documentation online or elsewhere that I could find that described the elements you can import. As far as I have read, you can import modules to create circles, rectangles, lines, text, and other graphical elements, and the attributes of the size, names, and IDs of these things, as well as of the document (this comes in handy later).

How an extension works is that you define your main function (lines 17 and 18) that calls a class you define (line 7). Within the class, you have a series of modules inherited from inkex that you overload with your own code. In this case, you only use one module, effect () (lines 8 to 15).

As an SVG graphic is really just an XML file, and an extension like this one just writes a chunk of XML into the file, you need to tell Inkscape where it is going to go. You do that by specifying the circle's parent, in this case, the current layer you want to draw on (line 9).

On line 10, you set up the style {} of your circle in a Python dictionary – an array with keys and values. The style {} dictionary contains details regarding the stroke (the circle's outline), such as its color, thickness, etc., and also for the fill. For this example, the stroke is going to be black with a width of one unit. The unit will depend on whatever you are using in the document – it can be pixels, millimeters, etc. The fill is going to be solid red.

The style dictionary is then passed into another dictionary (line 11) that also contains the label for the object (mycircle – line 12), its position on the page (100, 100 – line 13), and the size of its radius (100 units – line 14).

You then pass that as a list of parameters to inkex's Circle () module using Python's ** operator. The ** operator allows you to pass a random number of arguments as key and value pairs to a function. The resulting XML data will be added to the parent, in this case the current layer (line 15), and the document will update showing a circle.

Save both these files, circle_draw.inx and circle_draw.py, side by side in the .config/inkscape/extensions directory in your home directory. The next time you start Inkscape, Draw Circle will appear under Extensions | Render in the menu. Click it, and a red circle with a black perimeter shows up on the current layer, as shown in Figure 1.

Figure 1: A circle drawn using our custom extension.

Less Lousiness

Admittedly, the extension above is only a little bit more useful than a "Hello World!" one. A less lousy version would let the user at least decide on the circle's radius (Listing 3).

Listing 3

draw_circle.inx (II)

01 <?xml version="1.0" encoding="UTF-8"?>
02 <inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
03     <name>Draw Circle</name>
04     <id>org.linuxmagazine.inkscape.effects.drawcircle01</id>
05     <param name="radius" type="int" min="1" max="1000" gui-text="Radius:">100</param>
06     <effect>
07         <effects-menu>
08             <submenu name="Render"/>
09         </effects-menu>
10     </effect>
11     <script>
12         <command location="inx" interpreter="python">draw_circle.py</command>
13     </script>
14 </inkscape-extension>

One thing has changed in the .inx file: On line 5, you add the <param> ... </param> tags. This automatically creates a dialog (Figure 2) when you click Draw Circle…. It also adds an ellipsis (…) automagically to the menu entry indicating that clicking will lead to a dialog.

Figure 2: Adding <param> … </param> to your .inx file generates a dialog box where you can set fields – in this case, the circle's radius.

Your dialog currently contains one field, radius, that expects an integer number within the range of 1 and 1,000. You could have just as easily made it a float and that would've allowed you to input numbers with decimal points.

The gui_text option is the label that will be shown next to the field. You can also use _gui_text, and then the label will be marked for translation for the Inkscape translation team.

Getting on to the Python bit, Listing 4 shows how you deal with arguments passed on to the script. On lines 8 and 9, you overload another of inkex's methods, in this case add_arguments (). You use this method to read in all the parameters passed on by the inx dialog.

Listing 4

draw_circle.py (II)

01 #!/usr/bin/env python
02 # coding=utf-8
03
04 import inkex
05 from inkex import Circle
06
07 class DrawCircle (inkex.EffectExtension):
08     def add_arguments (self, pars):
09         pars.add_argument ("--radius", type=int, default=100, help="Radius")
10
11     def effect (self):
12         parent = self.svg.get_current_layer ()
13         style = {'stroke': '#000000', 'stroke-width': 1, 'fill': '#FF0000'}
14         circle_attribs = {'style': str(inkex.Style(style)),
15                            inkex.addNS('label', 'inkscape'): "mycircle",
16                            'cx': '100', 'cy': '100',
17                            'r': str (self.options.radius)}
18         parent.add(Circle (**circle_attribs))
19
20 if __name__ == '__main__':
21     DrawCircle ().run ()

The pars.add_argument works virtually the same as other Python modules used for parsing parameters passed from the command line. Picking apart line 9, "--radius" is the name of the field in <param></param> that you set up in draw_circle.inx. It will also double up as the name of the variable containing the value of the parameter. Look at line 17 in the effect () function, and you can see how you get to it.

The type of the argument comes next (type="int") and then the default value and the help, which would be shown if this script were run from the command line. Both default and help are optional.

As mentioned above, the only change to effect () (lines 11 to 18) is substituting the constant 100 for the variable self.options.radius… and making it a string, which is what inkex's Circle method expects.

There are many other types of parameters you can use to make complex dialogs. A boolean parameter, for example, generates a checkbox to set a true/false value. You can set the default value to true or 1, or false or 0. An optiongroup parameter generates a set of radio buttons from which you can choose one predefined value – the different choices are created with <option> elements.

Fortunately, this is one area with good documentation, so you can read all about the different types of fields you can use [1].

Have a look at Listing 5 for an overview of some of these fields in action. On line 7, you have a text box that lets you input a value between 1 and 1,000 for the radius of the circle, but you already saw that in the example above.

Lines 9 to 13 and 15 to 19 do something more interesting (Figure 3). Say you want to offer your users some options that allow them to choose where to place the circle on the page – something that lets them choose whether to position the circle on the left of the page, in the center, or on the right; and at the top, in the middle, or at the bottom of the page. This is one of those cases where the optiongroup comes in handy, but you can give it a twist: Instead of having it show radio buttons, you can include the appearance="combo" option. This makes the options appear in a combo box, like a drop-down menu.

Figure 3: Several types of fields are used to create this more complex dialog box, which gives the user many choices for setting the properties of the circle.

The notebook widget (lines 21 to 28) creates a set of tabs that can contain other widgets. Your users can switch between each tab and input parameters into the fields they contain. In this example, the notebook widget contains two pages, each containing a color box, one for the stroke of the circle and another for the fill. As you can see in Figure 3, these color boxes are very complete and let your users set not only colors, but also the transparency. The color box even comes with a color-picker. You could, of course, have one color box on top of another, but putting them in a notebook is more elegant.

Also note that, despite notebook being a container for other widgets, you still have to capture a value from it when you process the input from the inx form in your script. Otherwise you will get an error when you click Apply. You will see how to do that below.

At the bottom of Figure 3, you have another text box containing an integer variable, but with a twist: By using the appearance = "full" option (line 30 in Listing 5), you add a slide bar that you can use to also modify the width of the stroke.

Listing 5

draw_circle.inx (III)

01 <?xml version="1.0" encoding="UTF-8"?>
02 <inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
03
04     <name>Draw Circle</name>
05     <id>org.linuxmagazine.inkscape.effects.drawcircle</id>
06
07     <param name="radius" type="int" min="1" max="1000" gui-text="Radius:">100</param>
08
09    <param name="posx" type="optiongroup" appearance="combo" gui-text="Position x:">
10        <option value="left">Left</option>
11        <option value="center">Center</option>
12        <option value="right">Right</option>
13    </param>
14
15    <param name="posy" type="optiongroup" appearance="combo" gui-text="Position y:">
16        <option value="top">Top</option>
17        <option value="center">Center</option>
18        <option value="bottom">Bottom</option>
19    </param>
20
21    <param name="tab" type="notebook">
22        <page name="stroke" gui-text="Stroke">
23            <param name="scolor" type="color" gui-text="Stroke Color:">255</param>
24        </page>
25        <page name="fill" gui-text="Fill">
26            <param name="fcolor" type="color" gui-text="Fill Color:">255</param>
27        </page>
28    </param>
29
30    <param name="swidth" type="int" appearance="full" min="1" max="10" gui-text="Stroke width:">1</param>
31
32    <effect>
33        <object-type>path</object-type>
34        <effects-menu>
35            <submenu name="Render"/>
36        </effects-menu>
37    </effect>
38
39    <script>
40        <command location="inx" interpreter="python">draw_circle.py</command>
41    </script>
42 </inkscape-extension>

Dealing with the options the user sets in the inx dialog is again pretty easy. The script in Listing 6 shows how it is done. You just add the arguments one by one to options using pars.add.argument () in the overloaded add_arguments () method (lines 8 to 17). As mentioned above, even though the notebook widget doesn't really return any useful parameter, you still have to acknowledge it as shown on line 13.

Listing 6

draw_circle.py (III)

01 #!/usr/bin/env python
02 # coding=utf-8
03
04 import inkex
05 from inkex import Circle
06
07 class DrawCircle (inkex.EffectExtension):
08     def add_arguments (self, pars):
09         pars.add_argument ("--radius", type=int, default=100, help="Radius")
10         pars.add_argument ("--posx", type=str, default="left", help="Horizontal position")
11         pars.add_argument ("--posy", type=str, default="top", help="Vertical position")
12
13         pars.add_argument ("--tab", default="object")
14         pars.add_argument ("--scolor", type=inkex.Color, default=inkex.Color("black"), help="Border color")
15         pars.add_argument ("--fcolor", type=inkex.Color, default=inkex.Color("white"), help="Fill color")
16
17         pars.add_argument ("--swidth", type=int, default=1, help="Stroke width")
18
19     def effect (self):
20         parent = self.svg.get_current_layer ()
21
22         if self.options.posx == "left":
23             center_x = self.options.radius + (self.options.swidth/2)
24         elif self.options.posx == "center":
25             center_x = self.svg.width / 2
26         else:
27             center_x = self.svg.width - self.options.radius - (self.options.swidth/2)
28
29         center_x = str (center_x)
30
31         if self.options.posy == "top":
32             center_y = self.options.radius + (self.options.swidth/2)
33         elif self.options.posy == "center":
34             center_y = self.svg.height / 2
35         else:
36             center_y = self.svg.height - self.options.radius - (self.options.swidth/2)
37
38         center_y = str (center_y)
39
40         style = {'stroke': self.options.scolor, 'stroke-width': str(self.options.swidth), 'fill': self.options.fcolor}
41         circle_attribs = {'style': str (inkex.Style(style)),
42                            inkex.addNS ('label', 'inkscape'): "mycircle",
43                            'cx': center_x, 'cy': center_y ,
44                            'r': str (self.options.radius) }
45         parent.add (Circle (**circle_attribs))
46
47 if __name__ == '__main__':
48     DrawCircle ().run ()

Down in the effect () method from line 22 to 27, you process the horizontal position of the circle. If your user chose Left in the form (line 22), line 23 calculates where to place the center of the circle so it is touching the left border of the page. Lines 24 and 25 do a similar thing if the user decides to place the circle in the center of the page and, then again, lines 26 and 27 deal with the "place on the right" choice.

You do a similar thing for the vertical position in lines 31 to 38.

Remember that inkex's functions expect strings and not integer or float numbers so you always have to convert the parameters (lines 29, 38, 40, and 44) before passing them on to Circle () on line 45.

In Figure 4, you can see the result of running Draw Circle several times.

Figure 4: The complete Draw Circle dialog with examples.

Conclusions

Inkscape is one of those applications that always comes up when talking about the astounding heights that free software can reach. Even if it only offered what it does at face value – that is, a program for creating vector graphics – it would be outstanding.

But, when you factor in the effects engine, it becomes so much more. It is a pity, therefore, that the lack of documentation lets it down, cutting off its potential from contributors. Hopefully, with this introduction, more people will begin developing extensions and third party tutorials will start popping up and make up for the missing official manual.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Trembling Text

    Writing your own extension for Inkscape opens up a whole world of possibilities. Apart from creating new objects, you can modify existing objects and even animate them.

  • Inkscape

    The Inkscape vector graphics tool replaces expensive commercial solutions such as Adobe Illustrator. This article shows how to get started with Inkscape.

  • Inkscape 0.45.1

    Inkscape has always been good, but now version 0.45.1 of the vector drawing program shows a totally new creative aspect.

  • Inkscape Vector Graphics

    When it comes to drawing with the computer, professionals often opt for vector graphics. Inkscape brings the power of vector graphics to Linux users. Our workshop demonstrates how to use the program.

  • Create appealing presentations with Inkscape and JessyInk

    Using Inkscape with the JessyInk add-on helps you to create graphically appealing presentations that can be run in a web browser and are indexable by search engines.

comments powered by Disqus

Direct Download

Read full article as PDF:

Price $2.95

News