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:
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:
- http://wiki.inkscape.org/wiki/index.php/Script_extensions
- http://wiki.inkscape.org/wiki/index.php/PythonEffectTutorial
- http://www.hoboes.com/Mimsy/hacks/write-inkscape-extension-create-multiple-duplicates/
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.
(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.
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.
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
Post a Comment
Please be relevant, helpful and nice.