Merge lp:~rwhite8282/inkscape/frame-extension into lp:~inkscape.dev/inkscape/trunk

Proposed by Richard White
Status: Merged
Merged at revision: 15728
Proposed branch: lp:~rwhite8282/inkscape/frame-extension
Merge into: lp:~inkscape.dev/inkscape/trunk
Diff against target: 411 lines (+389/-0)
4 files modified
share/extensions/frame.inx (+32/-0)
share/extensions/frame.py (+175/-0)
share/extensions/test/frame_test.py (+120/-0)
share/extensions/test/svg/single_box.svg (+62/-0)
To merge this branch: bzr merge lp:~rwhite8282/inkscape/frame-extension
Reviewer Review Type Date Requested Status
Martin Owens code and direction Approve
Review via email: mp+287379@code.launchpad.net

Description of the change

A new frame extension that renders a frame around selected objects.

To use:
1) Select an object such as a box.
2) Activate the frame extension.
3) The desired frame should render around the selected object.

To post a comment you must log in.
14670. By Richard White

Corrected frame extension stroke and fill values on 64 bit machine.
The lack of L suffix in the represented hex value caused an improper interpretation.

14671. By Richard White

Merge from Inkscape trunk.

14672. By Richard White

Corrected frame extension inside option box size.

Revision history for this message
Martin Owens (doctormo) wrote :

This is a solid extension. The python looks good, the inx looks good and the tests are much appreciated.

review: Approve (code and direction)
Revision history for this message
Richard White (rwhite8282) wrote :

