Chapter 2.  Triangle Extension Code

Posted on

Extension Structure

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.

Inx File

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.

  1. this line indicates it is an xml file
  2. name tag, specifies the name on submenu (Render -> Triangle)
  3. id tag, unique id of the extension
  4. param tag, specifies an input control on dialog box
  5. param tag, specifies a select control
  6. submenu tag, shows up as a submenu under Extension menu (Extensions -> Render)
  7. command tag, the name of the Python file to invoke

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.

Python File

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()

Logging Experiment

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.

What’s Next

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.