80ba5c44abc3e3a06987499d05a1a2432a5e6a7b
1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2012-2015 Alexis Laferrière <alexis.laf@xymus.net>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 # Extract images of objects from an SVG file using Inkscape
18 module svg_to_png_and_nit
23 # Image information extracted from the SVG file
25 # Name extracted from the object ID minus the `0` prefix and Nit safe
41 fun right
: Int do return x
+w
44 fun bottom
: Int do return y
+h
46 redef fun to_s
do return name
49 # Document being processed, concerns both the source and the target
51 # Name of the source file
52 var drawing_name
: String
54 # Name of the class to generate
55 var nit_class_name
: String = drawing_name
.capitalized
+ "Images" is lazy
57 # Scaling to apply to the exported image
72 # Get the coordinates for `image` as `"x, y, w, h"`
73 fun coordinates
(image
: Image): String
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
80 return "{x}, {y}, {w}, {h}"
84 # Nit module with a single class to retrieve to access the extracted images
85 abstract class ImageSetSrc
89 var document
: Document
91 # Images found in the source document
92 var images
: Array[Image]
95 # Nit module targeting the MNit framework
101 # Known array of images
102 var arrays_of_images
= new Array[String]
104 # Attributes of the generated class
105 var attributes
= new Array[String]
107 # Statements for the generated `load_all` method
108 var load_exprs
= new Array[String]
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
)
115 var nit_name
= image
.name
116 var last_char
= nit_name
.chars
.last
117 if last_char
.to_s
.is_numeric
then
119 # TODO support more than 10 images in an array
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"
127 load_exprs
.add
"\t\t{nit_name}.add(main_image.subimage({coordinates}))\n"
130 attributes
.add
"\tvar {nit_name}: Image is noinit\n"
131 load_exprs
.add
"\t\t{nit_name} = main_image.subimage({coordinates})\n"
136 # File generated by svg_to_png_and_nit, do not modify, redef instead
138 import mnit::image_set
140 class {{{document.nit_class_name}}}
143 private var main_image: Image is noinit
148 redef fun load_all(app: App)
150 main_image = app.load_image(\"images/{{{document.drawing_name}}}.png\")
160 # Nit module targeting the Gamnit framework
162 # Gamnit's `Texture` already manage the lazy loading, no need to do it here.
163 class GamnitImageSetSrc
166 private fun attributes
: Array[String]
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]]
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
178 nit_name
= nit_name
.substring
(0, nit_name
.length-1
)
179 if not arrays_of_images
.keys
.has
(nit_name
) then
181 var array
= new Array[Image]
182 arrays_of_images
[nit_name
] = array
185 arrays_of_images
[nit_name
].add image
188 single_images
.add image
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"
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
)
201 attributes
.add
"\tvar {image.name}: Texture = main_image.subtexture({coordinates})\n"
204 # Add array of images too
205 for name
, images
in arrays_of_images
do
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})"
214 var {{{name}}} = new Array[Texture].with_items(
215 {{{lines.join(",\n")}}})
225 # File generated by svg_to_png_and_nit, do not modify, redef instead
227 import gamnit::display
229 class {{{document.nit_class_name}}}
240 # Magic adaption of this coordinates to the given `margin` and `scale`
241 fun adapt
(margin
: Int, scale
: Float): Int
243 var corrected
= self-margin
244 return (corrected
.to_f
*scale
).to_i
247 # The first power of to equal or greater than `self`
251 while p
< self do p
= p
*2
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_help
= new OptionBool("Print this help message", "--help", "-h")
262 var opt_context
= new OptionContext
263 opt_context
.add_option
(opt_out_src
, opt_assets
, opt_scale
, opt_gamnit
, opt_help
)
265 opt_context
.parse
(args
)
266 var rest
= opt_context
.rest
267 var errors
= opt_context
.errors
268 if rest
.is_empty
and not opt_help
.value
then errors
.add
"You must specify at least one source drawing file"
269 if not errors
.is_empty
or opt_help
.value
then
270 print errors
.join
("\n")
271 print
"Usage: svg_to_png_and_nit [Options] drawing.svg [Other files]"
277 if not "inkscape".program_is_in_path
then
278 print
"This tool needs the external program `inkscape`, make sure it is installed and in your PATH."
283 for drawing
in drawings
do
284 if not drawing
.file_exists
then
285 stderr
.write
"Source drawing file '{drawing}' does not exist."
290 var assets_path
= opt_assets
.value
291 if assets_path
== null then assets_path
= "assets"
292 if not assets_path
.file_exists
then
293 stderr
.write
"Assets dir '{assets_path}' does not exist (use --assets)\n"
297 var src_path
= opt_out_src
.value
298 if src_path
== null then src_path
= "src"
299 if not src_path
.file_exists
and src_path
.file_extension
!= "nit" then
300 stderr
.write
"Source dir '{src_path}' does not exist (use --src)\n"
304 var scale
= opt_scale
.value
306 for drawing
in drawings
do
307 var drawing_name
= drawing
.basename
(".svg")
309 # Get the page dimensions
310 # Inkscape doesn't give us this information
313 var svg_file
= new FileReader.open
(drawing
)
314 while not svg_file
.eof
do
315 var line
= svg_file
.read_line
317 if page_width
== -1 and line
.search
("width") != null then
318 var words
= line
.split
("=")
320 n
= n
.substring
(1, n
.length-2
) # remove ""
321 page_width
= n
.to_f
.ceil
.to_i
322 else if page_height
== -1 and line
.search
("height") != null then
323 var words
= line
.split
("=")
325 n
= n
.substring
(1, n
.length-2
) # remove ""
326 page_height
= n
.to_f
.ceil
.to_i
331 assert page_width
!= -1
332 assert page_height
!= -1
335 var prog
= "inkscape"
336 var proc
= new ProcessReader.from_a
(prog
, ["--without-gui", "--query-all", drawing
])
342 var images
= new Array[Image]
344 # Gather all images beginning with 0
345 # also get the bounding box of all images
346 while not proc
.eof
do
347 var line
= proc
.read_line
348 var words
= line
.split
(",")
350 if words
.length
== 5 then
353 var x
= words
[1].to_f
.floor
.to_i
354 var y
= words
[2].to_f
.floor
.to_i
355 var w
= words
[3].to_f
.ceil
.to_i
+1
356 var h
= words
[4].to_f
.ceil
.to_i
+1
358 if id
.has_prefix
("0") then
359 var nit_name
= id
.substring_from
(1)
360 nit_name
= nit_name
.replace
('-', "_")
362 var image
= new Image(nit_name
, x
, y
, w
, h
)
365 max_x
= max_x
.max
(image
.right
)
366 max_y
= max_y
.max
(image
.bottom
)
375 # Sort images by name, it prevents Array errors and looks better
376 alpha_comparator
.sort
(images
)
378 var document
= new Document(drawing_name
, scale
, min_x
, max_x
, min_y
, max_y
)
381 var nit_src
: ImageSetSrc
382 if opt_gamnit
.value
then
383 nit_src
= new GamnitImageSetSrc(document
, images
)
385 nit_src
= new MnitImageSetSrc(document
, images
)
388 if not src_path
.file_extension
== "nit" then
389 src_path
= src_path
/drawing_name
+".nit"
393 var src_file
= new FileWriter.open
(src_path
)
394 nit_src
.write_to
(src_file
)
397 # Find closest power of 2
398 var dx
= max_x
- min_x
399 max_x
= dx
.next_pow2
+ min_x
401 var dy
= max_y
- min_y
402 max_y
= dy
.next_pow2
+ min_y
404 # Inkscape's --export-area inverts the Y axis. It uses the lower left corner of
405 # the drawing area where as queries return coordinates from the top left.
406 var y0
= page_height
- max_y
407 var y1
= page_height
- min_y
409 # Output png file to assets
410 var png_path
= "{assets_path}/images/{drawing_name}.png"
411 var proc2
= new Process.from_a
(prog
, [drawing
, "--without-gui",
412 "--export-dpi={(90.0*scale).to_i}",
413 "--export-png={png_path}",
414 "--export-area={min_x}:{y0}:{max_x}:{y1}",
415 "--export-background=#000000", "--export-background-opacity=0.0"])