Skip to main content

Inkscape extension example

Recently, when preparing figures for publications using Inkscape, I decided to simplify my life. Some of the figures can be described as 2x2, 2x3, or even 2x6 grids. Sometimes it is very awkward to rearrange them if you decide to switch from 2x6 to 3x4 or vice versa. It would be nice to have an Inkscape extension, which will minimize the efforts.

For the sake of simplicity we assume that all the elements are equal in width and should be rearranged symmetrically with respect to the vertical axis. Also, the element should be ordered according to its relative to the other elements location. The top left element is the first and the bottom right is the last one. So for example, you can visually prepare the elements and then tidy them up with a single click of our magic button.

Hint: before developing a similar extension, you may want to check the standard Align and Distribute tool (Ctrl+Shift+A).

Before writing any actual code let's take a look at some official Inkscape extensions information:
First, we need to create a GUI dialog, something like this one.


Simply enough, the code below does the job. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<inkscape-extension>
    <_name>Grid Layout</_name>
    <id>org.pub.scientific.grid_layout</id>
    <dependency type="executable" location="extensions">grid_layout.py</dependency>
    <dependency type="executable" location="extensions">inkex.py</dependency>
    <param name="rows" type="int" min="1" max="100" _gui-text="Rows">2</param>
    <param name="cols" type="int" min="1" max="100" _gui-text="Cols">2</param>
    <param name="box-width" type="float" min="-1000.0" max="1000.0" _gui-text="Grid item width">100.0</param>

    <param name="xoffset" type="float" min="-1000.0" max="1000.0" _gui-text="X Offset">10.0</param>
    <param name="yoffset" type="float" min="-1000.0" max="1000.0" _gui-text="Y Offset">10.0</param>
    <effect>
        <object-type>all</object-type>
        <effects-menu>
        <submenu _name="Scientific Paper"/>
        </effects-menu>
    </effect>
    <script>
        <command reldir="extensions" interpreter="python">grid_layout.py</command>
    </script>
</inkscape-extension>

Put this code into the ~/.config/inkscape/extensions/grid_layout.inx file. It is quite straightforward and is similar to the other tutorials. So let's concentrate on the actual extension code.

As we have already mentioned, we will need to prepare our objects visually. But how do we know the ordering for the layout? And here it goes, the tricky part. We will need to compare the objects before we build the grid. For example, we can compare only top left corners, and prioritize the ordering by vertical arrangement. That is, firstly we check if one object is below the other, and if not, compare the objects horizontal position. We need to prioritize since we work in the two-dimensional space. Moreover, we will need to do it in a slight fuzzy way, because if you have two objects with almost similar vertical or horizontal coordinates (i.e. a few pixels plus or minus), you will want to treat them as equal vertically or horizontally rather then different.

Actually, it is not as hard, as it sounds. For instance, we can define some tolerance level (i.e. 5%) to tell whether objects are different or not. And here is the Python code for the comparison routine.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def compare((ax, ay, aw, ah), (bx, by, bw, bh), tol=0.05):
    """
    Compares with priority of vertical location.
    """
    # Kind of tolerance normalization
    toln = tol * max([min([aw, bw]), min([ah, bh])])
    if ay > by + toln:
        return 1
    elif ay < by - toln:
        return -1
    if ax > bx + toln:
        return 1
    elif ax < bx - toln:
        return -1
    return 0

(ax, ay, aw, ah) are the x-, y-coordinate, width and height for the node A and (bx, by, bw, bh) are the x-, y-coordinate, width and height for the node B. toln is a normalized value of tol percentage converted to some physical units (usually pixels).

And here is a simple task for the reader:
1) Modify the code to compare objects by their centers of mass instead of top left coordinates.
2) Think of other ways of ordering.
3) What are the other possible ways to define toln?

Now that we have solved our ordering task, a technical part is left. We need to derive the Inkex.Effect class imported from inkex Python module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from collections import defaultdict

import inkex

def coords(node):
    """
    Returns a tuple of coordinates: (x, y, width, height).
    """
    t = [float(node.get(x)) for x in ['x', 'y', 'width', 'height']]
    return (t[0], t[1], t[2], t[3])


