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 Gamnit framework
97 # Gamnit's `Texture` already manage the lazy loading, no need to do it here.
98 class GamnitImageSetSrc
101 private fun attributes
: Array[String]
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]]
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
113 nit_name
= nit_name
.substring
(0, nit_name
.length-1
)
114 if not arrays_of_images
.keys
.has
(nit_name
) then
116 var array
= new Array[Image]
117 arrays_of_images
[nit_name
] = array
120 arrays_of_images
[nit_name
].add image
123 single_images
.add image
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"
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
)
136 attributes
.add
"\tvar {image.name}: Texture = root_texture.subtexture({coordinates})\n"
139 # Add array of images too
140 for name
, images
in arrays_of_images
do
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})"
149 var {{{name}}} = new Array[Texture].with_items(
150 {{{lines.join(",\n")}}})
160 # File generated by svg_to_png_and_nit, do not modify, redef instead
162 import gamnit::textures
164 class {{{document.nit_class_name}}}
175 # Magic adaption of this coordinates to the given `margin` and `scale`
176 fun adapt
(margin
: Int, scale
: Float): Int
178 var corrected
= self-margin
179 return (corrected
.to_f
*scale
).to_i
182 # The first power of to equal or greater than `self`
186 while p
< self do p
= p
*2
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")
198 var opt_context
= new OptionContext
199 opt_context
.add_option
(opt_out_src
, opt_assets
, opt_scale
, opt_gamnit
, opt_pow2
, opt_help
)
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]"
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."
218 # Get the inkscape version
219 var p
= new ProcessReader("inkscape", "--version")
220 var version_string
= p
.read_all
223 var version_re
= "([0-9]+)\\.([0-9]+)".to_re
224 var match
= version_string
.search
(version_re
)
228 assert major
!= null and minor
!= null
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
234 # Collect source files
236 for drawing
in drawings
do
237 if not drawing
.file_exists
then
238 stderr
.write
"Source drawing file '{drawing}' does not exist."
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"
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"
257 var scale
= opt_scale
.value
259 for drawing
in drawings
do
260 var drawing_name
= drawing
.basename
(".svg")
262 # Get the page dimensions
263 # Inkscape doesn't give us this information
266 var svg_file
= new FileReader.open
(drawing
)
267 while not svg_file
.eof
do
268 var line
= svg_file
.read_line
270 if page_width
== -1 and line
.search
("width") != null then
271 var words
= line
.split
("=")
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
("=")
278 n
= n
.substring
(1, n
.length-2
) # remove ""
279 page_height
= n
.to_f
.ceil
.to_i
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"
290 var prog
= "inkscape"
291 var proc
= new ProcessReader.from_a
(prog
, ["--without-gui", "--query-all", drawing
])
297 var images
= new Array[Image]
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
(",")
305 if words
.length
== 5 then
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
313 if id
.has_prefix
("0") then
314 var nit_name
= id
.substring_from
(1)
315 nit_name
= nit_name
.replace
('-', "_")
317 var image
= new Image(nit_name
, x
, y
, w
, h
)
320 max_x
= max_x
.max
(image
.right
)
321 max_y
= max_y
.max
(image
.bottom
)
330 # Sort images by name, it prevents Array errors and looks better
331 alpha_comparator
.sort
(images
)
333 var document
= new Document(drawing_name
, scale
, min_x
, max_x
, min_y
, max_y
)
336 var nit_src
= new GamnitImageSetSrc(document
, images
)
338 if not src_path
.file_extension
== "nit" then
339 src_path
= src_path
/drawing_name
+".nit"
343 var src_file
= new FileWriter.open
(src_path
)
344 nit_src
.write_to
(src_file
)
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
352 var dy
= max_y
- min_y
353 max_y
= dy
.next_pow2
+ min_y
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
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"])