diff --git a/img2cpi.c b/img2cpi.c new file mode 100644 index 0000000..67c603e --- /dev/null +++ b/img2cpi.c @@ -0,0 +1,327 @@ +// x-run: ~/scripts/runc.sh % -Wall -Wextra -std=c99 -pedantic -lm --- ~/images/boykisser.png cpi-images/boykisser.cpi +#define STB_IMAGE_IMPLEMENTATION +#include +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_COLOR_DIFFERENCE 768 + +struct arguments { + bool text_mode; + int width, height; + enum cpi_version { + CPI_VERSION_AUTO, + CPI_VERSION_0, + CPI_VERSION_1, + CPI_VERSION_2, + } cpi_version; + enum placement { + PLACEMENT_CENTER, + PLACEMENT_COVER, + PLACEMENT_TILE, + PLACEMENT_FULL, + PLACEMENT_EXTEND, + PLACEMENT_FILL + } placement; + char *input_path; + char *output_path; +} args = { + .text_mode = false, + .width = 4 * 8 - 1, + .height = 3 * 6 - 2, + .cpi_version = CPI_VERSION_AUTO, + .placement = PLACEMENT_FULL, + .input_path = NULL, + .output_path = NULL +}; + +struct image { + int w, h; + union color { // Alpha channel is not used, it's just there to align it all + struct rgba { uint8_t r, g, b, a; } rgba; + uint32_t v; + } *pixels; +}; + +bool parse_cmdline(int argc, char **argv); +void show_help(const char *progname, bool show_all, FILE *fp); +struct image *image_load(const char *fp); +struct image *image_new(int w, int h); +struct image *image_resize(struct image *original, int new_w, int new_h); +struct image *image_dither(struct image *original, union color *colors, size_t n_colors); +void image_unload(struct image *img); + +const char *known_file_extensions[] = { + ".png", ".jpg", ".jpeg", ".jfif", ".jpg", ".gif", + ".tga", ".bmp", ".hdr", ".pnm", 0 +}; + +static const struct optiondocs { + char shortopt; + char *longopt; + char *target; + char *doc; + struct optiondocs_choice { char *value; char *doc; } *choices; +} optiondocs[] = { + { 'h', "help", 0, "Show help", 0 }, + { 't', "textmode", 0, "Output Lua script instead of binary", 0 }, + { 'W', "width", "width", "Width in characters", 0 }, + { 'h', "height", "height", "Height in characters", 0 }, + { 'V', "cpi_version", "version", "Force specific version of CPI", + (struct optiondocs_choice[]) { + { "-1", "Choose best available" }, + { "0", "OG CPI, 255x255, uncompressed" }, + { "1", "CPIv1, huge images, uncompressed" }, + { "255", "In-dev version, may not work" }, + { 0, 0 } } }, + { 'p', "placement", "placement", "Image placement mode (same as in hsetroot)", + (struct optiondocs_choice[]){ + { "center", "Render image centered on the canvas" }, + { "cover", "Centered on screen, scaled to fill fully" }, + { "tile", "Render image tiled" }, + { "full", "Use maximum aspect ratio" }, + { "extend", "Same as \"full\", but filling borders" }, + { "fill", "Stretch to fill" }, + { 0, 0 } } }, + { 0, 0, "input.*", "Input file path", 0 }, + { 0, 0, "output.cpi", "Output file path", 0 }, + { 0 } +}; + +int main(int argc, char **argv) { + if (!parse_cmdline(argc, argv)) { + show_help(argv[0], false, stderr); + fprintf(stderr, "Fatal error occurred, exiting.\n"); + return EXIT_FAILURE; + } + + struct image *src_image = image_load(args.input_path); + if (!src_image) { + fprintf(stderr, "Error: failed to open the file\n"); + return EXIT_FAILURE; + } + + struct image *canvas = image_new(args.width * 2, args.height * 3); + if (!canvas) { + fprintf(stderr, "Error: failed to allocate second image buffer\n"); + return EXIT_FAILURE; + } + + // TODO: actually do stuff + + image_unload(src_image); + image_unload(canvas); + return EXIT_SUCCESS; +} + +bool parse_cmdline(int argc, char **argv) { + static struct option options[] = { + { "help", no_argument, 0, 'h' }, + { "textmode", no_argument, 0, 't' }, + { "width", required_argument, 0, 'W' }, + { "height", required_argument, 0, 'H' }, + { "cpi_version", required_argument, 0, 'V' }, + { "placement", required_argument, 0, 'p' }, + { 0, 0, 0, 0 } + }; + + while (true) { + int option_index = 0; + int c = getopt_long(argc, argv, "htW:H:V:p:", options, &option_index); + if (c == -1) break; + if (c == 0) c = options[option_index].val; + if (c == '?') break; + + switch (c) { + case 'h': + show_help(argv[0], true, stdout); + exit(EXIT_SUCCESS); + break; + case 't': + args.text_mode = true; + if (args.cpi_version != CPI_VERSION_AUTO) { + fprintf(stderr, "Warning: text mode ignores version\n"); + } + break; + case 'W': + args.width = atoi(optarg); + break; + case 'H': + args.height = atoi(optarg); + break; + case 'V': + { + int v = atoi(optarg); + if ((v < -1 || v > 1) && v != 255) { + fprintf(stderr, "Error: Invalid CPI version: %d\n", args.cpi_version); + return false; + } + args.cpi_version = v == -1 ? CPI_VERSION_AUTO : v; + } + break; + case 'p': + if (0 == strcmp(optarg, "center")) { + args.placement = PLACEMENT_CENTER; + } else if (0 == strcmp(optarg, "cover")) { + args.placement = PLACEMENT_COVER; + } else if (0 == strcmp(optarg, "tile")) { + args.placement = PLACEMENT_TILE; + } else if (0 == strcmp(optarg, "full")) { + args.placement = PLACEMENT_FULL; } + else if (0 == strcmp(optarg, "extend")) { + args.placement = PLACEMENT_EXTEND; + } else if (0 == strcmp(optarg, "fill")) { + args.placement = PLACEMENT_FILL; + } else { + fprintf(stderr, "Error: invaild placement %s\n", optarg); + return false; + } + break; + } + } + + if (optind == argc) { + fprintf(stderr, "Error: no input file provided\n"); + return false; + } else if (optind + 1 == argc) { + fprintf(stderr, "Error: no output file provided\n"); + return false; + } else if ((argc - optind) != 2) { + fprintf(stderr, "Error: too many arguments\n"); + return false; + } + + args.input_path = argv[optind]; + args.output_path = argv[optind + 1]; + + const char *extension = strrchr(args.input_path, '.'); + if (!extension) { + fprintf(stderr, "Warning: no file extension, reading may fail!\n"); + } else { + bool known = false; + for (int i = 0; known_file_extensions[i] != 0; i++) { + if (0 == strcasecmp(known_file_extensions[i], extension)) { + known = true; + break; + } + } + if (!known) { + fprintf(stderr, "Warning: unknown file extension %s, reading may fail!\n", extension); + } + } + + return true; +} + +void show_help(const char *progname, bool show_all, FILE *fp) { + fprintf(fp, "usage: %s", progname); + for (int i = 0; optiondocs[i].doc != 0; i++) { + struct optiondocs doc = optiondocs[i]; + fprintf(fp, " ["); + if (doc.shortopt) fprintf(fp, "-%c", doc.shortopt); + if (doc.shortopt && doc.longopt) fprintf(fp, "|"); + if (doc.longopt) fprintf(fp, "--%s", doc.longopt); + if (doc.target) { + if (doc.shortopt || doc.longopt) fprintf(fp, " "); + fprintf(fp, "%s", doc.target); + } + fprintf(fp, "]"); + } + fprintf(fp, "\n"); + + if (!show_all) return; + + fprintf(fp, "\n"); + fprintf(fp, "ComputerCraft Palette Image converter\n"); + fprintf(fp, "\n"); + fprintf(fp, "positional arguments:\n"); + for (int i = 0; optiondocs[i].doc != 0; i++) { + struct optiondocs doc = optiondocs[i]; + if (!doc.shortopt && !doc.longopt) { + fprintf(fp, " %s\t%s\n", doc.target, doc.doc); + } + } + fprintf(fp, "\n"); + fprintf(fp, "options:\n"); + for (int i = 0; optiondocs[i].doc != 0; i++) { + struct optiondocs doc = optiondocs[i]; + if (!doc.shortopt && !doc.longopt) { continue; } + fprintf(fp, " "); + int x = 2; + if (doc.shortopt) { fprintf(fp, "-%c", doc.shortopt); x += 2; } + if (doc.shortopt && doc.longopt) { fprintf(fp, ", "); x += 2; } + if (doc.longopt) { fprintf(fp, "--%s", doc.longopt); x += strlen(doc.longopt) + 2; } + if (doc.choices) { + fprintf(fp, " {"); + for (int j = 0; doc.choices[j].value != 0; j++) { + if (j > 0) { fprintf(fp, ","); x += 1; } + fprintf(fp, "%s", doc.choices[j].value); + x += strlen(doc.choices[j].value); + } + fprintf(fp, "}"); + x += 3; + } else if (doc.target) { + fprintf(fp, " "); + fprintf(fp, "%s", doc.target); + x += strlen(doc.target) + 1; + } + if (x > 24) fprintf(fp, "\n%24c", ' '); + else fprintf(fp, "%*c", 24 - x, ' '); + fprintf(fp, "%s\n", doc.doc); + if (doc.choices) { + for (int j = 0; doc.choices[j].value != 0; j++) { + fprintf(fp, "%26c", ' '); + if (doc.shortopt) fprintf(fp, "-%c ", doc.shortopt); + else if (doc.longopt) fprintf(fp, "--%s", doc.longopt); + fprintf(fp, "%-12s %s\n", doc.choices[j].value, doc.choices[j].doc); + } + } + } +} + +struct image *image_load(const char *fp) { + struct image *img = calloc(1, sizeof(struct image)); + if (!img) return NULL; + img->pixels = (union color*)stbi_load(fp, &img->w, &img->h, 0, 4); + if (!img->pixels) { + free(img); + return NULL; + } + return img; +} + +struct image *image_new(int w, int h) { + struct image *img = calloc(1, sizeof(struct image)); + if (!img) return NULL; + img->pixels = calloc(h, sizeof(union color) * w); + img->w = w; + img->h = h; + if (!img->pixels) { + free(img); + return NULL; + } + return img; +} + +struct image *image_resize(struct image *original, int new_w, int new_h) { + struct image *resized = image_new(new_w, new_h); + if (!resized) return NULL; + stbir_resize_uint8_srgb((unsigned char *)original->pixels, original->w, original->h, 0, + (unsigned char *)resized->pixels, resized->w, resized->h, 0, + STBIR_RGBA); + return resized; +} + +void image_unload(struct image *img) { + free(img->pixels); + free(img); +} +