Thank you for your efforts Martin, and your kind words.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'share/extensions/frame.inx'
2--- share/extensions/frame.inx 1970-01-01 00:00:00 +0000
3+++ share/extensions/frame.inx 2016-05-19 02:06:20 +0000
4@@ -0,0 +1,32 @@
5+<?xml version="1.0" encoding="UTF-8"?>
6+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
7+ <_name>Frame</_name>
8+ <id>frame</id>
9+ <dependency type="executable" location="extensions">frame.py</dependency>
10+ <dependency type="executable" location="extensions">inkex.py</dependency>
11+ <param name="tab" type="notebook">
12+ <page name="stroke" gui-text="Stroke">
13+ <param name="stroke_color" type="color" gui-text="Stroke Color:">000000FF</param>
14+ </page>
15+ <page name="fill" gui-text="Fill">
16+ <param name="fill_color" type="color" gui-text="Fill Color:">00000000</param>
17+ </page>
18+ </param>
19+ <param name="position" type="optiongroup" appearance="minimal" gui-text="Position">
20+ <option value="outside">Outside</option>
21+ <option value="inside">Inside</option>
22+ </param>
23+ <param name="clip" type="boolean" gui-text="Clip"></param>
24+ <param name="group" type="boolean" gui-text="Group"></param>
25+ <param name="width" type="float" min="0" max="100" gui-text="Width(px)">2</param>
26+ <param name="corner_radius" type="int" min="0" max="1000" gui-text="Corner Radius">0</param>
27+ <effect>
28+ <object-type>all</object-type>
29+ <effects-menu>
30+ <submenu _name="Render"/>
31+ </effects-menu>
32+ </effect>
33+ <script>
34+ <command reldir="extensions" interpreter="python">frame.py</command>
35+ </script>
36+</inkscape-extension>
37\ No newline at end of file
38
39=== added file 'share/extensions/frame.py'
40--- share/extensions/frame.py 1970-01-01 00:00:00 +0000
41+++ share/extensions/frame.py 2016-05-19 02:06:20 +0000
42@@ -0,0 +1,175 @@
43+#!/usr/bin/env python
44+"""
45+An Inkscape extension that creates a frame around a selected object.
46+
47+Copyright (C) 2016 Richard White, rwhite8282@gmail.com
48+
49+This program is free software; you can redistribute it and/or modify
50+it under the terms of the GNU General Public License as published by
51+the Free Software Foundation; either version 2 of the License, or
52+(at your option) any later version.
53+
54+This program is distributed in the hope that it will be useful,
55+but WITHOUT ANY WARRANTY; without even the implied warranty of
56+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
57+GNU General Public License for more details.
58+
59+You should have received a copy of the GNU General Public License
60+along with this program; if not, write to the Free Software
61+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
62+"""
63+
64+# These two lines are only needed if you don't put the script directly into
65+# the installation directory
66+import sys
67+sys.path.append('/usr/share/inkscape/extensions')
68+
69+import inkex
70+import simplestyle
71+from simpletransform import *
72+from simplestyle import *
73+
74+
75+def get_picker_data(value):
76+ """ Returns color data in style string format.
77+ value -- The value returned from the color picker.
78+ Returns an object with color and opacity properties.
79+ """
80+ v = '%08X' % (value & 0xFFFFFFFF)
81+ color = '#' + v[0:-2].rjust(6, '0')
82+ opacity = '%1.2f' % (float(int(v[6:].rjust(2, '0'), 16))/255)
83+ return type('', (object,), {'color':color, 'opacity':opacity})()
84+
85+
86+def size_box(box, delta):
87+ """ Returns a box with an altered size.
88+ delta -- The amount the box should grow.
89+ Returns a box with an altered size.
90+ """
91+ return ((box[0]-delta), (box[1]+delta), (box[2]-delta), (box[3]+delta))
92+
93+
94+# Frame maker Inkscape effect extension
95+class Frame(inkex.Effect):
96+ """ An Inkscape extension that creates a frame around a selected object.
97+ """
98+ def __init__(self):
99+ inkex.Effect.__init__(self)
100+ self.defs = None
101+
102+ # Parse the options.
103+ self.OptionParser.add_option('--clip',
104+ action='store', type='inkbool',
105+ dest='clip', default=False)
106+ self.OptionParser.add_option('--corner_radius',
107+ action='store', type='int',
108+ dest='corner_radius', default=0)
109+ self.OptionParser.add_option('--fill_color',
110+ action='store', type='int',
111+ dest='fill_color', default='00000000')
112+ self.OptionParser.add_option('--group',
113+ action='store', type='inkbool',
114+ dest='group', default=False)
115+ self.OptionParser.add_option('--position',
116+ action='store', type='string',
117+ dest='position', default='outside')
118+ self.OptionParser.add_option('--stroke_color',
119+ action='store', type='int',
120+ dest='stroke_color', default='00000000')
121+ self.OptionParser.add_option('--tab',
122+ action='store', type='string',
123+ dest='tab', default='object')
124+ self.OptionParser.add_option('--width',
125+ action='store', type='float',
126+ dest='width', default=2)
127+
128+
129+ def add_clip(self, node, clip_path):
130+ """ Adds a new clip path node to the defs and sets
131+ the clip-path on the node.
132+ node -- The node that will be clipped.
133+ clip_path -- The clip path object.
134+ """
135+ if self.defs is None:
136+ defs_nodes = self.document.getroot().xpath('//svg:defs', namespaces=inkex.NSS)
137+ if defs_nodes:
138+ self.defs = defs_nodes[0]
139+ else:
140+ inkex.errormsg('Could not locate defs node for clip.')
141+ return
142+ clip = inkex.etree.SubElement(self.defs, inkex.addNS('clipPath','svg'))
143+ clip.append(copy.deepcopy(clip_path))
144+ clip_id = self.uniqueId('clipPath')
145+ clip.set('id', clip_id)
146+ node.set('clip-path', 'url(#%s)' % str(clip_id))
147+
148+
149+ def add_frame(self, parent, name, box, style, radius=0):
150+ """ Adds a new frame to the parent object.
151+ parent -- The parent that the frame will be added to.
152+ name -- The name of the new frame object.
153+ box -- The boundary box of the node.
154+ style -- The style used to draw the path.
155+ radius -- The corner radius of the frame.
156+ returns a new frame node.
157+ """
158+ r = min([radius, (abs(box[1]-box[0])/2), (abs(box[3]-box[2])/2)])
159+ if (radius > 0):
160+ d = ' '.join(str(x) for x in
161+ ['M', box[0], (box[2]+r)
162+ ,'A', r, r, '0 0 1', (box[0]+r), box[2]
163+ ,'L', (box[1]-r), box[2]
164+ ,'A', r, r, '0 0 1', box[1], (box[2]+r)
165+ ,'L', box[1], (box[3]-r)
166+ ,'A', r, r, '0 0 1', (box[1]-r), box[3]
167+ ,'L', (box[0]+r), box[3]
168+ ,'A', r, r, '0 0 1', box[0], (box[3]-r), 'Z'])
169+ else:
170+ d = ' '.join(str(x) for x in
171+ ['M', box[0], box[2]
172+ ,'L', box[1], box[2]
173+ ,'L', box[1], box[3]
174+ ,'L', box[0], box[3], 'Z'])
175+
176+ attributes = {'style':style, inkex.addNS('label','inkscape'):name, 'd':d}
177+ return inkex.etree.SubElement(parent, inkex.addNS('path','svg'), attributes )
178+
179+
180+ def effect(self):
181+ """ Performs the effect.
182+ """
183+ # Get the style values.
184+ corner_radius = self.options.corner_radius
185+ stroke_data = get_picker_data(self.options.stroke_color)
186+ fill_data = get_picker_data(self.options.fill_color)
187+
188+ # Determine common properties.
189+ parent = self.current_layer
190+ position = self.options.position
191+ width = self.options.width
192+ style = simplestyle.formatStyle({'stroke':stroke_data.color
193+ , 'stroke-opacity':stroke_data.opacity
194+ , 'stroke-width':str(width)
195+ , 'fill':(fill_data.color if (fill_data.opacity > 0) else 'none')
196+ , 'fill-opacity':fill_data.opacity})
197+
198+ for id, node in self.selected.iteritems():
199+ box = computeBBox([node])
200+ if 'outside' == position:
201+ box = size_box(box, (width/2))
202+ else:
203+ box = size_box(box, -(width/2))
204+ name = 'Frame'
205+ frame = self.add_frame(parent, name, box, style, corner_radius)
206+ if self.options.clip:
207+ self.add_clip(node, frame)
208+ if self.options.group:
209+ group = inkex.etree.SubElement(node.getparent(),inkex.addNS('g','svg'))
210+ group.append(node)
211+ group.append(frame)
212+
213+
214+if __name__ == '__main__': #pragma: no cover
215+ # Create effect instance and apply it.
216+ effect = Frame()
217+ effect.affect()
218\ No newline at end of file
219
220=== added file 'share/extensions/test/frame_test.py'
221--- share/extensions/test/frame_test.py 1970-01-01 00:00:00 +0000
222+++ share/extensions/test/frame_test.py 2016-05-19 02:06:20 +0000
223@@ -0,0 +1,120 @@
224+#!/usr/bin/env python
225+
226+"""
227+An Inkscape frame extension test class.
228+
229+Copyright (C) 2016 Richard White, rwhite8282@gmail.com
230+
231+This program is free software; you can redistribute it and/or modify
232+it under the terms of the GNU General Public License as published by
233+the Free Software Foundation; either version 2 of the License, or
234+(at your option) any later version.
235+
236+This program is distributed in the hope that it will be useful,
237+but WITHOUT ANY WARRANTY; without even the implied warranty of
238+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
239+GNU General Public License for more details.
240+
241+You should have received a copy of the GNU General Public License
242+along with this program; if not, write to the Free Software
243+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
244+"""
245+
246+import sys
247+sys.path.append('/usr/share/inkscape/extensions')
248+sys.path.append('..') # this line allows to import the extension code
249+
250+import unittest
251+import inkex
252+from frame import *
253+
254+
255+class Frame_Test(unittest.TestCase):
256+
257+
258+ def get_frame(self, document):
259+ return document.xpath('//svg:g[@id="layer1"]//svg:path[@inkscape:label="Frame"]'
260+ , namespaces=inkex.NSS)[0]
261+
262+
263+ def test_empty_no_parameters(self):
264+ args = [ 'svg/empty-SVG.svg' ]
265+ uut = Frame()
266+ uut.affect( args, False )
267+
268+
269+ def test_single_frame(self):
270+ args = [
271+ '--corner_radius=20'
272+ , '--fill_color=-16777124'
273+ , '--id=rect3006'
274+ , '--position=inside'
275+ , '--stroke_color=255'
276+ , '--tab="stroke"'
277+ , '--width=10'
278+ , 'svg/single_box.svg']
279+ uut = Frame()
280+ uut.affect( args, False )
281+ new_frame = self.get_frame(uut.document)
282+ self.assertIsNotNone(new_frame)
283+ self.assertEqual('{http://www.w3.org/2000/svg}path', new_frame.tag)
284+ new_frame_style = new_frame.attrib['style'].lower()
285+ self.assertTrue('fill-opacity:0.36' in new_frame_style
286+ , 'Invalid fill-opacity in "' + new_frame_style + '".')
287+ self.assertTrue('stroke:#000000' in new_frame_style
288+ , 'Invalid stroke in "' + new_frame_style + '".')
289+ self.assertTrue('stroke-width:10.0' in new_frame_style
290+ , 'Invalid stroke-width in "' + new_frame_style + '".')
291+ self.assertTrue('stroke-opacity:1.00' in new_frame_style
292+ , 'Invalid stroke-opacity in "' + new_frame_style + '".')
293+ self.assertTrue('fill:#ff0000' in new_frame_style
294+ , 'Invalid fill in "' + new_frame_style + '".')
295+
296+
297+ def test_single_frame_grouped(self):
298+ args = [
299+ '--corner_radius=20'
300+ , '--fill_color=-16777124'
301+ , '--group=True'
302+ , '--id=rect3006'
303+ , '--position=inside'
304+ , '--stroke_color=255'
305+ , '--tab="stroke"'
306+ , '--width=10'
307+ , 'svg/single_box.svg']
308+ uut = Frame()
309+ uut.affect( args, False )
310+ new_frame = self.get_frame(uut.document)
311+ self.assertIsNotNone(new_frame)
312+ self.assertEqual('{http://www.w3.org/2000/svg}path', new_frame.tag)
313+ group = new_frame.getparent()
314+ self.assertEqual('{http://www.w3.org/2000/svg}g', group.tag)
315+ self.assertEqual('{http://www.w3.org/2000/svg}rect', group[0].tag)
316+ self.assertEqual('{http://www.w3.org/2000/svg}path', group[1].tag)
317+ self.assertEqual("Frame", group[1].xpath('@inkscape:label', namespaces=inkex.NSS)[0])
318+
319+
320+ def test_single_frame_clipped(self):
321+ args = [
322+ '--clip=True'
323+ , '--corner_radius=20'
324+ , '--fill_color=-16777124'
325+ , '--id=rect3006'
326+ , '--position=inside'
327+ , '--stroke_color=255'
328+ , '--tab="stroke"'
329+ , '--width=10'
330+ , 'svg/single_box.svg']
331+ uut = Frame()
332+ uut.affect( args, False )
333+ new_frame = self.get_frame(uut.document)
334+ self.assertIsNotNone(new_frame)
335+ self.assertEqual('{http://www.w3.org/2000/svg}path', new_frame.tag)
336+ group = new_frame.getparent()
337+ self.assertEqual('url(#clipPath)', group[0].get('clip-path'))
338+ clip_path = uut.document.xpath('//svg:defs/svg:clipPath', namespaces=inkex.NSS)[0]
339+ self.assertEqual('{http://www.w3.org/2000/svg}clipPath', clip_path.tag)
340+
341+
342+if __name__ == '__main__':
343+ unittest.main()
344\ No newline at end of file
345
346=== added file 'share/extensions/test/svg/single_box.svg'
347--- share/extensions/test/svg/single_box.svg 1970-01-01 00:00:00 +0000
348+++ share/extensions/test/svg/single_box.svg 2016-05-19 02:06:20 +0000
349@@ -0,0 +1,62 @@
350+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
351+<!-- Created with Inkscape (http://www.inkscape.org/) -->
352+
353+<svg
354+ xmlns:dc="http://purl.org/dc/elements/1.1/"
355+ xmlns:cc="http://creativecommons.org/ns#"
356+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
357+ xmlns:svg="http://www.w3.org/2000/svg"
358+ xmlns="http://www.w3.org/2000/svg"
359+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
360+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
361+ width="744.09448819"
362+ height="1052.3622047"
363+ id="svg2"
364+ version="1.1"
365+ inkscape:version="0.48.3.1 r9886"
366+ sodipodi:docname="New document 1">
367+ <defs
368+ id="defs4" />
369+ <sodipodi:namedview
370+ id="base"
371+ pagecolor="#ffffff"
372+ bordercolor="#666666"
373+ borderopacity="1.0"
374+ inkscape:pageopacity="0.0"
375+ inkscape:pageshadow="2"
376+ inkscape:zoom="0.35"
377+ inkscape:cx="375"
378+ inkscape:cy="514.28571"
379+ inkscape:document-units="px"
380+ inkscape:current-layer="layer1"
381+ showgrid="false"
382+ inkscape:window-width="479"
383+ inkscape:window-height="379"
384+ inkscape:window-x="1319"
385+ inkscape:window-y="75"
386+ inkscape:window-maximized="0" />
387+ <metadata
388+ id="metadata7">
389+ <rdf:RDF>
390+ <cc:Work
391+ rdf:about="">
392+ <dc:format>image/svg+xml</dc:format>
393+ <dc:type
394+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
395+ <dc:title></dc:title>
396+ </cc:Work>
397+ </rdf:RDF>
398+ </metadata>
399+ <g
400+ inkscape:label="Layer 1"
401+ inkscape:groupmode="layer"
402+ id="layer1">
403+ <rect
404+ style="opacity:0.5;fill:#6900ff;fill-opacity:0.36000001;stroke:#cae8ef;stroke-width:7.19999981;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
405+ id="rect3006"
406+ width="237.14285"
407+ height="305.71429"
408+ x="285.71429"
409+ y="406.64789" />
410+ </g>
411+</svg>