In this tutorial, we will take a look at the Triangle extension source code.
Usually an Inkscape extension consists of two files—one .inx file and
one .py file. The .inx file contains xml code describing the user interface
and the .py file is the
Python code doing the actual work. The .py file can import other modules so an extension
could involve multiple Python modules.
The Triangle extension code is in the triangle.inx and triangle.py
files. Both files are in the system extension directory.
The triangle.inx file has 27 lines. The content of the file is shown below.
<?xml version="1.0" encoding="UTF-8"?> ➊
<inkscape-extension
xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Triangle</name> ➋
<id>math.triangle</id> ➌
<param name="s_a" type="float" min="0.01" max="10000"
gui-text="Side Length a (px):">100.0</param> ➍
<param name="s_c" type="float" min="0.01" max="10000"
gui-text="Side Length c (px):">100.0</param>
<param name="a_a" type="float" min="0" max="180"
gui-text="Angle a (deg):">60</param>
<param name="a_b" type="float" min="0" max="180"
gui-text="Angle b (deg):">30</param>
<param name="a_c" type="float" min="0" max="180"
gui-text="Angle c (deg):">90</param>
<param name="s_b" type="float" min="0.01" max="10000"
gui-text="Side Length b (px):">100.0</param>
<param name="mode" type="optiongroup" appearance="combo"
gui-text="Mode:"> ➎
<option value="3_sides">From Three Sides</option>
<option value="s_ab_a_c">From Sides a, b and Angle c</option>
<option value="s_ab_a_a">From Sides a, b and Angle a</option>
<option value="s_a_a_ab">From Side a and Angles a, b</option>
<option value="s_c_a_ab">From Side c and Angles a, b</option>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Render"/> ➏
</effects-menu>
</effect>
<script>
<command location="inx"
interpreter="python">triangle.py</command> ➐
</script>
</inkscape-extension>
Here is a list of descriptions for above numbered lines.
The first line and inkscape-extension tag are boilerplate code. Every
extension has those lines. Inside the inkscape-extension tag, there
are name, id, param, effect, and script tags.
The name tag value Triangle shows up on the submenu Render. The second
level menu Render (under Extensions main menu) is specified in the submenu tag
under effect -> effects-menu.
The id value must be unique for each extension. We can add
a namespace such as math. before the triangle to make it distinctive.
The param tags represent input controls on the dialog. This Triangle extension
includes two types of param element float and optiongroup. There are many other
types we can use. This
Inkscape wiki page
has a complete list. We don’t have to memorize the user interface
control syntax. We use
the wiki page as a reference and grep the system extension directory
to find some examples.
The command tag under the script element indicates that the extension
code is a Python program in triangle.py file. When we click the apply
button on the dialog, the triangle.py Python program will start running.
The .inx file is in XML format.
XML stands for extensible markup language, which is a popular file format
in early 2000s. I still remember attending a seminar in college and the
speaker says something like every one should learn XML and use XML. The
format becomes less popular over time. The default Inkscape file format
SVG is also in XML format.
The triangle.py file has 188 lines. Part of the content is shown below.
import sys
from math import acos, asin, cos, pi, sin, sqrt
import inkex
X, Y = range(2)
def draw_SVG_tri(point1, point2, point3, offset,
width, name, parent):
...
......
def draw_tri_from_3_sides(s_a, s_b, s_c, offset, width, parent):
if is_valid_tri_from_sides(s_a, s_b, s_c):
a_b = angle_from_3_sides(s_a, s_c, s_b)
a = (0, 0)
b = v_add(a, (s_c, 0))
c = v_add(b, pt_on_circ(s_a, pi - a_b))
c[1] = -c[1]
offx = max(b[0], c[0]) / 2
offy = c[1] / 2
offset = (offset[0] - offx, offset[1] - offy)
draw_SVG_tri(a, b, c, offset, width, 'Triangle', parent)
else:
inkex.errormsg('Invalid Triangle Specifications.')
class Triangle(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--s_a", type=float, default=100.0,
help="Side Length a")
pars.add_argument("--s_b", type=float, default=100.0,
help="Side Length b")
...
pars.add_argument("--mode", default='3_sides',
help="Side Length c")
def effect(self):
tri = self.svg.get_current_layer()
offset = self.svg.namedview.center
self.options.s_a = self.svg.unittouu(
str(self.options.s_a) + 'px')
self.options.s_b = self.svg.unittouu(
str(self.options.s_b) + 'px')
self.options.s_c = self.svg.unittouu(
str(self.options.s_c) + 'px')
stroke_width = self.svg.unittouu('2px')
if self.options.mode == '3_sides':
s_a = self.options.s_a
s_b = self.options.s_b
s_c = self.options.s_c
draw_tri_from_3_sides(s_a, s_b, s_c,
offset, stroke_width, tri)
elif self.options.mode == 's_ab_a_c':
...
......
if __name__ == '__main__':
Triangle().run()
The last two lines of the file is the main entry point. The Python program
initializes an instance of Triangle class and calls the run method.
The Triangle class
itself only defines two methods add_argument and effect, so the run method
must be inherited from other classes.
The Triangle class inherits EffectExtension class of inkex module.
The Python modules are in the inkex subdirectory of system extension directory.
The inkex is
the most basic module of Inkscape extension system. It acts like a framework (API) upon
which we build user extensions.
Here are directory names and file names under the inkex directory. The first
column shows that it is a directory (dir) or number of lines for a Python file.
(dir) deprecated-simple
(dir) elements
(dir) tester
33 ./__init__.py # line of code | file name
377 ./base.py
425 ./bezier.py
474 ./colors.py
233 ./command.py
403 ./deprecated.py
378 ./extensions.py
50 ./inkscape_env.py
214 ./inx.py
66 ./localization.py
1672 ./paths.py
100 ./ports.py
382 ./styles.py
1116 ./transforms.py
120 ./turtle.py
76 ./tween.py
107 ./units.py
209 ./utils.py
6435 total
3 directories, 18 files
The EffectExtension class is defined in the extensions.py file, but it’s just
a subclass of SvgThroughMixin and InkscapeExtension. Pay attention to the docstring
of the class, which summarizes what this class does.
class EffectExtension(SvgThroughMixin, InkscapeExtension):
"""
Takes the SVG from Inkscape, modifies the selection or the document
and returns an SVG to Inkscape.
"""
pass
The SvgThroughMixin and InkscapeExtension classes are defined in the base.py
file. The run method is defined in InkscapeExtension class. The code is shown
below.
def run(self, args=None, output=stdout):
# type: (Optional[List[str]], Union[str, IO]) -> None
"""Main entry point for any Inkscape Extension"""
try:
if args is None:
args = sys.argv[1:]
self.parse_arguments(args)
if self.options.input_file is None:
self.options.input_file = sys.stdin
if self.options.output is None:
self.options.output = output
self.load_raw()
self.save_raw(self.effect())
except AbortExtension as err:
err.write()
sys.exit(ABORT_STATUS)
finally:
self.clean_up()
Let’s add some logging code to this file and check logging output. If you are not familiar with Python logging module, check out the Python Logging Howto Page. This 15 minutes youtube video explains the basics of logging module very well.
In order to
modify files in the system extension directory, we need to change the directory and
file permissions. Run those bash commands when you are in the system extension
directory. Be sure to make a copy of the directory before modifying files.
$ cd /usr/share/inkscape/extensions
$ sudo chmod -R 777 ../extensions/
If you install inkscape via snap, you will have a difficult time modifying any
files under /snap directory. This is a security feature of snap apps. How do
I know it? I waste several hours trying
various methods but fail to modify permissions. In the end I simply uninstall
the snap app and reinstall Inkscape through apt commands.
Add those lines at the top of base.py to setup logging module. You need to
change the filename directory if you are following this example.
from .localization import localize
# setup logging
import logging
# change filename path
logging.basicConfig(filename='/home/george/Desktop/new-logging.txt',
filemode='w', format='%(levelname)s: %(message)s', level=logging.DEBUG)
Then add six logging debug output lines in the run method.
def run(self, args=None, output=stdout):
# type: (Optional[List[str]], Union[str, IO]) -> None
"""Main entry point for any Inkscape Extension"""
logging.debug('run starts') ##1
logging.debug(f'python exec: {sys.executable}') ##2
try:
if args is None:
args = sys.argv[1:]
self.parse_arguments(args)
if self.options.input_file is None:
self.options.input_file = sys.stdin
if self.options.output is None:
self.options.output = output
logging.debug(f'sys argv: {sys.argv}') ##3
logging.debug(f'input : {self.options.input_file}') ##4
logging.debug(f'output : {self.options.output}') ##5
self.load_raw()
self.save_raw(self.effect())
except AbortExtension as err:
err.write()
sys.exit(ABORT_STATUS)
finally:
self.clean_up()
logging.debug('run ends') ##6
The results of logging in the new-loggin.txt file are
DEBUG: run starts
DEBUG: python exec: /usr/bin/python3
DEBUG: sys argv: ['triangle.py', '--s_a=100', '--s_b=100',
'--s_c=100', '--a_a=60', '--a_b=30', '--a_c=90',
'--mode=3_sides', '/tmp/ink_ext_XXXXXX.svgVYXM70']
DEBUG: input : /tmp/ink_ext_XXXXXX.svgVYXM70
DEBUG: output : <_io.BufferedWriter name='<stdout>'>
DEBUG: run ends
Notice the sys.executable output is /usr/bin/python3. Inkscape invokes Python
complier at this location to run extension programs. The inkex module depends on
the lxml module, which is already installed for this compiler. You can simply
import lxml module (shown below) to confirm that.
george@NUC:~$ /usr/bin/python3
Python 3.9.7 (default, Sep 10 2021, 14:59:43)
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> import lxml
>>>
Between the load_raw and save_raw method calls, there is an effect method call.
The effect method is defined in the InkscapeExtension class, but it raises an
NotImplementedError exception. The method is a placeholder for subclasses to override. The effect method in Triangle class overrides it.
The InkscapeExtension class defines a debug method. We can invoke this method
to output messages. The method redirects a message to the standard error stream,
and Inkscape will display the message on a dialog box. However, the logging
module is more flexible to use. The debug method is designed to
deliver a message to an extension user.
You may feel overwhelmed or even frustrated by now if you are not familiar with Python. Most Python introductory books do not even cover classes. But keep reading and experimenting, and the code will gradually make sense to you.
Like most things in the programming world, it’s better to learn a little and start working on something. There may be things that you don’t understand, but you shouldn’t wait to start because you probably will never understand everything. The extension Python code is all open source and available to you. You are free to experiment and modify it as you like.
2. Triangle Extension