contrib/inkscape_tools: make rounding to the next power of 2 optional
[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 MNit framework
96 class MnitImageSetSrc
97 super ImageSetSrc
98
99 redef fun rendering
100 do
101 # Known array of images
102 var arrays_of_images = new Array[String]
103
104 # Attributes of the generated class
105 var attributes = new Array[String]
106
107 # Statements for the generated `load_all` method
108 var load_exprs = new Array[String]
109
110 # Add images to Nit source file
111 for image in images do
112 # Adapt coordinates to new top left and scale
113 var coordinates = document.coordinates(image)
114
115 var nit_name = image.name
116 var last_char = nit_name.chars.last
117 if last_char.to_s.is_numeric then
118 # Array of images
119 # TODO support more than 10 images in an array
120
121 nit_name = nit_name.substring(0, nit_name.length-1)
122 if not arrays_of_images.has(nit_name) then
123 # Create class attribute to store Array
124 arrays_of_images.add(nit_name)
125 attributes.add "\tvar {nit_name} = new Array[Image]\n"
126 end
127 load_exprs.add "\t\t{nit_name}.add(main_image.subimage({coordinates}))\n"
128 else
129 # Single image
130 attributes.add "\tvar {nit_name}: Image is noinit\n"
131 load_exprs.add "\t\t{nit_name} = main_image.subimage({coordinates})\n"
132 end
133 end
134
135 add """
136 # File generated by svg_to_png_and_nit, do not modify, redef instead
137
138 import mnit::image_set
139
140 class {{{document.nit_class_name}}}
141 super ImageSet
142
143 private var main_image: Image is noinit
144 """
145 add_all attributes
146 add """
147
148 redef fun load_all(app: App)
149 do
150 main_image = app.load_image(\"images/{{{document.drawing_name}}}.png\")
151 """
152 add_all load_exprs
153 add """
154 end
155 end
156 """
157 end
158 end
159
160 # Nit module targeting the Gamnit framework
161 #
162 # Gamnit's `Texture` already manage the lazy loading, no need to do it here.
163 class GamnitImageSetSrc
164 super ImageSetSrc
165
166 private fun attributes: Array[String]
167 do
168 # Separate the images from the arrays of images
169 var single_images = new Array[Image]
170 var arrays_of_images = new HashMap[String, Array[Image]]
171
172 for image in images do
173 var nit_name = image.name
174 var last_char = nit_name.chars.last
175 if last_char.to_s.is_numeric then
176
177 # Is an array
178 nit_name = nit_name.substring(0, nit_name.length-1)
179 if not arrays_of_images.keys.has(nit_name) then
180 # Create a new array
181 var array = new Array[Image]
182 arrays_of_images[nit_name] = array
183 end
184
185 arrays_of_images[nit_name].add image
186 else
187 # Is a single image
188 single_images.add image
189 end
190 end
191
192 # Attributes of the class
193 var attributes = new Array[String]
194 attributes.add "\tprivate var main_image = new Texture(\"images/{document.drawing_name}.png\")\n"
195
196 # Add single images to Nit source file
197 for image in single_images do
198 # Adapt coordinates to new top left and scale
199 var coordinates = document.coordinates(image)
200
201 attributes.add "\tvar {image.name}: Texture = main_image.subtexture({coordinates})\n"
202 end
203
204 # Add array of images too
205 for name, images in arrays_of_images do
206
207 var lines = new Array[String]
208 for image in images do
209 var coordinates = document.coordinates(image)
210 lines.add "\t\tmain_image.subtexture({coordinates})"
211 end
212
213 attributes.add """
214 var {{{name}}} = new Array[Texture].with_items(
215 {{{lines.join(",\n")}}})
216 """
217 end
218
219 return attributes
220 end
221
222 redef fun rendering
223 do
224 add """
225 # File generated by svg_to_png_and_nit, do not modify, redef instead
226
227 import gamnit::display
228
229 class {{{document.nit_class_name}}}
230
231 """
232 add_all attributes
233 add """
234 end
235 """
236 end
237 end
238
239 redef class Int
240 # Magic adaption of this coordinates to the given `margin` and `scale`
241 fun adapt(margin: Int, scale: Float): Int
242 do
243 var corrected = self-margin
244 return (corrected.to_f*scale).to_i
245 end
246
247 # The first power of to equal or greater than `self`
248 fun next_pow2: Int
249 do
250 var p = 2
251 while p < self do p = p*2
252 return p
253 end
254 end
255
256 var opt_out_src = new OptionString("Path to output source file (folder or file)", "--src", "-s")
257 var opt_assets = new OptionString("Path to assert dir where to put PNG files", "--assets", "-a")
258 var opt_scale = new OptionFloat("Apply scaling to exported images (default at 1.0 of 90dpi)", 1.0, "--scale", "-x")
259 var opt_gamnit = new OptionBool("Target the Gamnit framework (by default it targets Mnit)", "--gamnit", "-g")
260 var opt_pow2 = new OptionBool("Round the image size to the next power of 2", "--pow2")
261 var opt_help = new OptionBool("Print this help message", "--help", "-h")
262
263 var opt_context = new OptionContext
264 opt_context.add_option(opt_out_src, opt_assets, opt_scale, opt_gamnit, opt_pow2, opt_help)
265
266 opt_context.parse(args)
267 var rest = opt_context.rest
268 var errors = opt_context.errors
269 if rest.is_empty and not opt_help.value then errors.add "You must specify at least one source drawing file"
270 if not errors.is_empty or opt_help.value then
271 print errors.join("\n")
272 print "Usage: svg_to_png_and_nit [Options] drawing.svg [Other files]"
273 print "Options:"
274 opt_context.usage
275 exit 1
276 end
277
278 if not "inkscape".program_is_in_path then
279 print "This tool needs the external program `inkscape`, make sure it is installed and in your PATH."
280 exit 1
281 end
282
283 var drawings = rest
284 for drawing in drawings do
285 if not drawing.file_exists then
286 stderr.write "Source drawing file '{drawing}' does not exist."
287 exit 1
288 end
289 end
290
291 var assets_path = opt_assets.value
292 if assets_path == null then assets_path = "assets"
293 if not assets_path.file_exists then
294 stderr.write "Assets dir '{assets_path}' does not exist (use --assets)\n"
295 exit 1
296 end
297
298 var src_path = opt_out_src.value
299 if src_path == null then src_path = "src"
300 if not src_path.file_exists and src_path.file_extension != "nit" then
301 stderr.write "Source dir '{src_path}' does not exist (use --src)\n"
302 exit 1
303 end
304
305 var scale = opt_scale.value
306
307 for drawing in drawings do
308 var drawing_name = drawing.basename(".svg")
309
310 # Get the page dimensions
311 # Inkscape doesn't give us this information
312 var page_width = -1
313 var page_height = -1
314 var svg_file = new FileReader.open(drawing)
315 while not svg_file.eof do
316 var line = svg_file.read_line
317
318 if page_width == -1 and line.search("width") != null then
319 var words = line.split("=")
320 var n = words[1]
321 n = n.substring(1, n.length-2) # remove ""
322 page_width = n.to_f.ceil.to_i
323 else if page_height == -1 and line.search("height") != null then
324 var words = line.split("=")
325 var n = words[1]
326 n = n.substring(1, n.length-2) # remove ""
327 page_height = n.to_f.ceil.to_i
328 end
329 end
330 svg_file.close
331
332 assert page_width != -1
333 assert page_height != -1
334
335 # Query Inkscape
336 var prog = "inkscape"
337 var proc = new ProcessReader.from_a(prog, ["--without-gui", "--query-all", drawing])
338
339 var min_x = 1000000
340 var min_y = 1000000
341 var max_x = -1
342 var max_y = -1
343 var images = new Array[Image]
344
345 # Gather all images beginning with 0
346 # also get the bounding box of all images
347 while not proc.eof do
348 var line = proc.read_line
349 var words = line.split(",")
350
351 if words.length == 5 then
352 var id = words[0]
353
354 var x = words[1].to_f.floor.to_i
355 var y = words[2].to_f.floor.to_i
356 var w = words[3].to_f.ceil.to_i+1
357 var h = words[4].to_f.ceil.to_i+1
358
359 if id.has_prefix("0") then
360 var nit_name = id.substring_from(1)
361 nit_name = nit_name.replace('-', "_")
362
363 var image = new Image(nit_name, x, y, w, h)
364 min_x = min_x.min(x)
365 min_y = min_y.min(y)
366 max_x = max_x.max(image.right)
367 max_y = max_y.max(image.bottom)
368
369 images.add image
370 end
371 end
372 end
373 proc.close
374
375
376 # Sort images by name, it prevents Array errors and looks better
377 alpha_comparator.sort(images)
378
379 var document = new Document(drawing_name, scale, min_x, max_x, min_y, max_y)
380
381 # Nit class
382 var nit_src: ImageSetSrc
383 if opt_gamnit.value then
384 nit_src = new GamnitImageSetSrc(document, images)
385 else
386 nit_src = new MnitImageSetSrc(document, images)
387 end
388
389 if not src_path.file_extension == "nit" then
390 src_path = src_path/drawing_name+".nit"
391 end
392
393 # Output source file
394 var src_file = new FileWriter.open(src_path)
395 nit_src.write_to(src_file)
396 src_file.close
397
398 # Find next power of 2
399 if opt_pow2.value then
400 var dx = max_x - min_x
401 max_x = dx.next_pow2 + min_x
402
403 var dy = max_y - min_y
404 max_y = dy.next_pow2 + min_y
405 end
406
407 # Inkscape's --export-area inverts the Y axis. It uses the lower left corner of
408 # the drawing area where as queries return coordinates from the top left.
409 var y0 = page_height - max_y
410 var y1 = page_height - min_y
411
412 # Output png file to assets
413 var png_path = "{assets_path}/images/{drawing_name}.png"
414 var proc2 = new Process.from_a(prog, [drawing, "--without-gui",
415 "--export-dpi={(90.0*scale).to_i}",
416 "--export-png={png_path}",
417 "--export-area={min_x}:{y0}:{max_x}:{y1}",
418 "--export-background=#000000", "--export-background-opacity=0.0"])
419 proc2.wait
420 end