class GridLayoutEffect(inkex.Effect):
    """
    Inkscape effect extension. Rearranges selected items into a grid. Resizes
    items proportionally to desired width.
    """
    def __init__(self):
        inkex.Effect.__init__(self)

        self.OptionParser.add_option('', '--rows', action='store',
            type='int', dest='rows', default=2,
            help='Number of rows')
        self.OptionParser.add_option('', '--cols', action='store',
            type='int', dest='cols', default=2,
            help='Number of cols')
        self.OptionParser.add_option('', '--box-width', action='store',
            type='float', dest='box_width', default=100.0,
            help='Grid item width')
        self.OptionParser.add_option('', '--xoffset', action='store',
            type='float', dest='xoffset', default=10.0,
            help='X offset')
        self.OptionParser.add_option('', '--yoffset', action='store',
            type='float', dest='yoffset', default=10.0,
            help='Y offset')

    def effect(self):
        """
        Rearranges selected elements into a grid with respect to the first
        element top left corner. If there are not enough elements, empty slots
        are left. If there are not enough slots, odd elements are ignored.
        """
        if self.selected:
            all_nodes = [x[1] for x in self.selected.iteritems()]
            ordered_set = sorted(all_nodes, cmp=compare, key=coords)
            left_top = (ordered_set[0].get('x'), ordered_set[0].get('y'))
            o = self.options
            col_heights = defaultdict(lambda: 0)
            for index, node in enumerate(ordered_set):
                col = index % o.cols
                row = int(index / o.cols)
                x = col * (o.xoffset + o.box_width)
                node.set('x', str(x))
                y = col_heights[col]
                node.set('y', str(y))
                resized_box_height = float(node.get('height')) / \
                                     (float(node.get('width')) / o.box_width)
                col_heights[col] += o.yoffset + resized_box_height
                node.set('height', str(resized_box_height))
                node.set('width', str(o.box_width))
        else:
            inkex.debug('No items selected')

It is not hard to see, the __init__ method is utilized to bypass the parameters from the Inkscape GUI dialog. The effect is the core of our extension. We check if there are selected objects (line #43). A useful method inkex.debug (line #62) may be involved providing feedback from the extension to the Inkscape.

self.selected.iteritems() returns an enumerator over the selected nodes, providing id and node pairs. We just need nodes, second element in pair, for this task (line #44). On the line #45 we apply our compare method to the nodes, which were ordered by their ids, using standard Python way. Interesting is the key parameter. It defines the key to sort. Here the coords adapter method is the key. It extracts coordinates from the nodes, so the compare method will receive plain 4-number tuples, not node objects. Now we have our nodes ordered by our top-bottom, left-right priority we have previously defined in the compare method.

On the line #46 we define the top left corner with respect to which all the grid is arranged. Simply, it is a top left corner of the least priority element (or the first one in the ordered set).

Lines #50-55 are to calculate the grid coordinates and to apply them on the selected objects using set method. defaultdict (defined back on line #48) is a dictionary with default value (zero in our case). First time when we check the col_heights defaultdict, we receive zero, because no col height was calculated before. On the line #58 we store the current column height to reuse it later for the next object placed below in the same column.

Code on #56-60 proportionally scales objects. As we assumed in the beginning, the width is common for all the objects and is set in the GUI dialog.

Here is the full code of our extension. Place the contents into the already known ~/.config/inkscape/extensions location. The final task is simple: adjust it for your own needs.

That's all folk's.

Comments

Popular posts from this blog

How to program SPI flash memory on Digilent Nexys4 and other 7th generation devices using Vivado 2014.x

Update: The methods described below work also on Vivado 2015! Please let me know if you had any issues with other Vivado versions. The information in the official user guides and tutorials seems to be fragmentary and sometimes out-of-date, that's why I decided to fill in this gap, not waiting for the official Xilinx updates. Our goal is to show how to program the SPI flash memory of the 7th generation Xilinx devices with Vivado 2014 on example of  Nexys4  board. Note:  before we start, we assume you already have a working project, that means the generated bitstream file works on your device. If not, you may want to go directly to the  Program the SPI flash  section to test your Nexys4 board with prepared .mcs file. Plan Configure the hardware to work with SPI flash memory Regenerate the bitstream file Convert the bitstream file into the "Prom" format Program the SPI flash Boot the device Configure the hardware to work with SPI flash memory ...

How to fix 'ImportError: cannot import name QtCore' on Debian Linux

What to do if such import error occurs? >>> import PySide >>> from PySide import QtCore Traceback (most recent call last):   File "<stdin>", line 1, in <module> ImportError: cannot import name QtCore 1. Try to locate where your pyside is installed: $ locate PySide ... /usr/lib/python2.7/dist-packages/PySide /usr/lib/python2.7/dist-packages/PySide/__init__.py /usr/lib/python2.7/dist-packages/PySide/__init__.pyc /usr/lib/python2.7/dist-packages/PySide/phonon.so /usr/lib/python2.7/dist-packages/PySide/QtCore.so /usr/lib/python2.7/dist-packages/PySide/QtDeclarative.so /usr/lib/python2.7/dist-packages/PySide/QtGui.so /usr/lib/python2.7/dist-packages/PySide/QtHelp.so ... Hint: use this to update your file index: $ sudo updatedb 2. The solution is easy: add this to your ~/.bashrc (or ~/.<shell>rc you actually have). export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/python2.7/dist-packages/PySide