contrib/inkscape_tools: revamp doc of svg_to_png_and_nit
[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 info 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 # Nit module with a single class to retrieve to access the extracted images
50 class ImageSetSrc
51 super Template
52
53 var name: String
54
55 var attributes = new Array[String]
56 var load_exprs = new Array[String]
57
58 redef fun rendering
59 do
60 add """
61 # File generated by svg_to_png_and_nit, do not modify, redef instead
62
63 import mnit::image_set
64
65 class {{{name}}}
66 super ImageSet
67
68 """
69 add_all attributes
70 add """
71
72 redef fun load_all(app: App)
73 do
74 """
75 add_all load_exprs
76 add """
77 end
78 end
79 """
80 end
81 end
82
83 redef class Int
84 # Magic adaption of this coordinates to the given `margin` and `scale`
85 fun adapt(margin: Int, scale: Float): Int
86 do
87 var corrected = self-margin
88 return (corrected.to_f*scale).to_i
89 end
90
91 # The first power of to equal or greater than `self`
92 fun next_pow2: Int
93 do
94 var p = 2
95 while p < self do p = p*2
96 return p
97 end
98 end
99
100 var opt_out_src = new OptionString("Path to output source file", "--src", "-s")
101 var opt_assets = new OptionString("Path to assert dir where to put PNG files", "--assets", "-a")
102 var opt_scale = new OptionFloat("Apply scaling to exported images (default at 1.0 of 90dpi)", 1.0, "--scale", "-x")
103 var opt_help = new OptionBool("Print this help message", "--help", "-h")
104
105 var opt_context = new OptionContext
106 opt_context.add_option(opt_out_src, opt_assets, opt_scale, opt_help)
107
108 opt_context.parse(args)
109 var rest = opt_context.rest
110 var errors = opt_context.errors
111 if rest.is_empty and not opt_help.value then errors.add "You must specify at least one source drawing file"
112 if not errors.is_empty or opt_help.value then
113 print errors.join("\n")
114 print "Usage: svg_to_png_and_nit [Options] drawing.svg [Other files]"
115 print "Options:"
116 opt_context.usage
117 exit 1
118 end
119
120 if not "inkscape".program_is_in_path then
121 print "This tool needs the external program `inkscape`, make sure it is installed and in your PATH."
122 exit 1
123 end
124
125 var drawings = rest
126 for drawing in drawings do
127 if not drawing.file_exists then
128 stderr.write "Source drawing file '{drawing}' does not exist."
129 exit 1
130 end
131 end
132
133 var assets_path = opt_assets.value
134 if assets_path == null then assets_path = "assets"
135 if not assets_path.file_exists then
136 stderr.write "Assets dir '{assets_path}' does not exist (use --assets)\n"
137 exit 1
138 end
139
140 var src_path = opt_out_src.value
141 if src_path == null then src_path = "src"
142 if not src_path.file_exists then
143 stderr.write "Source dir '{src_path}' does not exist (use --src)\n"
144 exit 1
145 end
146
147 var scale = opt_scale.value
148
149 var arrays_of_images = new Array[String]
150
151 for drawing in drawings do
152 var drawing_name = drawing.basename(".svg")
153
154 # Get the page dimensions
155 # Inkscape doesn't give us this information
156 var page_width = -1
157 var page_height = -1
158 var svg_file = new FileReader.open(drawing)
159 while not svg_file.eof do
160 var line = svg_file.read_line
161
162 if page_width == -1 and line.search("width") != null then
163 var words = line.split("=")
164 var n = words[1]
165 n = n.substring(1, n.length-2) # remove ""
166 page_width = n.to_f.ceil.to_i
167 else if page_height == -1 and line.search("height") != null then
168 var words = line.split("=")
169 var n = words[1]
170 n = n.substring(1, n.length-2) # remove ""
171 page_height = n.to_f.ceil.to_i
172 end
173 end
174 svg_file.close
175
176 assert page_width != -1
177 assert page_height != -1
178
179 # Query Inkscape
180 var prog = "inkscape"
181 var proc = new ProcessReader.from_a(prog, ["--without-gui", "--query-all", drawing])
182
183 var min_x = 1000000
184 var min_y = 1000000
185 var max_x = -1
186 var max_y = -1
187 var images = new Array[Image]
188
189 # Gather all images beginning with 0
190 # also get the bounding box of all images
191 while not proc.eof do
192 var line = proc.read_line
193 var words = line.split(",")
194
195 if words.length == 5 then
196 var id = words[0]
197
198 var x = words[1].to_f.floor.to_i
199 var y = words[2].to_f.floor.to_i
200 var w = words[3].to_f.ceil.to_i+1
201 var h = words[4].to_f.ceil.to_i+1
202
203 if id.has_prefix("0") then
204 var nit_name = id.substring_from(1)
205 nit_name = nit_name.replace('-', "_")
206
207 var image = new Image(nit_name, x, y, w, h)
208 min_x = min_x.min(x)
209 min_y = min_y.min(y)
210 max_x = max_x.max(image.right)
211 max_y = max_y.max(image.bottom)
212
213 images.add image
214 end
215 end
216 end
217 proc.close
218
219 # Nit class
220 var nit_class_name = drawing_name.chars.first.to_s.to_upper + drawing_name.substring_from(1) + "Images"
221 var nit_src = new ImageSetSrc(nit_class_name)
222 nit_src.attributes.add "\tprivate var main_image: Image is noinit\n"
223 nit_src.load_exprs.add "\t\tmain_image = app.load_image(\"images/{drawing_name}.png\")\n"
224
225 # Sort images by name, it prevents Array errors and looks better
226 alpha_comparator.sort(images)
227
228 # Add images to Nit source file
229 for image in images do
230 # Adapt coordinates to new top left and scale
231 var x = image.x.adapt(min_x, scale)
232 var y = image.y.adapt(min_y, scale)
233 var w = (image.w.to_f*scale).to_i
234 var h = (image.h.to_f*scale).to_i
235
236 var nit_name = image.name
237 var last_char = nit_name.chars.last
238 if last_char.to_s.is_numeric then
239 # Array of images
240 # TODO support more than 10 images in an array
241
242 nit_name = nit_name.substring(0, nit_name.length-1)
243 if not arrays_of_images.has(nit_name) then
244 # Create class attribute to store Array
245 arrays_of_images.add(nit_name)
246 nit_src.attributes.add "\tvar {nit_name} = new Array[Image]\n"
247 end
248 nit_src.load_exprs.add "\t\t{nit_name}.add(main_image.subimage({x}, {y}, {w}, {h}))\n"
249 else
250 # Single image
251 nit_src.attributes.add "\tvar {nit_name}: Image is noinit\n"
252 nit_src.load_exprs.add "\t\t{nit_name} = main_image.subimage({x}, {y}, {w}, {h})\n"
253 end
254 end
255
256 # Output source file
257 var src_file = new FileWriter.open("{src_path}/{drawing_name}.nit")
258 nit_src.write_to(src_file)
259 src_file.close
260
261 # Find closest power of 2
262 var dx = max_x - min_x
263 max_x = dx.next_pow2 + min_x
264
265 var dy = max_y - min_y
266 max_y = dy.next_pow2 + min_y
267
268 # Inkscape's --export-area inverts the Y axis. It uses the lower left corner of
269 # the drawing area where as queries return coordinates from the top left.
270 var y0 = page_height - max_y
271 var y1 = page_height - min_y
272
273 # Output png file to assets
274 var png_path = "{assets_path}/images/{drawing_name}.png"
275 var proc2 = new Process.from_a(prog, [drawing, "--without-gui",
276 "--export-dpi={(90.0*scale).to_i}",
277 "--export-png={png_path}",
278 "--export-area={min_x}:{y0}:{max_x}:{y1}",
279 "--export-background=#000000", "--export-background-opacity=0.0"])
280 proc2.wait
281 end