1 #!/usr/bin/env python
2 '''
3 guillotine.py
5 Copyright (C) 2010 Craig Marshall, craig9 [at] gmail.com
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 -----------------------
23 This script slices an inkscape drawing along the guides, similarly to
24 the GIMP plugin called "guillotine". It can optionally export to the
25 same directory as the SVG file with the same name, but with a number
26 suffix. e.g.
28 /home/foo/drawing.svg
30 will export to:
32 /home/foo/drawing0.png
33 /home/foo/drawing1.png
34 /home/foo/drawing2.png
35 /home/foo/drawing3.png
37 etc.
39 '''
41 import os
42 import sys
43 import inkex
44 import simplestyle
45 import locale
46 import gettext
47 _ = gettext.gettext
49 locale.setlocale(locale.LC_ALL, '')
51 try:
52 from subprocess import Popen, PIPE
53 bsubprocess = True
54 except:
55 bsubprocess = False
57 def float_sort(a, b):
58 '''
59 This is used to sort the horizontal and vertical guide positions,
60 which are floating point numbers, but which are held as text.
61 '''
62 return cmp(float(a), float(b))
64 class Guillotine(inkex.Effect):
65 """Exports slices made using guides"""
66 def __init__(self):
67 inkex.Effect.__init__(self)
68 self.OptionParser.add_option("--directory", action="store",
69 type="string", dest="directory",
70 default=None, help="")
72 self.OptionParser.add_option("--image", action="store",
73 type="string", dest="image",
74 default=None, help="")
76 self.OptionParser.add_option("--ignore", action="store",
77 type="inkbool", dest="ignore",
78 default=None, help="")
80 def get_guides(self):
81 '''
82 Returns all guide elements as an iterable collection
83 '''
84 root = self.document.getroot()
85 guides = []
86 xpath = self.document.xpath("//sodipodi:guide",
87 namespaces=inkex.NSS)
88 for g in xpath:
89 guide = {}
90 (x, y) = g.attrib['position'].split(',')
91 if g.attrib['orientation'] == '0,1':
92 guide['orientation'] = 'horizontal'
93 guide['position'] = y
94 guides.append(guide)
95 elif g.attrib['orientation'] == '1,0':
96 guide['orientation'] = 'vertical'
97 guide['position'] = x
98 guides.append(guide)
99 return guides
101 def get_all_horizontal_guides(self):
102 '''
103 Returns all horizontal guides as a list of floats stored as
104 strings. Each value is the position from 0 in pixels.
105 '''
106 guides = []
107 for g in self.get_guides():
108 if g['orientation'] == 'horizontal':
109 guides.append(g['position'])
110 return guides
112 def get_all_vertical_guides(self):
113 '''
114 Returns all vertical guides as a list of floats stored as
115 strings. Each value is the position from 0 in pixels.
116 '''
117 guides = []
118 for g in self.get_guides():
119 if g['orientation'] == 'vertical':
120 guides.append(g['position'])
121 return guides
123 def get_horizontal_slice_positions(self):
124 '''
125 Make a sorted list of all horizontal guide positions,
126 including 0 and the document height, but not including
127 those outside of the canvas
128 '''
129 root = self.document.getroot()
130 horizontals = ['0']
131 height = inkex.unittouu(root.attrib['height'])
132 for h in self.get_all_horizontal_guides():
133 if h >= 0 and float(h) <= float(height):
134 horizontals.append(h)
135 horizontals.append(height)
136 horizontals.sort(cmp=float_sort)
137 return horizontals
139 def get_vertical_slice_positions(self):
140 '''
141 Make a sorted list of all vertical guide positions,
142 including 0 and the document width, but not including
143 those outside of the canvas.
144 '''
145 root = self.document.getroot()
146 verticals = ['0']
147 width = inkex.unittouu(root.attrib['width'])
148 for v in self.get_all_vertical_guides():
149 if v >= 0 and float(v) <= float(width):
150 verticals.append(v)
151 verticals.append(width)
152 verticals.sort(cmp=float_sort)
153 return verticals
155 def get_slices(self):
156 '''
157 Returns a list of all "slices" as denoted by the guides
158 on the page. Each slice is really just a 4 element list of
159 floats (stored as strings), consisting of the X and Y start
160 position and the X and Y end position.
161 '''
162 hs = self.get_horizontal_slice_positions()
163 vs = self.get_vertical_slice_positions()
164 slices = []
165 for i in range(len(hs)-1):
166 for j in range(len(vs)-1):
167 slices.append([vs[j], hs[i], vs[j+1], hs[i+1]])
168 return slices
170 def get_filename_parts(self):
171 '''
172 Attempts to get directory and image as passed in by the inkscape
173 dialog. If the boolean ignore flag is set, then it will ignore
174 these settings and try to use the settings from the export
175 filename.
176 '''
178 if self.options.ignore == False:
179 if self.options.image == "" or self.options.image is None:
180 inkex.errormsg("Please enter an image name")
181 sys.exit(0)
182 return (self.options.directory, self.options.image)
183 else:
184 '''
185 First get the export-filename from the document, if the
186 document has been exported before (TODO: Will not work if it
187 hasn't been exported yet), then uses this to return a tuple
188 consisting of the directory to export to, and the filename
189 without extension.
190 '''
191 svg = self.document.getroot()
192 att = '{http://www.inkscape.org/namespaces/inkscape}export-filename'
193 try:
194 export_file = svg.attrib[att]
195 except KeyError:
196 inkex.errormsg("To use the export hints option, you " +
197 "need to have previously exported the document. " +
198 "Otherwise no export hints exist!")
199 sys.exit(-1)
200 dirname, filename = os.path.split(export_file)
201 filename = filename.rsplit(".", 1)[0] # Without extension
202 return (dirname, filename)
204 def check_dir_exists(self, dir):
205 if not os.path.isdir(dir):
206 os.makedirs(dir)
208 def get_localised_string(self, str):
209 return locale.format("%.f", float(str), 0)
211 def export_slice(self, s, filename):
212 '''
213 Runs inkscape's command line interface and exports the image
214 slice from the 4 coordinates in s, and saves as the filename
215 given.
216 '''
217 svg_file = self.args[-1]
218 command = "inkscape -a %s:%s:%s:%s -e \"%s\" \"%s\" " % (self.get_localised_string(s[0]), self.get_localised_string(s[1]), self.get_localised_string(s[2]), self.get_localised_string(s[3]), filename, svg_file)
219 if bsubprocess:
220 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
221 return_code = p.wait()
222 f = p.stdout
223 err = p.stderr
224 else:
225 _, f, err = os.open3(command)
226 f.close()
228 def export_slices(self, slices):
229 '''
230 Takes the slices list and passes each one with a calculated
231 filename/directory into export_slice.
232 '''
233 dirname, filename = self.get_filename_parts()
234 output_files = list()
235 if dirname == '' or dirname == None:
236 dirname = './'
238 dirname = os.path.expanduser(dirname)
239 dirname = os.path.expandvars(dirname)
240 dirname = os.path.abspath(dirname)
241 if dirname[-1] != os.path.sep:
242 dirname += os.path.sep
243 self.check_dir_exists(dirname)
244 i = 0
245 for s in slices:
246 f = dirname + filename + str(i) + ".png"
247 output_files.append(f)
248 self.export_slice(s, f)
249 i += 1
250 inkex.errormsg(_("The sliced bitmaps have been saved as:") + "\n\n" + "\n".join(output_files))
252 def effect(self):
253 slices = self.get_slices()
254 self.export_slices(slices)
256 if __name__ == "__main__":
257 e = Guillotine()
258 e.affect()