# This file is part of NIT ( http://www.nitlanguage.org ).
#
-# Copyright 2012-2014 Alexis Laferrière <alexis.laf@xymus.net>
+# Copyright 2012-2015 Alexis Laferrière <alexis.laf@xymus.net>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# See the License for the specific language governing permissions and
# limitations under the License.
-# This script extracts pngs from a single svg for all objects with ids
-# beginning by 0. Requires Inkscape.
+# Extract images of objects from an SVG file using Inkscape
module svg_to_png_and_nit
import opts
import template
+# Image information extracted from the SVG file
class Image
+ # Name extracted from the object ID minus the `0` prefix and Nit safe
var name: String
+
+ # Left border
var x: Int
+
+ # Top border
var y: Int
+
+ # Image width
var w: Int
+
+ # Image height
var h: Int
+
+ # Right border
fun right: Int do return x+w
+
+ # Bottom border
fun bottom: Int do return y+h
redef fun to_s do return name
end
-# The Nit source file to retreive all images
-class ImageSetSrc
+# Document being processed, concerns both the source and the target
+class Document
+ # Name of the source file
+ var drawing_name: String
+
+ # Name of the class to generate
+ var nit_class_name: String = drawing_name.capitalized + "Images" is lazy
+
+ # Scaling to apply to the exported image
+ var scale: Float
+
+ # Source minimum X
+ var min_x: Int
+
+ # Source maximum X
+ var max_x: Int
+
+ # Source minimum Y
+ var min_y: Int
+
+ # Source maximum Y
+ var max_y: Int
+
+ # Get the coordinates for `image` as `"x, y, w, h"`
+ fun coordinates(image: Image): String
+ do
+ var x = image.x.adapt(min_x, scale)
+ var y = image.y.adapt(min_y, scale)
+ var w = (image.w.to_f*scale).to_i
+ var h = (image.h.to_f*scale).to_i
+
+ return "{x}, {y}, {w}, {h}"
+ end
+end
+
+# Nit module with a single class to retrieve to access the extracted images
+abstract class ImageSetSrc
super Template
- var name: String
- init(name: String) do self.name = name
+ # Target document
+ var document: Document
- var attributes = new Array[String]
- var load_exprs = new Array[String]
+ # Images found in the source document
+ var images: Array[Image]
+end
- redef fun rendering
+# Nit module targeting the Gamnit framework
+#
+# Gamnit's `Texture` already manage the lazy loading, no need to do it here.
+class GamnitImageSetSrc
+ super ImageSetSrc
+
+ private fun attributes: Array[String]
do
- add """
-# file generated by svg_to_png, do not modify, redef instead
+ # Separate the images from the arrays of images
+ var single_images = new Array[Image]
+ var arrays_of_images = new HashMap[String, Array[Image]]
+
+ for image in images do
+ var nit_name = image.name
+ var last_char = nit_name.chars.last
+ if last_char.to_s.is_numeric then
+
+ # Is an array
+ nit_name = nit_name.substring(0, nit_name.length-1)
+ if not arrays_of_images.keys.has(nit_name) then
+ # Create a new array
+ var array = new Array[Image]
+ arrays_of_images[nit_name] = array
+ end
+
+ arrays_of_images[nit_name].add image
+ else
+ # Is a single image
+ single_images.add image
+ end
+ end
-import mnit::image_set
+ # Attributes of the class
+ var attributes = new Array[String]
+ attributes.add "\tprivate var root_texture = new Texture(\"images/{document.drawing_name}.png\")\n"
-class {{{name}}}
- super ImageSet
+ # Add single images to Nit source file
+ for image in single_images do
+ # Adapt coordinates to new top left and scale
+ var coordinates = document.coordinates(image)
+ attributes.add "\tvar {image.name}: Texture = root_texture.subtexture({coordinates})\n"
+ end
+
+ # Add array of images too
+ for name, images in arrays_of_images do
+
+ var lines = new Array[String]
+ for image in images do
+ var coordinates = document.coordinates(image)
+ lines.add "\t\troot_texture.subtexture({coordinates})"
+ end
+
+ attributes.add """
+ var {{{name}}} = new Array[Texture].with_items(
+{{{lines.join(",\n")}}})
"""
- add_all attributes
- add """
+ end
- redef fun load_all(app: App)
+ return attributes
+ end
+
+ redef fun rendering
do
+ add """
+# File generated by svg_to_png_and_nit, do not modify, redef instead
+
+import gamnit::textures
+
+class {{{document.nit_class_name}}}
+
"""
- add_all load_exprs
+ add_all attributes
add """
- end
end
"""
end
end
redef class Int
- fun adapt(d: Int, scale: Float): Int
+ # Magic adaption of this coordinates to the given `margin` and `scale`
+ fun adapt(margin: Int, scale: Float): Int
do
- var corrected = self-d
+ var corrected = self-margin
return (corrected.to_f*scale).to_i
end
+ # The first power of to equal or greater than `self`
fun next_pow2: Int
do
var p = 2
end
end
-var opt_out_src = new OptionString("Path to output source file", "--src", "-s")
+var opt_out_src = new OptionString("Path to output source file (folder or file)", "--src", "-s")
var opt_assets = new OptionString("Path to assert dir where to put PNG files", "--assets", "-a")
-var opt_scale = new OptionFloat("Apply scaling to exported images (defaut at 1.0 of 90dpi)", 1.0, "--scale", "-x")
+var opt_scale = new OptionFloat("Apply scaling to exported images (default at 1.0)", 1.0, "--scale", "-x")
+var opt_gamnit = new OptionBool("Target the Gamnit framework (the default, kept for compatibility)", "--gamnit", "-g")
+var opt_pow2 = new OptionBool("Round the image size to the next power of 2", "--pow2")
var opt_help = new OptionBool("Print this help message", "--help", "-h")
var opt_context = new OptionContext
-opt_context.add_option(opt_out_src, opt_assets, opt_scale, opt_help)
+opt_context.add_option(opt_out_src, opt_assets, opt_scale, opt_gamnit, opt_pow2, opt_help)
opt_context.parse(args)
var rest = opt_context.rest
exit 1
end
+# Get the inkscape version
+var p = new ProcessReader("inkscape", "--version")
+var version_string = p.read_all
+p.wait
+p.close
+var version_re = "([0-9]+)\\.([0-9]+)".to_re
+var match = version_string.search(version_re)
+assert match != null
+var major = match[1]
+var minor = match[2]
+assert major != null and minor != null
+
+# Set the default API using the version as heuristic
+var default_dpi = 96.0
+if major.to_s.to_i == 0 and minor.to_s.to_i < 92 then default_dpi = 90.0
+
+# Collect source files
var drawings = rest
for drawing in drawings do
if not drawing.file_exists then
var src_path = opt_out_src.value
if src_path == null then src_path = "src"
-if not src_path.file_exists then
+if not src_path.file_exists and src_path.file_extension != "nit" then
stderr.write "Source dir '{src_path}' does not exist (use --src)\n"
exit 1
end
var scale = opt_scale.value
-var arrays_of_images = new Array[String]
-
for drawing in drawings do
var drawing_name = drawing.basename(".svg")
# Inkscape doesn't give us this information
var page_width = -1
var page_height = -1
- var svg_file = new IFStream.open(drawing)
+ var svg_file = new FileReader.open(drawing)
while not svg_file.eof do
var line = svg_file.read_line
end
svg_file.close
- assert page_width != -1
- assert page_height != -1
+ if page_width == -1 or page_height == -1 then
+ stderr.write "Source drawing file '{drawing}' doesn't look like an SVG file\n"
+ exit 1
+ end
# Query Inkscape
var prog = "inkscape"
- var proc = new IProcess.from_a(prog, ["--without-gui", "--query-all", drawing])
+ var proc = new ProcessReader.from_a(prog, ["--without-gui", "--query-all", drawing])
var min_x = 1000000
var min_y = 1000000
while not proc.eof do
var line = proc.read_line
var words = line.split(",")
-
+
if words.length == 5 then
var id = words[0]
end
proc.close
- # Nit class
- var nit_class_name = drawing_name.chars.first.to_s.to_upper + drawing_name.substring_from(1) + "Images"
- var nit_src = new ImageSetSrc(nit_class_name)
- nit_src.attributes.add "\tprivate var main_image: Image\n"
- nit_src.load_exprs.add "\t\tmain_image = app.load_image(\"images/{drawing_name}.png\")\n"
# Sort images by name, it prevents Array errors and looks better
alpha_comparator.sort(images)
- # Add images to Nit source file
- for image in images do
- # Adapt coordinates to new top left and scale
- var x = image.x.adapt(min_x, scale)
- var y = image.y.adapt(min_y, scale)
- var w = (image.w.to_f*scale).to_i
- var h = (image.h.to_f*scale).to_i
+ var document = new Document(drawing_name, scale, min_x, max_x, min_y, max_y)
- var nit_name = image.name
- var last_char = nit_name.chars.last
- if last_char.to_s.is_numeric then
- # Array of images
- # TODO support more than 10 images in an array
-
- nit_name = nit_name.substring(0, nit_name.length-1)
- if not arrays_of_images.has(nit_name) then
- # Create class attribute to store Array
- arrays_of_images.add(nit_name)
- nit_src.attributes.add "\tvar {nit_name} = new Array[Image]\n"
- end
- nit_src.load_exprs.add "\t\t{nit_name}.add(main_image.subimage({x}, {y}, {w}, {h}))\n"
- else
- # Single image
- nit_src.attributes.add "\tvar {nit_name}: Image\n"
- nit_src.load_exprs.add "\t\t{nit_name} = main_image.subimage({x}, {y}, {w}, {h})\n"
- end
+ # Nit class
+ var nit_src = new GamnitImageSetSrc(document, images)
+
+ if not src_path.file_extension == "nit" then
+ src_path = src_path/drawing_name+".nit"
end
# Output source file
- var src_file = new OFStream.open("{src_path}/{drawing_name}.nit")
+ var src_file = new FileWriter.open(src_path)
nit_src.write_to(src_file)
src_file.close
- # Find closest power of 2
- var dx = max_x - min_x
- max_x = dx.next_pow2 + min_x
+ # Find next power of 2
+ if opt_pow2.value then
+ var dx = max_x - min_x
+ max_x = dx.next_pow2 + min_x
- var dy = max_y - min_y
- max_y = dy.next_pow2 + min_y
+ var dy = max_y - min_y
+ max_y = dy.next_pow2 + min_y
+ end
# Inkscape's --export-area inverts the Y axis. It uses the lower left corner of
# the drawing area where as queries return coordinates from the top left.
# Output png file to assets
var png_path = "{assets_path}/images/{drawing_name}.png"
var proc2 = new Process.from_a(prog, [drawing, "--without-gui",
- "--export-dpi={(90.0*scale).to_i}",
+ "--export-dpi={(default_dpi*scale).to_i}",
"--export-png={png_path}",
"--export-area={min_x}:{y0}:{max_x}:{y1}",
"--export-background=#000000", "--export-background-opacity=0.0"])