-
Notifications
You must be signed in to change notification settings - Fork 0
/
sppak.ml
215 lines (175 loc) · 5.88 KB
/
sppak.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
open Bin_pack;;
open Printf;;
open Stb_image;;
open Bigarray;;
let help () =
print_endline "Sprite Packer";
print_endline "by Strrationalism Studio 2021";
print_endline "";
print_endline "Usage:";
print_endline " sppak <inputDir> <output> [OPTIONS]";
print_endline "";
print_endline "Options:";
print_endline " --align-to-4 Align packed image to 4 * N pixels.";
print_endline " --margin <margin> Set margin in pixels in each sprite.";
print_endline " --channels <channels> Set number (1 ~ 4) of channels in each sprite.";
print_endline ""
;;
let rec get_files prefix dir =
if Sys.is_directory dir then
Sys.readdir dir
|> Array.to_list
|> List.concat_map (fun name ->
get_files
(prefix ^ name ^ "/")
(dir ^ "/" ^ name))
else [String.sub prefix 0 (String.length prefix - 1)]
;;
let get_image_files channels dir =
get_files "" dir
|> List.filter (fun file ->
let ext =
Filename.extension file
|> String.lowercase_ascii in
ext = ".png" || ext = ".bmp")
|> List.map (fun name ->
let img_result =
Stb_image.load
~channels:channels
(dir ^ "/" ^ name) in
match img_result with
| Error (`Msg x) -> failwith x
| Ok img ->
if
Array1.dim (data img)
<> width img * height img * channels
then
"You must pass images in " ^ Int.to_string channels ^ " channels."
|> failwith;
name, img)
;;
type align_mode =
| No_align
| Align_to_4
;;
let align_to_4 x =
if x mod 4 <> 0 then (x / 4 + 1) * 4
else x
;;
let align align_mode (result: _ Bin_pack.result) =
match align_mode with
| No_align -> result
| Align_to_4 ->
{ result with
width = align_to_4 result.width;
height = align_to_4 result.height }
;;
type options =
{ margin: int;
align_mode: align_mode;
channels: int }
;;
let default_options =
{ margin = 0;
align_mode = No_align;
channels = 4 }
;;
let rec parse_options prev_option =
function
| [] -> Ok prev_option
| "--margin" :: margin_value :: other ->
parse_options
{ prev_option with margin = int_of_string margin_value }
other
| "--align-to-4" :: other ->
parse_options
{ prev_option with align_mode = Align_to_4 }
other
| "--channels" :: channels :: other ->
parse_options
{ prev_option with channels = int_of_string channels }
other
| _ -> Error ()
;;
let write_bin_pack_result options out (result: _ Bin_pack.result) =
let pack =
Array1.create
Int8_unsigned
c_layout
(result.width * result.height * options.channels) in
Array1.fill pack 0;
let output_csv = open_out (out ^ ".csv") in
fprintf output_csv "name, x, y, w, h\n";
let sprites =
result.rects
|> List.map (fun (rect, tag) ->
{ x = rect.x + options.margin;
y = rect.y + options.margin;
w = rect.w - 2 * options.margin;
h = rect.h - 2 * options.margin; },
tag) in
sprites
|> List.iter (fun (rect, (name, image)) ->
if width image <> rect.w then
failwith "Width of image is not equals to rect.";
if height image <> rect.h then
failwith "Height of image is not equals to rect.";
let image_pixels = data image in
for src_y = 0 to rect.h - 1 do
for src_x = 0 to rect.w - 1 do
let dst_y = rect.y + src_y in
let dst_x = rect.x + src_x in
let dst_offset =
(dst_y * result.width + dst_x) * options.channels in
let src_offset =
(src_y * rect.w + src_x) * options.channels in
for channel = 0 to options.channels - 1 do
Array1.get image_pixels (src_offset + channel)
|> Array1.set pack (dst_offset + channel)
done
done
done;
fprintf
output_csv
"\"%s\", %d, %d, %d, %d\n"
(Filename.chop_extension name)
rect.x
rect.y
rect.w
rect.h);
close_out output_csv;
let save_function =
match Filename.extension out with
| ".png" -> Stb_image_write.png
| ".bmp" -> Stb_image_write.bmp
| ".tga" -> Stb_image_write.tga
| _ -> failwith "Unknown output type." in
save_function
out
~w:result.width
~h:result.height
~c:options.channels
pack
;;
let () =
Sys.argv
|> Array.to_list
|> function
| _ :: input_dir :: out :: options ->
if Sys.is_directory input_dir |> not then
failwith "<inputDir> must be a dirctory.";
begin
match parse_options default_options options with
| Error () -> help ()
| Ok options ->
get_image_files options.channels input_dir
|> List.map (fun (name, image) ->
{ w = width image + 2 * options.margin;
h = height image + 2 * options.margin;
tag = name, image })
|> bin_pack
|> align options.align_mode
|> write_bin_pack_result options out
end
| _ -> help ()
;;