inkscape_tools: fix Inkscape version detection on Ubuntu
[nit.git] / contrib / inkscape_tools / src / svg_to_png_and_nit.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2012-2015 Alexis Laferrière <alexis.laf@xymus.net>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Extract images of objects from an SVG file using Inkscape
18 module svg_to_png_and_nit
19
20 import opts
21 import template
22
23 # Image information extracted from the SVG file
24 class Image
25 # Name extracted from the object ID minus the `0` prefix and Nit safe
26 var name: String
27
28 # Left border
29 var x: Int
30
31 # Top border
32 var y: Int
33
34 # Image width
35 var w: Int
36
37 # Image height
38 var h: Int
39
40 # Right border
41 fun right: Int do return x+w
42
43 # Bottom border
44 fun bottom: Int do return y+h
45
46 redef fun to_s do return name
47 end
48
49 # Document being processed, concerns both the source and the target
50 class Document
51 # Name of the source file
52 var drawing_name: String
53
54 # Name of the class to generate
55 var nit_class_name: String = drawing_name.capitalized + "Images" is lazy
56
57 # Scaling to apply to the exported image
58 var scale: Float
59
60 # Source minimum X
61 var min_x: Int
62
63 # Source maximum X
64 var max_x: Int
65
66 # Source minimum Y
67 var min_y: Int
68
69 # Source maximum Y
70 var max_y: Int
71
72 # Get the coordinates for `image` as `"x, y, w, h"`
73 fun coordinates(image: Image): String
74 do
75 var x = image.x.adapt(min_x, scale)
76 var y = image.y.adapt(min_y, scale)
77 var w = (image.w.to_f*scale).to_i
78 var h = (image.h.to_f*scale).to_i
79
80 return "{x}, {y}, {w}, {h}"
81 end
82 end
83
84 # Nit module with a single class to retrieve to access the extracted images
85 abstract class ImageSetSrc
86 super Template
87
88 # Target document
89 var document: Document
90
91 # Images found in the source document
92 var images: Array[Image]
93 end
94
95 # Nit module targeting the Gamnit framework
96 #
97 # Gamnit's `Texture` already manage the lazy loading, no need to do it here.
98 class GamnitImageSetSrc
99 super ImageSetSrc
100
101 private fun attributes: Array[String]
102 do
103 # Separate the images from the arrays of images
104 var single_images = new Array[Image]
105 var arrays_of_images = new HashMap[String, Array[Image]]
106
107 for image in images do
108 var nit_name = image.name
109 var last_char = nit_name.chars.last
110 if last_char.to_s.is_numeric then
111
112 # Is an array
113 nit_name = nit_name.substring(0, nit_name.length-1)
114 if not arrays_of_images.keys.has(nit_name) then
115 # Create a new array
116 var array = new Array[Image]
117 arrays_of_images[nit_name] = array
118 end
119
120 arrays_of_images[nit_name].add image
121 else
122 # Is a single image
123 single_images.add image
124 end
125 end
126
127 # Attributes of the class
128 var attributes = new Array[String]
129 attributes.add "\tprivate var root_texture = new Texture(\"images/{document.drawing_name}.png\")\n"
130
131 # Add single images to Nit source file
132 for image in single_images do
133 # Adapt coordinates to new top left and scale
134 var coordinates = document.coordinates(image)
135
136 attributes.add "\tvar {image.name}: Texture = root_texture.subtexture({coordinates})\n"
137 end
138
139 # Add array of images too
140 for name, images in arrays_of_images do
141
142 var lines = new Array[String]
143 for image in images do
144 var coordinates = document.coordinates(image)
145 lines.add "\t\troot_texture.subtexture({coordinates})"
146 end
147
148 attributes.add """
149 var {{{name}}} = new Array[Texture].with_items(
150 {{{lines.join(",\n")}}})
151 """
152 end
153
154 return attributes
155 end
156
157 redef fun rendering
158 do
159 add """
160 # File generated by svg_to_png_and_nit, do not modify, redef instead
161
162 import gamnit::textures
163
164 class {{{document.nit_class_name}}}
165
166 """
167 add_all attributes
168 add """
169 end
170 """
171 end
172 end
173
174 redef class Int
175 # Magic adaption of this coordinates to the given `margin` and `scale`
176 fun adapt(margin: Int, scale: Float): Int
177 do
178 var corrected = self-margin
179 return (corrected.to_f*scale).to_i
180 end
181
182 # The first power of to equal or greater than `self`
183 fun next_pow2: Int
184 do
185 var p = 2
186 while p < self do p = p*2
187 return p
188 end
189 end
190
191 var opt_out_src = new OptionString("Path to output source file (folder or file)", "--src", "-s")
192 var opt_assets = new OptionString("Path to assert dir where to put PNG files", "--assets", "-a")
193 var opt_scale = new OptionFloat("Apply scaling to exported images (default at 1.0)", 1.0, "--scale", "-x")
194 var opt_gamnit = new OptionBool("Target the Gamnit framework (the default, kept for compatibility)", "--gamnit", "-g")
195 var opt_pow2 = new OptionBool("Round the image size to the next power of 2", "--pow2")
196 var opt_help = new OptionBool("Print this help message", "--help", "-h")
197
198 var opt_context = new OptionContext
199 opt_context.add_option(opt_out_src, opt_assets, opt_scale, opt_gamnit, opt_pow2, opt_help)
200
201 opt_context.parse(args)
202 var rest = opt_context.rest
203 var errors = opt_context.errors
204 if rest.is_empty and not opt_help.value then errors.add "You must specify at least one source drawing file"
205 if not errors.is_empty or opt_help.value then
206 print errors.join("\n")
207 print "Usage: svg_to_png_and_nit [Options] drawing.svg [Other files]"
208 print "Options:"
209 opt_context.usage
210 exit 1
211 end
212
213 if not "inkscape".program_is_in_path then
214 print "This tool needs the external program `inkscape`, make sure it is installed and in your PATH."
215 exit 1
216 end
217
218 # Get the inkscape version
219 var p = new ProcessReader("inkscape", "--version")
220 var version_string = p.read_all
221 p.wait
222 p.close
223 var version_re = "([0-9]+)\\.([0-9]+)".to_re
224 var match = version_string.search(version_re)
225 assert match != null
226 var major = match[1]
227 var minor = match[2]
228 assert major != null and minor != null
229
230 # Set the default API using the version as heuristic
231 var default_dpi = 96.0
232 if major.to_s.to_i == 0 and minor.to_s.to_i < 92 then default_dpi = 90.0
233
234 # Collect source files
235 var drawings = rest
236 for drawing in drawings do
237 if not drawing.file_exists then
238 stderr.write "Source drawing file '{drawing}' does not exist."
239 exit 1
240 end
241 end
242
243 var assets_path = opt_assets.value
244 if assets_path == null then assets_path = "assets"
245 if not assets_path.file_exists then
246 stderr.write "Assets dir '{assets_path}' does not exist (use --assets)\n"
247 exit 1
248 end
249
250 var src_path = opt_out_src.value
251 if src_path == null then src_path = "src"
252 if not src_path.file_exists and src_path.file_extension != "nit" then
253 stderr.write "Source dir '{src_path}' does not exist (use --src)\n"
254 exit 1
255 end
256
257 var scale = opt_scale.value
258
259 for drawing in drawings do
260 var drawing_name = drawing.basename(".svg")
261
262 # Get the page dimensions
263 # Inkscape doesn't give us this information
264 var page_width = -1
265 var page_height = -1
266 var svg_file = new FileReader.open(drawing)
267 while not svg_file.eof do
268 var line = svg_file.read_line
269
270 if page_width == -1 and line.search("width") != null then
271 var words = line.split("=")
272 var n = words[1]
273 n = n.substring(1, n.length-2) # remove ""
274 page_width = n.to_f.ceil.to_i
275 else if page_height == -1 and line.search("height") != null then
276 var words = line.split("=")
277 var n = words[1]
278 n = n.substring(1, n.length-2) # remove ""
279 page_height = n.to_f.ceil.to_i
280 end
281 end
282 svg_file.close
283
284 if page_width == -1 or page_height == -1 then
285 stderr.write "Source drawing file '{drawing}' doesn't look like an SVG file\n"
286 exit 1
287 end
288
289 # Query Inkscape
290 var prog = "inkscape"
291 var proc = new ProcessReader.from_a(prog, ["--without-gui", "--query-all", drawing])
292
293 var min_x = 1000000
294 var min_y = 1000000
295 var max_x = -1
296 var max_y = -1
297 var images = new Array[Image]
298
299 # Gather all images beginning with 0
300 # also get the bounding box of all images
301 while not proc.eof do
302 var line = proc.read_line
303 var words = line.split(",")
304
305 if words.length == 5 then
306 var id = words[0]
307
308 var x = words[1].to_f.floor.to_i
309 var y = words[2].to_f.floor.to_i
310 var w = words[3].to_f.ceil.to_i+1
311 var h = words[4].to_f.ceil.to_i+1
312
313 if id.has_prefix("0") then
314 var nit_name = id.substring_from(1)
315 nit_name = nit_name.replace('-', "_")
316
317 var image = new Image(nit_name, x, y, w, h)
318 min_x = min_x.min(x)
319 min_y = min_y.min(y)
320 max_x = max_x.max(image.right)
321 max_y = max_y.max(image.bottom)
322
323 images.add image
324 end
325 end
326 end
327 proc.close
328
329
330 # Sort images by name, it prevents Array errors and looks better
331 alpha_comparator.sort(images)
332
333 var document = new Document(drawing_name, scale, min_x, max_x, min_y, max_y)
334
335 # Nit class
336 var nit_src = new GamnitImageSetSrc(document, images)
337
338 if not src_path.file_extension == "nit" then
339 src_path = src_path/drawing_name+".nit"
340 end
341
342 # Output source file
343 var src_file = new FileWriter.open(src_path)
344 nit_src.write_to(src_file)
345 src_file.close
346
347 # Find next power of 2
348 if opt_pow2.value then
349 var dx = max_x - min_x
350 max_x = dx.next_pow2 + min_x
351
352 var dy = max_y - min_y
353 max_y = dy.next_pow2 + min_y
354 end
355
356 # Inkscape's --export-area inverts the Y axis. It uses the lower left corner of
357 # the drawing area where as queries return coordinates from the top left.
358 var y0 = page_height - max_y
359 var y1 = page_height - min_y
360
361 # Output png file to assets
362 var png_path = "{assets_path}/images/{drawing_name}.png"
363 var proc2 = new Process.from_a(prog, [drawing, "--without-gui",
364 "--export-dpi={(default_dpi*scale).to_i}",
365 "--export-png={png_path}",
366 "--export-area={min_x}:{y0}:{max_x}:{y1}",
367 "--export-background=#000000", "--export-background-opacity=0.0"])
368 proc2.wait
369 end