Merge lp:~rwhite8282/inkscape/frame-extension into lp:~inkscape.dev/inkscape/trunk
- frame-extension
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Owens | code and direction | Approve | |
Review via email: mp+287379@code.launchpad.net |
Commit message
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 : | # |
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> |
This is a solid extension. The python looks good, the inx looks good and the tests are much appreciated.