diff options
Diffstat (limited to 'img2gba/src/main.c')
-rw-r--r-- | img2gba/src/main.c | 452 |
1 files changed, 452 insertions, 0 deletions
diff --git a/img2gba/src/main.c b/img2gba/src/main.c new file mode 100644 index 0000000..8a5edfc --- /dev/null +++ b/img2gba/src/main.c | |||
@@ -0,0 +1,452 @@ | |||
1 | #include <getopt.h> | ||
2 | |||
3 | #include "shorthand.h" | ||
4 | |||
5 | #define STB_IMAGE_IMPLEMENTATION | ||
6 | #include "stb_image.h" | ||
7 | |||
8 | #define TILE_SIZE 8 | ||
9 | #define PALETTE_SIZE 16 | ||
10 | #define DEFAULT_BG_COLOR 0x0000 | ||
11 | #define MAX_FILE_NAME 1024 | ||
12 | |||
13 | typedef u16 Color; | ||
14 | |||
15 | static inline Color | ||
16 | rgb15(u32 red, u32 green, u32 blue ) { | ||
17 | return (blue << 10) | (green << 5) | red; | ||
18 | } | ||
19 | |||
20 | static inline | ||
21 | int quantize5(int color) { | ||
22 | return color * 31 / 255; | ||
23 | } | ||
24 | |||
25 | // TODO: Assuming 4bpp tiles. The tile is an index to a palette. | ||
26 | // TODO: Write more documentation about this. | ||
27 | typedef u32 Tile[TILE_SIZE]; | ||
28 | |||
29 | typedef struct Tiles { | ||
30 | Tile *data; | ||
31 | size_t size; | ||
32 | size_t capacity; | ||
33 | } Tiles; | ||
34 | |||
35 | typedef struct Palette { | ||
36 | Color data[PALETTE_SIZE]; | ||
37 | size_t size; | ||
38 | size_t capacity; | ||
39 | } Palette; | ||
40 | |||
41 | typedef struct Image { | ||
42 | unsigned char *data; | ||
43 | int width; | ||
44 | int height; | ||
45 | int n_channels; | ||
46 | } Image; | ||
47 | |||
48 | u8 | ||
49 | pack_1bb(u32 hex) { | ||
50 | u32 low_nibble = hex & 0xFFFF; | ||
51 | u32 high_nibble = (hex >> 16) & 0xFFFF; | ||
52 | u32 low = ((low_nibble >> 12) & 0x1); | ||
53 | low += 2 * ((low_nibble >> 8) & 0x1); | ||
54 | low += 4 * ((low_nibble >> 4) & 0x1); | ||
55 | low += 8 * ((low_nibble >> 0) & 0x1); | ||
56 | u32 high = ((high_nibble >> 12) & 0x1); | ||
57 | high += 2 * ((high_nibble >> 8) & 0x1); | ||
58 | high += 4 * ((high_nibble >> 4) & 0x1); | ||
59 | high += 8 * ((high_nibble >> 0) & 0x1); | ||
60 | return (low << 4) | high; | ||
61 | } | ||
62 | |||
63 | void | ||
64 | export_c_file(Tiles *tiles, Palette *palette, FILE *file, bool pack_1bpp) { | ||
65 | // Output tiles. | ||
66 | if (pack_1bpp) { | ||
67 | fprintf(file, "static const u32 packed_tiles[] = {\n "); | ||
68 | size_t counter = 0; | ||
69 | for (size_t i = 0; i < tiles->capacity; ++i) { | ||
70 | for (size_t j = 0; j < TILE_SIZE; j += 4, counter++) { | ||
71 | if (counter == 4) { | ||
72 | counter = 0; | ||
73 | fprintf(file, "\n "); | ||
74 | } | ||
75 | u32 tile_a = tiles->data[i][j]; | ||
76 | u32 tile_b = tiles->data[i][j + 1]; | ||
77 | u32 tile_c = tiles->data[i][j + 2]; | ||
78 | u32 tile_d = tiles->data[i][j + 3]; | ||
79 | u32 packed = pack_1bb(tile_d) << 0; | ||
80 | packed |= pack_1bb(tile_c) << 8; | ||
81 | packed |= pack_1bb(tile_b) << 16; | ||
82 | packed |= pack_1bb(tile_a) << 24; | ||
83 | fprintf(file, "0x%08x,", packed); | ||
84 | if (counter < 3) { | ||
85 | fprintf(file, " "); | ||
86 | } | ||
87 | } | ||
88 | } | ||
89 | fprintf(file, "\n};\n"); | ||
90 | fprintf(file, "\n"); | ||
91 | } else { | ||
92 | fprintf(file, "static const u32 tiles[%lu][%u] = {\n", tiles->capacity, TILE_SIZE); | ||
93 | for (size_t i = 0; i < tiles->capacity; ++i) { | ||
94 | fprintf(file, " {"); | ||
95 | for (size_t j = 0; j < TILE_SIZE; ++j) { | ||
96 | fprintf(file, "0x%08x", tiles->data[i][j]); | ||
97 | if (j != (TILE_SIZE - 1)) { | ||
98 | fprintf(file, ", "); | ||
99 | } | ||
100 | } | ||
101 | fprintf(file, "},\n"); | ||
102 | } | ||
103 | fprintf(file, "};\n"); | ||
104 | fprintf(file, "\n"); | ||
105 | } | ||
106 | |||
107 | // Output palette. | ||
108 | fprintf(file, "u16 palette[%lu] = {\n", palette->capacity); | ||
109 | fprintf(file, " "); | ||
110 | size_t counter = 0; | ||
111 | for (size_t i = 0; i < palette->capacity; ++i, ++counter) { | ||
112 | if (counter == 4) { | ||
113 | counter = 0; | ||
114 | fprintf(file, "\n "); | ||
115 | } | ||
116 | fprintf(file, "0x%04x,", palette->data[i]); | ||
117 | if (counter < 3) { | ||
118 | fprintf(file, " "); | ||
119 | } | ||
120 | } | ||
121 | fprintf(file, "\n"); | ||
122 | fprintf(file, "};\n"); | ||
123 | } | ||
124 | |||
125 | void | ||
126 | export_bin_file(Tiles *tiles, Palette *palette, FILE *file, FILE *filepal, bool pack_1bpp) { | ||
127 | if (pack_1bpp) { | ||
128 | u64 counter = 0; | ||
129 | for (size_t i = 0; i < tiles->capacity; ++i) { | ||
130 | for (size_t j = 0; j < TILE_SIZE; j += 4, counter++) { | ||
131 | u32 tile_a = tiles->data[i][j]; | ||
132 | u32 tile_b = tiles->data[i][j + 1]; | ||
133 | u32 tile_c = tiles->data[i][j + 2]; | ||
134 | u32 tile_d = tiles->data[i][j + 3]; | ||
135 | u32 packed = pack_1bb(tile_a) << 0; | ||
136 | packed |= pack_1bb(tile_b) << 8; | ||
137 | packed |= pack_1bb(tile_c) << 16; | ||
138 | packed |= pack_1bb(tile_d) << 24; | ||
139 | fwrite(&packed, sizeof(u32), 1, file); | ||
140 | } | ||
141 | } | ||
142 | // Pad with zeros to avoid alignment issues. | ||
143 | if (counter) { | ||
144 | u32 zero = 0; | ||
145 | fwrite(&zero, sizeof(u32), counter % 8, file); | ||
146 | } | ||
147 | } else { | ||
148 | for (size_t i = 0; i < tiles->capacity; ++i) { | ||
149 | fwrite(&tiles->data[i][0], sizeof(u32), TILE_SIZE, file); | ||
150 | } | ||
151 | fwrite(palette->data, sizeof(u16), PALETTE_SIZE, filepal); | ||
152 | } | ||
153 | } | ||
154 | |||
155 | void | ||
156 | extract_tile(Tiles *tiles, Palette *palette, Image *img, | ||
157 | size_t offset_x, size_t offset_y) { | ||
158 | for (size_t j = 0; j < TILE_SIZE; ++j) { | ||
159 | u32 col_index = 0x00000000; | ||
160 | for (size_t i = 0; i < TILE_SIZE; ++i) { | ||
161 | size_t pal_index = palette->size; | ||
162 | // Find the memory index for this pixel. | ||
163 | int idx = (i + offset_x) + (j + offset_y) * img->width; | ||
164 | idx *= img->n_channels; | ||
165 | |||
166 | int red = img->data[idx]; | ||
167 | int green = img->data[idx + 1]; | ||
168 | int blue = img->data[idx + 2]; | ||
169 | |||
170 | // Quantize to 5 bits per channel. | ||
171 | red = quantize5(red); | ||
172 | green = quantize5(green); | ||
173 | blue = quantize5(blue); | ||
174 | |||
175 | Color clr = rgb15(red, green, blue); | ||
176 | bool found = false; | ||
177 | // Brute force search, but since the palette size will likely be | ||
178 | // small, shouldn't be a big problem. | ||
179 | if (palette->size < palette->capacity) { | ||
180 | // If the palette is not full try to find if the color is | ||
181 | // already stored. | ||
182 | for (size_t p = 0; p < palette->capacity; ++p) { | ||
183 | if (clr == palette->data[p]) { | ||
184 | pal_index = p; | ||
185 | found = true; | ||
186 | break; | ||
187 | } | ||
188 | } | ||
189 | } else { | ||
190 | // If the palette is full, find the closest perceived color | ||
191 | // instead. | ||
192 | size_t best_index = 0; | ||
193 | double best_distance = 1e99; | ||
194 | for (size_t p = 0; p < palette->capacity; ++p) { | ||
195 | Color pal_clr = palette->data[p]; | ||
196 | int pal_red = pal_clr & 0x1F; | ||
197 | int pal_green = (pal_clr >> 5) & 0x1F; | ||
198 | int pal_blue = (pal_clr >> 10) & 0x1F; | ||
199 | double r = (pal_red - red); | ||
200 | double g = (pal_green - green); | ||
201 | double b = (pal_blue - blue); | ||
202 | double distance = r * r * 0.3 + g * g * 0.59 + b * b * 0.11; | ||
203 | if (distance < best_distance) { | ||
204 | best_distance = distance; | ||
205 | best_index = p; | ||
206 | } | ||
207 | } | ||
208 | pal_index = best_index; | ||
209 | found = true; | ||
210 | } | ||
211 | if (!found) { | ||
212 | palette->data[palette->size++] = clr; | ||
213 | } | ||
214 | col_index |= pal_index << i * 4; // TODO: FlipH? | ||
215 | } | ||
216 | tiles->data[tiles->size][j] = col_index; | ||
217 | } | ||
218 | tiles->size++; | ||
219 | } | ||
220 | |||
221 | void | ||
222 | print_usage(void) { | ||
223 | printf("Usage: %s [options] <filename>\n", BIN_NAME); | ||
224 | printf("\n"); | ||
225 | printf("\t-b <RRGGBB> Selects the background color used for transparency.\n"); | ||
226 | printf("\t-o <out_file> Path to the output file. If blank, stdout will be used with C array export.\n"); | ||
227 | printf("\t-x <0> Start x position (in pixels).\n"); | ||
228 | printf("\t-y <0> Start y position (in pixels).\n"); | ||
229 | printf("\t-W <8> Select the sprite width in pixels. Must be a multiple of the tile size.\n"); | ||
230 | printf("\t-H <8> Select the sprite height in pixels. Must be a multiple of the tile size.\n"); | ||
231 | printf("\t-1 Bit pack the output to 1bbp (4x size reduction).\n"); | ||
232 | printf("\t-w <0> Width of region to extract from (x0,y0). Defaults to image width.\n"); | ||
233 | printf("\t-h <0> Height of region to extract from (x0,y0). Defaults to image height.\n"); | ||
234 | printf("\t-v Verbose mode\n"); | ||
235 | printf("\n"); | ||
236 | } | ||
237 | |||
238 | int | ||
239 | main(int argc, char *argv[]) { | ||
240 | // Initialize default parameters. | ||
241 | u16 background_color = DEFAULT_BG_COLOR; | ||
242 | u16 palette_size = PALETTE_SIZE; | ||
243 | size_t sprite_width = TILE_SIZE; | ||
244 | size_t sprite_height = TILE_SIZE; | ||
245 | char *out_file_path = NULL; | ||
246 | bool pack_1bpp = false; | ||
247 | size_t start_x = 0; | ||
248 | size_t start_y = 0; | ||
249 | size_t width = 0; | ||
250 | size_t height = 0; | ||
251 | bool verbose = false; | ||
252 | |||
253 | int option; | ||
254 | while ((option = getopt(argc, argv, "b:o:W:H:1x:y:w:h:v")) != -1) { | ||
255 | switch (option) { | ||
256 | case 'b': { | ||
257 | // Background color. | ||
258 | char *bg = optarg; | ||
259 | if (strlen(bg) != 6) { | ||
260 | fprintf(stderr, "%s: Invalid background color.\n", BIN_NAME); | ||
261 | print_usage(); | ||
262 | return EXIT_FAILURE; | ||
263 | } | ||
264 | unsigned int red = 0; | ||
265 | unsigned int green = 0; | ||
266 | unsigned int blue = 0; | ||
267 | |||
268 | // NOTE: Undefined behaviour, here we go! Also, no error checks, | ||
269 | // so if an invalid number is given it will be parsed as 0. | ||
270 | char r_str[3] = {bg[0], bg[1], '\0'}; | ||
271 | char g_str[3] = {bg[2], bg[3], '\0'}; | ||
272 | char b_str[3] = {bg[4], bg[5], '\0'}; | ||
273 | sscanf(r_str, "%x", &red); | ||
274 | sscanf(g_str, "%x", &green); | ||
275 | sscanf(b_str, "%x", &blue); | ||
276 | |||
277 | // Quantize and transform to rgb15. | ||
278 | red = quantize5(red); | ||
279 | green = quantize5(green); | ||
280 | blue = quantize5(blue); | ||
281 | background_color = rgb15(red, green, blue); | ||
282 | } break; | ||
283 | case 'W': { | ||
284 | // Sprite width. | ||
285 | char *arg = optarg; | ||
286 | sprite_width = 0; | ||
287 | sprite_width = atoi(arg); | ||
288 | |||
289 | // Check if a valid number is given. | ||
290 | if (sprite_width == 0 || sprite_width % TILE_SIZE != 0) { | ||
291 | fprintf(stderr, "%s: Invalid sprite width.\n", BIN_NAME); | ||
292 | print_usage(); | ||
293 | return EXIT_FAILURE; | ||
294 | } | ||
295 | } break; | ||
296 | case 'H': { | ||
297 | // Sprite height. | ||
298 | char *arg = optarg; | ||
299 | sprite_height = 0; | ||
300 | sprite_height = atoi(arg); | ||
301 | |||
302 | // Check if a valid number is given. | ||
303 | if (sprite_height == 0 || sprite_height % TILE_SIZE != 0) { | ||
304 | fprintf(stderr, "%s: Invalid sprite height.\n", BIN_NAME); | ||
305 | print_usage(); | ||
306 | return EXIT_FAILURE; | ||
307 | } | ||
308 | } break; | ||
309 | case 'o': { | ||
310 | // Output file. | ||
311 | out_file_path = optarg; | ||
312 | } break; | ||
313 | case '1': { | ||
314 | // Pack output to 1bpp. | ||
315 | pack_1bpp = true; | ||
316 | } break; | ||
317 | case 'x': { | ||
318 | // Start x position in pixels. | ||
319 | start_x = atoi(optarg); | ||
320 | } break; | ||
321 | case 'y': { | ||
322 | // Start y position in pixels. | ||
323 | start_y = atoi(optarg); | ||
324 | } break; | ||
325 | case 'w': { | ||
326 | // Width of region to extract from (x0,y0). | ||
327 | width = atoi(optarg); | ||
328 | } break; | ||
329 | case 'h': { | ||
330 | // Height of region to extract from (x0,y0). | ||
331 | height = atoi(optarg); | ||
332 | } break; | ||
333 | case 'v': { | ||
334 | verbose = true; | ||
335 | } break; | ||
336 | default: { | ||
337 | print_usage(); | ||
338 | return EXIT_FAILURE; | ||
339 | } break; | ||
340 | } | ||
341 | } | ||
342 | |||
343 | // Get the path to the file to be exported. | ||
344 | if (optind != argc - 1) { | ||
345 | fprintf(stderr, "%s: No filename given.\n", BIN_NAME); | ||
346 | print_usage(); | ||
347 | return EXIT_FAILURE; | ||
348 | } | ||
349 | char *file_name = argv[optind]; | ||
350 | |||
351 | // Fill the palette with the background color if one was given. | ||
352 | Palette palette = { | ||
353 | .size = 1, | ||
354 | .capacity = palette_size, | ||
355 | }; | ||
356 | for (size_t i = 0; i < palette_size; ++i) { | ||
357 | palette.data[i] = background_color; | ||
358 | } | ||
359 | |||
360 | Image img = {0}; | ||
361 | |||
362 | // Open the given file. | ||
363 | FILE *input_file = fopen(file_name, "rb"); | ||
364 | if (input_file == NULL) { | ||
365 | fprintf(stderr, "%s: can't open input file: %s\n", BIN_NAME, file_name); | ||
366 | return EXIT_FAILURE; | ||
367 | } | ||
368 | img.data = stbi_load_from_file(input_file, &img.width, &img.height, &img.n_channels, 0); | ||
369 | fclose(input_file); | ||
370 | |||
371 | // TODO: Implement support for different file inputs. | ||
372 | if (img.n_channels != 3) { | ||
373 | fprintf(stderr, "File format not supported. Only 3 channel files for now.\n"); | ||
374 | return EXIT_FAILURE; | ||
375 | } | ||
376 | |||
377 | if (width == 0) { | ||
378 | width = img.width; | ||
379 | } | ||
380 | if (height == 0) { | ||
381 | height = img.height; | ||
382 | } | ||
383 | size_t n_tiles_x = width / TILE_SIZE; | ||
384 | size_t n_tiles_y = height / TILE_SIZE; | ||
385 | size_t n_tiles = n_tiles_x * n_tiles_y; | ||
386 | |||
387 | // Allocate memory for the tiles in this file, with zero-initialization. | ||
388 | Tiles tiles = { | ||
389 | .data = calloc(n_tiles, sizeof(Tile)), | ||
390 | .size = 0, | ||
391 | .capacity = n_tiles, | ||
392 | }; | ||
393 | |||
394 | size_t n_sprites_x = width / sprite_width; | ||
395 | size_t n_sprites_y = height / sprite_height; | ||
396 | size_t n_sprite_tiles_x = sprite_width / TILE_SIZE; | ||
397 | size_t n_sprite_tiles_y = sprite_height / TILE_SIZE; | ||
398 | for (size_t p = 0; p < n_sprites_y; ++p) { | ||
399 | for (size_t k = 0; k < n_sprites_x; ++k) { | ||
400 | for (size_t j = 0; j < n_sprite_tiles_y; ++j) { | ||
401 | int offset_x = start_x + k * sprite_width; | ||
402 | int offset_y = start_y + p * sprite_height + j * TILE_SIZE; | ||
403 | for (size_t i = 0; i < n_sprite_tiles_x; ++i) { | ||
404 | extract_tile(&tiles, &palette, &img, offset_x, offset_y); | ||
405 | offset_x += TILE_SIZE; | ||
406 | } | ||
407 | } | ||
408 | } | ||
409 | } | ||
410 | |||
411 | if (out_file_path == NULL) { | ||
412 | export_c_file(&tiles, &palette, stdout, pack_1bpp); | ||
413 | } else { | ||
414 | char file_name[MAX_FILE_NAME] = {0}; | ||
415 | char file_name_pal[MAX_FILE_NAME] = {0}; | ||
416 | strncpy(file_name, out_file_path, MAX_FILE_NAME - 1); | ||
417 | strncpy(file_name_pal, out_file_path, MAX_FILE_NAME - 1); | ||
418 | FILE *file = fopen(strcat(file_name, ".img.bin"), "w"); | ||
419 | if (!file) { | ||
420 | fprintf(stderr, "%s: can't open output file: %s\n", BIN_NAME, file_name); | ||
421 | return EXIT_FAILURE; | ||
422 | } | ||
423 | if (pack_1bpp) { | ||
424 | export_bin_file(&tiles, &palette, file, NULL, pack_1bpp); | ||
425 | fclose(file); | ||
426 | if (verbose) { | ||
427 | printf("Exporting binary file (1bpp packed)\n"); | ||
428 | printf("FILE IMG: %s\n", file_name); | ||
429 | } | ||
430 | } else { | ||
431 | FILE *file_pal = fopen(strcat(file_name_pal, ".pal.bin"), "w"); | ||
432 | if (!file_pal) { | ||
433 | fprintf(stderr, "%s: can't open output file: %s\n", BIN_NAME, file_name_pal); | ||
434 | fclose(file); | ||
435 | return EXIT_FAILURE; | ||
436 | } | ||
437 | if (verbose) { | ||
438 | printf("Exporting binary file\n"); | ||
439 | printf("FILE IMG: %s\n", file_name); | ||
440 | printf("FILE PAL: %s\n", file_name_pal); | ||
441 | } | ||
442 | export_bin_file(&tiles, &palette, file, file_pal, pack_1bpp); | ||
443 | fclose(file); | ||
444 | fclose(file_pal); | ||
445 | } | ||
446 | } | ||
447 | |||
448 | // Cleanup resources and exit. | ||
449 | free(tiles.data); | ||
450 | stbi_image_free(img.data); | ||
451 | return EXIT_SUCCESS; | ||
452 | } | ||