/* * 2ooM: The Master of Orion II Reverse Engineering Project * Simple command-line tool to convert an LBX image to other formats. * Copyright © 2006-2011, 2013-2014, 2021 Nick Bowler * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include "help.h" #include "tools.h" #include "image.h" #include "error.h" #include "lbx.h" #include "imgoutput.h" #define MIN(a, b) ((a) < (b) ? (a) : (b)) /* Global flags */ static int verbose = 0; static char *outname = "out"; static int usepalette = 1; #include "imgopts.h" static const char sopts[] = SOPT_STRING; static const struct option lopts[] = { LOPTS_INITIALIZER, {0} }; static void print_usage(FILE *f) { const char *progname = tool_invocation(); fprintf(f, "Usage: %s [options] [-i|-d] [frame ...]\n", progname); if (f != stdout) fprintf(f, "Try %s --help for more information.\n", progname); } static void print_help(void) { const struct option *opt; print_usage(stdout); putchar('\n'); puts("Options:"); for (opt = lopts; opt->name; opt++) { struct lopt_help help; int w; if (!lopt_get_help(opt, &help)) continue; help_print_option(opt, help.arg, help.desc, 20); } putchar('\n'); puts("For more information, see the lbximg(1) man page."); putchar('\n'); printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT); } enum { MODE_NONE, MODE_DECODE, MODE_IDENT, }; static const struct img_format { img_output_func *output; char name[4]; bool enabled; } formats[] = { #if HAVE_LIBPNG { img_output_png, "png", 1 }, #endif { img_output_pam, "pam", 1 }, { img_output_ppm, "ppm", 1 }, { img_output_pbm, "pbm", 1 }, }; static int lookup_format(const char *fmt) { for (size_t i = 0; i < sizeof formats / sizeof formats[0]; i++) { assert(!formats[i].name[sizeof formats[i].name - 1]); if (!fmt && formats[i].enabled) return i; if (strcmp(formats[i].name, fmt)) continue; if (!formats[i].enabled) { tool_err(-1, "%s support disabled at build time", fmt); return -1; } return i; } tool_err(-1, "unknown format %s", fmt); return -1; } bool img_is_masked(unsigned char *mask, unsigned width, unsigned height) { unsigned long npixels = (unsigned long) width * height; unsigned long mask_sz = npixels / CHAR_BIT + (npixels % CHAR_BIT != 0); for (unsigned long i = 0; i < mask_sz; i++) { if (i+1 < mask_sz) { if (mask[i] != (unsigned char)-1) return true; } else { unsigned char test = (1u << npixels % CHAR_BIT) - 1; if ((mask[i] & test) != test) return true; } } return false; } int parserange(unsigned frames, char *str, unsigned char *bits) { unsigned long start, end; unsigned int i; char *endptr; start = strtoul(str, &endptr, 0); if (start >= frames) { tool_err(-1, "frame %lu out of range.", start); return -1; } if (endptr == str) { tool_err(-1, "invalid frame range: %s.", str); return -1; } switch (*endptr) { case '\0': end = start; break; case '-': end = strtoul(endptr+1, &endptr, 0); if (end >= frames) { tool_err(-1, "frame %lu out of range.", end); return -1; } if (endptr == str) end = frames - 1; break; default: tool_err(-1, "invalid frame range: %s.", str); return -1; } if (end < start) { tool_err(-1, "invalid frame range: %s.", str); return -1; } for (i = start; i <= end; i++) { bits[i / CHAR_BIT] |= 1 << (i % CHAR_BIT); } return 0; } static int output(unsigned int frameno, const struct img_format *fmt, unsigned char *pixels, unsigned char *pixel_mask, unsigned int width, unsigned int height, struct lbx_colour *palette) { char name[strlen(outname) + sizeof ".65535.png"]; FILE *of; int rc; assert(fmt->output != NULL); assert(frameno < 65536); snprintf(name, sizeof name, "%s.%03d.%s", outname, frameno, fmt->name); of = fopen(name, "wb"); if (!of) { tool_err(0, "failed to open %s", name); return -1; } rc = fmt->output(of, name, width, height, pixels, pixel_mask, palette); if (rc < 0) { fclose(of); return -1; } if (fclose(of) == EOF) { tool_err(0, "error writing %s", name); return -1; } if (verbose) printf("wrote %s\n", name); return 0; } static int loadoverride(FILE *f, struct lbx_colour *palette) { struct lbx_image *img; int rc, ret = 0; img = lbx_img_open(f, &lbx_default_fops, NULL); if (!img) { tool_err(-1, "failed to open override image: %s", lbx_errmsg()); return -1; } rc = lbx_img_getpalette(img, palette); if (rc < 0) { tool_err(-1, "error reading override palette: %s", lbx_errmsg()); ret = -1; } else if (rc == 0) { tool_err(-1, "override image has no palette."); ret = -1; } lbx_img_close(img); return ret; } static int loadpalette(struct lbx_image *img, FILE *palf, FILE *override, struct lbx_colour *palette) { int rc, ret = -1; /* Default the palette to a wonderful pink. */ for (unsigned i = 0; i < 256; i++) { palette[i] = (struct lbx_colour){0x3f, 0x00, 0x3f}; } /* Read the external palette, if any. */ if (palf) { rc = lbx_img_loadpalette(palf, &lbx_default_fops, palette); if (rc < 0) { tool_err(-1, "error reading external palette: %s", lbx_errmsg()); return -1; } ret = 0; } /* Read the embedded palette */ rc = lbx_img_getpalette(img, palette); if (rc < 0) { tool_err(-1, "error reading embedded palette: %s", lbx_errmsg()); return -1; } else if (rc > 0) { ret = 0; } /* Read the override palette, if any. */ if (override) { rc = loadoverride(override, palette); if (rc < 0) return -1; ret = 0; } /* If we literally have no palette data at all, may as well fail. */ if (ret < 0) tool_err(-1, "no palette available."); return ret; } /* Return true iff a divides b. */ static bool divides(unsigned a, unsigned b) { if (b == 0) return true; if (a == 0) return false; return b % a == 0; } /* Set n bits starting from offset in the bitmap. */ static void set_bits(unsigned char *bitmap, unsigned long offset, unsigned long n) { if (offset % CHAR_BIT) { bitmap[offset/CHAR_BIT] |= (n >= CHAR_BIT ? -1u : (1u << n) - 1) << offset%CHAR_BIT; n -= MIN(n, CHAR_BIT - offset%CHAR_BIT); offset += CHAR_BIT - offset%CHAR_BIT; } if (n > CHAR_BIT) { memset(&bitmap[offset/CHAR_BIT], -1, n/CHAR_BIT); offset += n - n%CHAR_BIT; n %= CHAR_BIT; } if (n) { bitmap[offset/CHAR_BIT] |= (1u << n) - 1; } } static int decode_frame(struct lbx_image *img, unsigned n, unsigned char *pixels, unsigned char *pixel_mask) { unsigned x, y; long rc; rc = lbx_img_seek(img, n); if (rc < 0) { tool_err(-1, "frame %u: invalid frame: %s\n", n, lbx_errmsg()); return -1; } while ((rc = lbx_img_read_row_header(img, &x, &y)) != 0) { unsigned long offset; if (rc < 0) { tool_err(-1, "frame %u: invalid row: %s", n, lbx_errmsg()); return -1; } offset = (unsigned long) y * img->width + x; rc = lbx_img_read_row_data(img, pixels+offset); if (rc < 0) { tool_err(-1, "frame %u: error reading row: %s\n", n, lbx_errmsg()); return -1; } set_bits(pixel_mask, offset, rc); } return 0; } static int decode(struct lbx_image *img, FILE *palf, FILE *override, int fmt, char **argv) { unsigned char *pixels = NULL, *pixel_mask = NULL, *framebits = NULL; struct lbx_colour palette[256]; int rc, ret = EXIT_FAILURE; int extracted = 0; unsigned int i; size_t npixels, mask_sz; assert(fmt >= 0 && fmt < sizeof formats / sizeof formats[0]); npixels = img->width; if (img->height && npixels >= (size_t)-1 / img->height) { tool_err(-1, "image too large"); goto err; } npixels *= img->height; /* Ensure there is at least 1 byte to allocate */ if (npixels == 0) npixels = 1; framebits = calloc(1, img->frames / CHAR_BIT + 1); if (!framebits) { tool_err(0, "failed to allocate memory"); goto err; } pixels = calloc(img->width, img->height); if (!pixels) { tool_err(0, "failed to allocate memory"); goto err; } mask_sz = npixels / CHAR_BIT + (npixels % CHAR_BIT != 0); pixel_mask = malloc(mask_sz); if (!pixel_mask) { tool_err(0, "failed to allocate memory"); goto err; } /* Figure out what images we're extracting. */ if (!argv[0]) { /* extract all images by default. */ memset(framebits, -1, img->frames / CHAR_BIT + 1); } else { for (i = 0; argv[i]; i++) { parserange(img->frames, argv[i], framebits); } } if (usepalette) { if (loadpalette(img, palf, override, palette) == -1) { ret = EXIT_FAILURE; goto err; } } /* Extract the images, in order. */ ret = EXIT_SUCCESS; for (i = 0; i < img->frames; i++) { if (divides(img->chunk, i)) memset(pixel_mask, 0, mask_sz); rc = decode_frame(img, i, pixels, pixel_mask); if (rc < 0) { ret = EXIT_FAILURE; goto err; } if (framebits[i / CHAR_BIT] & (1u << (i % CHAR_BIT))) { rc = output(i, &formats[fmt], pixels, pixel_mask, img->width, img->height, usepalette ? palette : NULL); if (rc == 0) { extracted = 1; } } } if (!extracted) { tool_err(-1, "no frames extracted."); ret = EXIT_FAILURE; } err: free(pixels); free(pixel_mask); free(framebits); return ret; } int main(int argc, char **argv) { int mode = MODE_NONE, fmt, opt, rc = EXIT_FAILURE; struct lbx_pipe_state stdin_handle = { .f = stdin }; const char *file = NULL, *fmtstring = NULL; const char *ext_palette = NULL, *ovr_palette = NULL; FILE *palf = NULL, *overf = NULL; struct lbx_image *img; tool_init("lbximg", argc, argv); while ((opt = getopt_long(argc, argv, sopts, lopts, NULL)) != -1) { switch(opt) { case 'i': mode = MODE_IDENT; break; case 'd': mode = MODE_DECODE; break; case 'v': verbose = 1; break; case 'F': fmtstring = optarg; break; case 'f': file = optarg; break; case 'n': usepalette = 0; break; case 'p': ext_palette = optarg; break; case 'O': ovr_palette = optarg; break; case LOPT_OUTPUT_PREFIX: outname = optarg; break; case 'V': tool_version(); return EXIT_SUCCESS; case 'H': print_help(); return EXIT_SUCCESS; default: return EXIT_FAILURE; } } if (mode == MODE_NONE) { tool_err(-1, "you must specify a mode."); return EXIT_FAILURE; } fmt = lookup_format(fmtstring); if (fmt < 0) return EXIT_FAILURE; if (file) img = lbx_img_fopen(file); else img = lbx_img_open(&stdin_handle, &lbx_pipe_fops, NULL); if (!img) { tool_err(-1, "failed to open image: %s.", lbx_errmsg()); return EXIT_FAILURE; } if (ext_palette && !(palf = fopen(ext_palette, "rb"))) { tool_err(0, "failed to open %s", optarg); return EXIT_FAILURE; } if (ovr_palette && !(overf = fopen(ovr_palette, "rb"))) { tool_err(0, "failed to open %s", optarg); return EXIT_FAILURE; } if (verbose || mode == MODE_IDENT) { int palette_count; if (!file) file = "stdin"; palette_count = lbx_img_getpalette(img, NULL); if (palette_count < 0) { tool_err(-1, "error reading image: %s", lbx_errmsg()); return EXIT_FAILURE; } printf("%s is %hux%hu LBX image, %hhu frame(s)%s%s%s\n", file, img->width, img->height, img->frames, palette_count ? ", embedded palette" : "", img->chunk ? ", chunked" : "", img->leadin+1 < img->frames ? ", loops" : ""); } switch (mode) { case MODE_IDENT: rc = 0; break; case MODE_DECODE: rc = decode(img, palf, overf, fmt, &argv[optind]); break; } lbx_img_close(img); if (palf) fclose(palf); if (overf) fclose(overf); return rc; }