#include #include #include #define STBI_NO_LINEAR #define STBI_NO_HDR #define STBI_NO_TGA #define STB_IMAGE_IMPLEMENTATION #include "../stb/stb_image.h" #define STB_IMAGE_WRITE_IMPLEMENTATION #include "../stb/stb_image_write.h" #include "exoquant/exoquant.h" #include "n64graphics_ci.h" #include "utils.h" // SCALE_M_N: upscale/downscale M-bit integer to N-bit #define SCALE_5_8(VAL_) (((VAL_) * 0xFF) / 0x1F) #define SCALE_8_5(VAL_) ((((VAL_) + 4) * 0x1F) / 0xFF) #define SCALE_4_8(VAL_) ((VAL_) * 0x11) #define SCALE_8_4(VAL_) ((VAL_) / 0x11) #define SCALE_3_8(VAL_) ((VAL_) * 0x24) #define SCALE_8_3(VAL_) ((VAL_) / 0x24) typedef enum { IMG_FORMAT_CI } img_format; rgba *raw2rgba(const uint8_t *raw, int width, int height, int depth) { rgba *img; int img_size; img_size = width * height * sizeof(*img); img = malloc(img_size); if (!img) { ERROR("Error allocating %d bytes\n", img_size); return NULL; } if (depth == 16) { for (int i = 0; i < width * height; i++) { img[i].red = SCALE_5_8((raw[i * 2] & 0xF8) >> 3); img[i].green = SCALE_5_8(((raw[i * 2] & 0x07) << 2) | ((raw[i * 2 + 1] & 0xC0) >> 6)); img[i].blue = SCALE_5_8((raw[i * 2 + 1] & 0x3E) >> 1); img[i].alpha = (raw[i * 2 + 1] & 0x01) ? 0xFF : 0x00; } } else if (depth == 32) { for (int i = 0; i < width * height; i++) { img[i].red = raw[i * 4]; img[i].green = raw[i * 4 + 1]; img[i].blue = raw[i * 4 + 2]; img[i].alpha = raw[i * 4 + 3]; } } return img; } // extract RGBA from CI raw data and palette rgba *rawci2rgba(const uint8_t *rawci, const uint8_t *palette, int width, int height, int depth) { uint8_t *raw_rgba; rgba *img = NULL; int raw_size; int pal_index_1; int pal_index_2; // first convert to raw RGBA raw_size = 2 * width * height; raw_rgba = malloc(raw_size); if (!raw_rgba) { ERROR("Error allocating %u bytes\n", raw_size); return NULL; } if (depth == 4) { // CI4 for (int i = 0; i < (width * height) / 2; i++) { pal_index_1 = rawci[i] >> 4; pal_index_2 = rawci[i] & 0xF; raw_rgba[i * 4 + 0] = palette[pal_index_1 * 2 + 0]; raw_rgba[i * 4 + 1] = palette[pal_index_1 * 2 + 1]; raw_rgba[i * 4 + 2] = palette[pal_index_2 * 2 + 0]; raw_rgba[i * 4 + 3] = palette[pal_index_2 * 2 + 1]; } } else { // CI8 for (int i = 0; i < width * height; i++) { pal_index_1 = rawci[i]; raw_rgba[i * 2 + 0] = palette[pal_index_1 * 2 + 0]; raw_rgba[i * 2 + 1] = palette[pal_index_1 * 2 + 1]; } } // then convert to RGBA image data img = raw2rgba(raw_rgba, width, height, 16); free(raw_rgba); return img; } int rgba2rawci(uint8_t *raw, uint8_t *out_palette, int *pal_len, const rgba *img, int width, int height, int depth) { int size = width * height * depth / 8; int num_colors = *pal_len / 2; INFO("Converting RGBA %dx%d to raw CI%d\n", width, height, depth); uint8_t *rgba32_palette = malloc(num_colors * 4); uint8_t *ci8_raw = malloc(width * height); // Use ExoQuant to convert the RGBA32 data buffer to an CI8 output exq_data *pExq = exq_init(); exq_feed(pExq, (uint8_t*)img, width * height); exq_quantize_hq(pExq, num_colors); exq_get_palette(pExq, rgba32_palette, num_colors); exq_map_image_ordered(pExq, width, height, (uint8_t*)img, ci8_raw); exq_free(pExq); if (depth == 4) { // Convert CI8 image to CI4 image for (int i = 0; i < size; i++) { raw[i] = (ci8_raw[i * 2 + 0] << 4) | ci8_raw[i * 2 + 1]; } } else { memcpy(raw, ci8_raw, size); } // Convert RGBA32 palette to RGBA16 for (int i = 0; i < num_colors; i++) { unsigned char red = (rgba32_palette[i * 4 + 0] / 8) & 0x1F; unsigned char green = (rgba32_palette[i * 4 + 1] / 8) & 0x1F; unsigned char blue = (rgba32_palette[i * 4 + 2] / 8) & 0x1F; unsigned char alpha = rgba32_palette[i * 4 + 3] > 0 ? 1 : 0; // 1 bit alpha out_palette[i * 2 + 0] = (red << 3) | (green >> 2); out_palette[i * 2 + 1] = ((green & 3) << 6) | (blue << 1) | alpha; } free(rgba32_palette); free(ci8_raw); return size; } rgba *png2rgba(const char *png_filename, int *width, int *height) { rgba *img = NULL; int w = 0; int h = 0; int channels = 0; int img_size; stbi_uc *data = stbi_load(png_filename, &w, &h, &channels, STBI_default); if (!data || w <= 0 || h <= 0) { ERROR("Error loading png file \"%s\"\n", png_filename); return NULL; } INFO("Read \"%s\" %dx%d channels: %d\n", png_filename, w, h, channels); img_size = w * h * sizeof(*img); img = malloc(img_size); if (!img) { ERROR("Error allocating %u bytes\n", img_size); return NULL; } switch (channels) { case 3: // red, green, blue case 4: // red, green, blue, alpha for (int j = 0; j < h; j++) { for (int i = 0; i < w; i++) { int idx = j*w + i; img[idx].red = data[channels*idx]; img[idx].green = data[channels*idx + 1]; img[idx].blue = data[channels*idx + 2]; if (channels == 4) { img[idx].alpha = data[channels*idx + 3]; } else { img[idx].alpha = 0xFF; } } } break; case 2: // grey, alpha for (int j = 0; j < h; j++) { for (int i = 0; i < w; i++) { int idx = j*w + i; img[idx].red = data[2 * idx]; img[idx].green = data[2 * idx]; img[idx].blue = data[2 * idx]; img[idx].alpha = data[2 * idx + 1]; } } break; default: ERROR("Don't know how to read channels: %d\n", channels); free(img); img = NULL; } // cleanup stbi_image_free(data); *width = w; *height = h; return img; } int rgba2png(const char *png_filename, const rgba *img, int width, int height) { int ret = 0; INFO("Saving RGBA %dx%d to \"%s\"\n", width, height, png_filename); // convert to format stb_image_write expects uint8_t *data = malloc(4 * width*height); if (data) { for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { int idx = j*width + i; data[4 * idx] = img[idx].red; data[4 * idx + 1] = img[idx].green; data[4 * idx + 2] = img[idx].blue; data[4 * idx + 3] = img[idx].alpha; } } ret = stbi_write_png(png_filename, width, height, 4, data, 0); free(data); } return ret; } const char *n64graphics_get_read_version(void) { return "stb_image 2.19"; } const char *n64graphics_get_write_version(void) { return "stb_image_write 1.09"; } /***************************************************************/ #define N64GRAPHICS_VERSION "0.4 - CI Only Branch" #include typedef enum { MODE_EXPORT, MODE_IMPORT, } tool_mode; typedef struct { char *img_filename; char *bin_filename; tool_mode mode; unsigned int offset; img_format format; int depth; int width; int height; int truncate; } graphics_config; static const graphics_config default_config = { .img_filename = NULL, .bin_filename = NULL, .mode = MODE_EXPORT, .offset = 0, .format = IMG_FORMAT_CI, .depth = 8, .width = 32, .height = 32, .truncate = 1, }; typedef struct { const char *name; img_format format; int depth; } format_entry; static const format_entry format_table[] = { { "ci4", IMG_FORMAT_CI, 4 }, { "ci8", IMG_FORMAT_CI, 8 }, }; static const char *format2str(img_format format, int depth) { for (unsigned i = 0; i < DIM(format_table); i++) { if (format == format_table[i].format && depth == format_table[i].depth) { return format_table[i].name; } } return "unknown"; } static int parse_format(graphics_config *config, const char *str) { for (unsigned i = 0; i < DIM(format_table); i++) { if (!strcasecmp(str, format_table[i].name)) { config->format = format_table[i].format; config->depth = format_table[i].depth; return 1; } } return 0; } static void print_usage(void) { ERROR("Usage: n64graphics_ci -e/-i BIN_FILE -g PNG_FILE [-o offset] [-f FORMAT] [-w WIDTH] [-h HEIGHT] [-V]\n" "\n" "n64graphics v" N64GRAPHICS_VERSION ": N64 graphics manipulator\n" "\n" "Required arguments:\n" " -e BIN_FILE export from BIN_FILE to PNG_FILE\n" " -i BIN_FILE import from PNG_FILE to BIN_FILE\n" " -g PNG_FILE graphics file to import/export (.png)\n" "Optional arguments:\n" " -o OFFSET starting offset in BIN_FILE (prevents truncation during import)\n" " -f FORMAT texture format: ci4, ci8 (default: %s)\n" " -w WIDTH export texture width (default: %d)\n" " -h HEIGHT export texture height (default: %d)\n" " -v verbose logging\n" " -V print version information\n", format2str(default_config.format, default_config.depth), default_config.width, default_config.height); } static void print_version(void) { ERROR("n64graphics v" N64GRAPHICS_VERSION ", using:\n" " %s\n" " %s\n", n64graphics_get_read_version(), n64graphics_get_write_version()); } // parse command line arguments static int parse_arguments(int argc, char *argv[], graphics_config *config) { for (int i = 1; i < argc; i++) { if (argv[i][0] == '-') { switch (argv[i][1]) { case 'e': if (++i >= argc) return 0; config->bin_filename = argv[i]; config->mode = MODE_EXPORT; break; case 'f': if (++i >= argc) return 0; if (!parse_format(config, argv[i])) { return 0; } break; case 'i': if (++i >= argc) return 0; config->bin_filename = argv[i]; config->mode = MODE_IMPORT; break; case 'g': if (++i >= argc) return 0; config->img_filename = argv[i]; break; case 'h': if (++i >= argc) return 0; config->height = strtoul(argv[i], NULL, 0); break; case 'o': if (++i >= argc) return 0; config->offset = strtoul(argv[i], NULL, 0); config->truncate = 0; break; case 'w': if (++i >= argc) return 0; config->width = strtoul(argv[i], NULL, 0); break; case 'v': g_verbosity = 1; break; case 'V': print_version(); exit(0); break; default: return 0; break; } } else { return 0; } } return 1; } char* getPaletteFilename(char* ci_filename) { int bin_filename_len; int extension_len; char* pal_bin_filename; char* extension_loc; char* extension; // Write Palette file bin_filename_len = strlen(ci_filename); extension_loc = strrchr(ci_filename, '.'); extension_len = (int)(extension_loc - ci_filename); extension = malloc(extension_len); memcpy(extension, extension_loc, extension_len); pal_bin_filename = malloc(bin_filename_len + 4); // +4 to include ".pal" if (!pal_bin_filename) { ERROR("Error allocating bytes for palette filename\n"); } memcpy(pal_bin_filename, ci_filename, bin_filename_len); strcpy(pal_bin_filename + bin_filename_len, ".pal"); //strcpy(pal_bin_filename + bin_filename_len, extension); free(extension); return pal_bin_filename; } int main(int argc, char* argv[]) { graphics_config config = default_config; rgba *imgr; FILE *fp; uint8_t *raw; int raw_size; int length = 0; int flength; int res; FILE *fp_pal; // for ci palette uint8_t *pal; int pal_len; char* pal_bin_filename; int valid = parse_arguments(argc, argv, &config); if (!valid || !config.bin_filename || !config.bin_filename || !config.img_filename) { print_usage(); exit(EXIT_FAILURE); } if (config.mode == MODE_IMPORT) { printf("%s\n", config.bin_filename); if (config.truncate) { fp = fopen(config.bin_filename, "wb"); } else { fp = fopen(config.bin_filename, "r+b"); } if (!fp) { ERROR("Error opening binary file \"%s\"\n", config.bin_filename); return -1; } if (!config.truncate) { fseek(fp, config.offset, SEEK_SET); } switch (config.format) { case IMG_FORMAT_CI: imgr = png2rgba(config.img_filename, &config.width, &config.height); raw_size = config.width * config.height * config.depth / 8; raw = malloc(raw_size); if (!raw) { ERROR("Error allocating %u bytes\n", raw_size); } if (config.depth == 4) { pal_len = 16 * 2; // CI4 } else { pal_len = 256 * 2; // CI8 } pal = malloc(pal_len); if (!pal) { ERROR("Error allocating %u bytes for palette\n", pal_len); } length = rgba2rawci(raw, pal, &pal_len, imgr, config.width, config.height, config.depth); //length = rgba2raw(raw, imgr, config.width, config.height, config.depth); break; } if (length <= 0) { ERROR("Error converting to raw format\n"); return EXIT_FAILURE; } INFO("Writing 0x%X bytes to offset 0x%X of \"%s\"\n", length, config.offset, config.bin_filename); flength = fwrite(raw, 1, length, fp); if (flength != length) { ERROR("Error writing %d bytes to \"%s\"\n", length, config.bin_filename); } fclose(fp); if (config.format == IMG_FORMAT_CI) { pal_bin_filename = getPaletteFilename(config.bin_filename); fp_pal = fopen(pal_bin_filename, "wb"); INFO("Writing 0x%X bytes to palette file \"%s\"\n", pal_len, pal_bin_filename); flength = fwrite(pal, 1, pal_len, fp_pal); if (flength != pal_len) { ERROR("Error writing %d bytes to \"%s\"\n", pal_len, pal_bin_filename); } fclose(fp_pal); free(pal_bin_filename); } } else { // Export if (config.width <= 0 || config.height <= 0 || config.depth <= 0) { ERROR("Error: must set position width and height for export\n"); return EXIT_FAILURE; } fp = fopen(config.bin_filename, "rb"); if (!fp) { ERROR("Error opening \"%s\"\n", config.bin_filename); return -1; } raw_size = config.width * config.height * config.depth / 8; raw = malloc(raw_size); if (config.offset > 0) { fseek(fp, config.offset, SEEK_SET); } flength = fread(raw, 1, raw_size, fp); if (flength != raw_size) { ERROR("Error reading %d bytes from \"%s\"\n", raw_size, config.bin_filename); } switch (config.format) { case IMG_FORMAT_CI: // Read Palette file pal_bin_filename = getPaletteFilename(config.bin_filename); fp_pal = fopen(pal_bin_filename, "rb"); if (!fp_pal) { ERROR("Error opening \"%s\"\n", pal_bin_filename); return -1; } if (config.depth == 4) { pal_len = 16 * 2; // CI4 } else { pal_len = 256 * 2; // CI8 } pal = malloc(pal_len); if (config.offset > 0) { fseek(fp, config.offset, SEEK_SET); } flength = fread(pal, 1, pal_len, fp_pal); imgr = rawci2rgba(raw, pal, config.width, config.height, config.depth); res = rgba2png(config.img_filename, imgr, config.width, config.height); free(pal_bin_filename); break; default: return EXIT_FAILURE; } if (!res) { ERROR("Error writing to \"%s\"\n", config.img_filename); } } return 0; }