From af13a143c8bfcd713216b1382448174e21b9f278 Mon Sep 17 00:00:00 2001 From: Nick Bowler Date: Fri, 14 Jun 2013 18:11:46 -0400 Subject: [PATCH] lbximg: Add support for Netpbm output. These netpbm formats are extremely simple image formats which can be written with a very small amount of code. Implement the ASCII PBM and PPM formats for eventual use in test scripts, and PAM as a fully-featured format so that we can make libpng optional. --- Makefile.am | 2 +- doc/man/lbximg.1 | 54 +++++-- src/imgoutput.h | 15 +- src/lbximg.c | 85 +++++++++-- src/png.c | 14 +- src/pnm.c | 357 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 486 insertions(+), 41 deletions(-) create mode 100644 src/pnm.c diff --git a/Makefile.am b/Makefile.am index 8cfec29..48398a6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -37,7 +37,7 @@ lbxtool_SOURCES = src/lbxtool.c src/tools.c lbxtool_LDADD = liblbx.la libgnu.la $(lbxtool_OBJECTS): $(gnulib_headers) -lbximg_SOURCES = src/lbximg.c src/tools.c src/png.c +lbximg_SOURCES = src/lbximg.c src/tools.c src/png.c src/pnm.c lbximg_LDADD = liblbx.la libgnu.la $(LIBPNG_LIBS) $(lbximg_OBJECTS): $(gnulib_headers) diff --git a/doc/man/lbximg.1 b/doc/man/lbximg.1 index 87bd3ee..f3ecece 100644 --- a/doc/man/lbximg.1 +++ b/doc/man/lbximg.1 @@ -1,9 +1,4 @@ -.\" Copyright (C) 2008-2010 Nick Bowler -.\" Copying and distribution of this file, with or without modification, -.\" are permitted in any medium without royalty provided the copyright -.\" notice and this notice are preserved. This file is offered as-is, -.\" without any warranty. -.Dd February 9, 2010 +.Dd June 14, 2013 .Os liblbx .Dt LBXIMG \&1 "2ooM Reference Manual" .Sh NAME @@ -14,6 +9,7 @@ .Op Fl i Ns | Ns Fl d .Op Fl v .Op Fl n +.Op Fl F Ar format .Op Fl p Ar palette_file .Op Fl O Ar override_file .Op Fl f Ar path @@ -24,7 +20,8 @@ identifies and decodes LBX image files, using .Em liblbx . LBX images are multi-frame, 256-colour paletted images with transparency. .Nm -can be used to convert some or all of the frames of an LBX image to PNG. +can be used to convert some or all of the frames of an LBX image to other image +formats. .Sh OPTIONS .Bl -tag -width indent .It Fl i , -ident @@ -33,14 +30,44 @@ Sets the operating mode to identify the image format. Sets the operating mode to decode frames to PNG. .It Fl v , -verbose Output additional information on standard output. +.It Fl F , -format Ar format +Select the desired output format. Some formats may not be available depending +on the compile-time settings of +.Nm . +If this option is not specified, the default is the first in the following list +which is enabled at build time. +.Bl -column -offset indent ".Em Format" +.It Em Format Ta Em Description +.It png Ta +Output images in Portable Network Graphics (PNG) format. This is is a +compressed format which is well-supported by other tools. All features of +.Nm +are supported with this format. +.It pam Ta +Output images in Netpbm PAM format. This is a simple uncompressed binary image +format supporting RGB and alpha channels. All features of +.Nm +are supported with this format. +.It ppm Ta +Output image colour data in Netpbm "plain" PPM format. This is a simple +7-bit clean uncompressed RGB format. It does not support transparency, so +images will have transparent pixels replaced with black. This format is +rather inefficient and provided mainly for testing. +.It pbm Ta +Output image mask data in Netpbm "plain" PBM format. This is a simple 7-bit +clean uncompressed bitmap format. It does not support colour data; instead, +bitmap values represent whether or not a pixel is transparent. Black (1) +pixels are transparent, white (0) pixels are opaque. This format is extremely +inefficient and provided mainly for testing. +.El .It Fl f , -file Ar path Read from the specified .Ar path instead of standard input. .It Fl n , -no-palette -Instead of looking up colour indices in the palette, use the index itself for -each of the red, green and blue components of the output. This is mainly -useful for debugging +Instead of looking up colour indices in the palette, emit a grayscale image +where the intensity value of a pixel is equal to the palette index itself. +This is mainly useful for debugging the image decoder in .Em liblbx . .It Fl p , -palette Ar palette_file Read the base palette from @@ -90,6 +117,13 @@ Decodes all the frames of the Microprose logo into a series of PNGs. .It Nm Li -dvf ships.lbx.042 -p fonts.lbx.012 -O ships.lbx.049 Decodes an image of a red star fortress. .El +.Sh AUTHORS +Nick Bowler +.Sh COPYRIGHT +Copyright \(co 2008\(en2010, 2013 Nick Bowler +.Pp +Permission is granted to copy, distribute and/or modify this manual under the +terms of the Do What The Fuck You Want To Public License, version 2. .Sh SEE ALSO .Xr lbxtool 1 , .Xr lbxgui 1 diff --git a/src/imgoutput.h b/src/imgoutput.h index 46d9c9a..bb0b2a5 100644 --- a/src/imgoutput.h +++ b/src/imgoutput.h @@ -1,9 +1,16 @@ #ifndef IMGOUTPUT_H_ #define IMGOUTPUT_H_ -int img_output_png(FILE *f, const char *filename, - unsigned width, unsigned height, - unsigned char **framedata, unsigned char **mask, - struct lbx_colour *palette); +#include "image.h" + +typedef int img_output_func(FILE *f, const char *filename, + unsigned width, unsigned height, + unsigned char **framedata, unsigned char **mask, + struct lbx_colour *palette); + +img_output_func img_output_pbm, img_output_ppm, img_output_pam; +img_output_func img_output_png; + +_Bool img_is_masked(unsigned char **mask, unsigned width, unsigned height); #endif diff --git a/src/lbximg.c b/src/lbximg.c index 6c54b0f..ada1692 100644 --- a/src/lbximg.c +++ b/src/lbximg.c @@ -1,6 +1,6 @@ /* * 2ooM: The Master of Orion II Reverse Engineering Project - * Simple command-line tool to convert an LBX image to a set of PNGs. + * Simple command-line tool to convert an LBX image to other formats. * Copyright © 2006-2011, 2013 Nick Bowler * * This program is free software: you can redistribute it and/or modify @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -41,7 +42,7 @@ static void printusage(void) { puts("usage: lbximg [-i|-d] [-v] [-p palette_file] [-O override_file]" " [-f path]"); - puts(" [frameno ...]"); + puts(" [-F format] [frameno ...]"); } static void printhelp(void) @@ -56,6 +57,52 @@ enum { MODE_IDENT, }; +static const struct img_format { + img_output_func *output; + char name[4]; + bool enabled; +} formats[] = { + { img_output_png, "png", 1 }, + { 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 x, y; + + for (x = y = 0; y < height; ++x < width || (x = 0, y++)) { + if (mask[y][x] == 0) + return true; + } + + return false; +} + int parserange(unsigned frames, char *str, unsigned char *bits) { unsigned long start, end; @@ -104,7 +151,7 @@ int parserange(unsigned frames, char *str, unsigned char *bits) return 0; } -int outpng(unsigned int frameno, +int output(unsigned int frameno, const struct img_format *fmt, unsigned char **framedata, unsigned char **mask, unsigned int width, unsigned int height, struct lbx_colour palette[static 256]) @@ -113,8 +160,9 @@ int outpng(unsigned int frameno, FILE *of; int rc; + assert(fmt->output != NULL); assert(frameno < 65536); - snprintf(name, sizeof name, "%s.%03d.png", outname, frameno); + snprintf(name, sizeof name, "%s.%03d.%s", outname, frameno, fmt->name); of = fopen(name, "wb"); if (!of) { @@ -122,7 +170,7 @@ int outpng(unsigned int frameno, return -1; } - rc = img_output_png(of, name, width, height, framedata, mask, palette); + rc = fmt->output(of, name, width, height, framedata, mask, palette); if (rc < 0) { fclose(of); return -1; @@ -203,7 +251,8 @@ static int loadpalette(struct lbx_image *img, struct lbx_imginfo *info, return 0; } -int decode(struct lbx_image *img, FILE *palf, FILE *override, char **argv) +static int +decode(struct lbx_image *img, FILE *palf, FILE *override, int fmt, char **argv) { unsigned char *framebits; struct lbx_colour palette[256]; @@ -211,6 +260,8 @@ int decode(struct lbx_image *img, FILE *palf, FILE *override, char **argv) int extracted = 0; unsigned int i; + assert(fmt >= 0 && fmt < sizeof formats / sizeof formats[0]); + lbx_img_getinfo(img, &info); framebits = calloc(1, img->frames / CHAR_BIT + 1); @@ -250,7 +301,8 @@ int decode(struct lbx_image *img, FILE *palf, FILE *override, char **argv) mask = lbx_img_getmask(img); - if (!outpng(i, data, mask, img->width, img->height, + if (!output(i, &formats[fmt], data, mask, + img->width, img->height, usepalette ? palette : NULL)) { extracted = 1; } @@ -270,18 +322,19 @@ err: int main(int argc, char **argv) { - int mode = MODE_NONE, opt, rc = EXIT_FAILURE; + int mode = MODE_NONE, fmt, opt, rc = EXIT_FAILURE; struct lbx_pipe_state stdin_handle = { .f = stdin }; + const char *file = NULL, *fmtstring = NULL; FILE *palf = NULL, *overf = NULL; - const char *file = NULL; struct lbx_image *img; - static const char *sopts = "idnvf:p:O:V"; + static const char sopts[] = "idnvF:f:p:O:VH"; static const struct option lopts[] = { { "ident", 0, NULL, 'i' }, { "decode", 0, NULL, 'd' }, { "verbose", 0, NULL, 'v' }, { "file", 1, NULL, 'f' }, + { "format", 1, NULL, 'F' }, { "palette", 1, NULL, 'p' }, { "override", 1, NULL, 'p' }, @@ -306,6 +359,9 @@ int main(int argc, char **argv) case 'v': verbose = 1; break; + case 'F': + fmtstring = optarg; + break; case 'f': file = optarg; break; @@ -336,8 +392,7 @@ int main(int argc, char **argv) case 'H': printhelp(); return EXIT_SUCCESS; - case '?': - case ':': + default: return EXIT_FAILURE; } } @@ -347,6 +402,10 @@ int main(int argc, char **argv) return EXIT_FAILURE; } + fmt = lookup_format(fmtstring); + if (fmt < 0) + return EXIT_FAILURE; + if (file) img = lbx_img_fopen(file); else @@ -373,7 +432,7 @@ int main(int argc, char **argv) switch (mode) { case MODE_DECODE: - rc = decode(img, palf, overf, &argv[optind]); + rc = decode(img, palf, overf, fmt, &argv[optind]); break; } diff --git a/src/png.c b/src/png.c index 83aeec4..c52db03 100644 --- a/src/png.c +++ b/src/png.c @@ -37,18 +37,6 @@ struct file_info { FILE *f; }; -static int is_masked(unsigned char **mask, unsigned width, unsigned height) -{ - for (unsigned y = 0; y < height; y++) { - for (unsigned x = 0; x < width; x++) { - if (mask[y][x] == 0) - return 1; - } - } - - return 0; -} - static void fatal(png_structp png, int err, const char *fmt, ...) { va_list ap; @@ -220,7 +208,7 @@ int img_output_png(FILE *f, const char *filename, unsigned char **framedata, unsigned char **mask, struct lbx_colour *palette) { - bool masked = is_masked(mask, width, height); + bool masked = img_is_masked(mask, width, height); struct file_info file = { filename, f }; png_structp png; png_infop info; diff --git a/src/pnm.c b/src/pnm.c new file mode 100644 index 0000000..865d2a1 --- /dev/null +++ b/src/pnm.c @@ -0,0 +1,357 @@ +/* + * 2ooM: The Master of Orion II Reverse Engineering Project + * Netpbm output routines for lbximg extration. + * Copyright © 2013 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 "image.h" +#include "tools.h" +#include "imgoutput.h" + +/* Convert c from the basic execution character set to ASCII. */ +static unsigned char to_ascii(unsigned char c) +{ + switch (c) { + case'\a': return 0x07; + case'\b': return 0x08; + case'\t': return 0x09; + case'\n': return 0x0a; + case'\v': return 0x0b; + case'\f': return 0x0c; + case'\r': return 0x0d; + case ' ': return 0x20; + case '!': return 0x21; + case '"': return 0x22; + case '#': return 0x23; + case '%': return 0x25; + case '&': return 0x26; + case'\'': return 0x27; + case '(': return 0x28; + case ')': return 0x29; + case '*': return 0x2a; + case '+': return 0x2b; + case ',': return 0x2c; + case '-': return 0x2d; + case '.': return 0x2e; + case '/': return 0x2f; + case '0': return 0x30; + case '1': return 0x31; + case '2': return 0x32; + case '3': return 0x33; + case '4': return 0x34; + case '5': return 0x35; + case '6': return 0x36; + case '7': return 0x37; + case '8': return 0x38; + case '9': return 0x39; + case ':': return 0x3a; + case ';': return 0x3b; + case '<': return 0x3c; + case '=': return 0x3d; + case '>': return 0x3e; + case '?': return 0x3f; + case 'A': return 0x41; + case 'B': return 0x42; + case 'C': return 0x43; + case 'D': return 0x44; + case 'E': return 0x45; + case 'F': return 0x46; + case 'G': return 0x47; + case 'H': return 0x48; + case 'I': return 0x49; + case 'J': return 0x4a; + case 'K': return 0x4b; + case 'L': return 0x4c; + case 'M': return 0x4d; + case 'N': return 0x4e; + case 'O': return 0x4f; + case 'P': return 0x50; + case 'Q': return 0x51; + case 'R': return 0x52; + case 'S': return 0x53; + case 'T': return 0x54; + case 'U': return 0x55; + case 'V': return 0x56; + case 'W': return 0x57; + case 'X': return 0x58; + case 'Y': return 0x59; + case 'Z': return 0x5a; + case '[': return 0x5b; + case'\\': return 0x5c; + case ']': return 0x5d; + case '^': return 0x5e; + case '_': return 0x5f; + case 'a': return 0x61; + case 'b': return 0x62; + case 'c': return 0x63; + case 'd': return 0x64; + case 'e': return 0x65; + case 'f': return 0x66; + case 'g': return 0x67; + case 'h': return 0x68; + case 'i': return 0x69; + case 'j': return 0x6a; + case 'k': return 0x6b; + case 'l': return 0x6c; + case 'm': return 0x6d; + case 'n': return 0x6e; + case 'o': return 0x6f; + case 'p': return 0x70; + case 'q': return 0x71; + case 'r': return 0x72; + case 's': return 0x73; + case 't': return 0x74; + case 'u': return 0x75; + case 'v': return 0x76; + case 'w': return 0x77; + case 'x': return 0x78; + case 'y': return 0x79; + case 'z': return 0x7a; + case '{': return 0x7b; + case '|': return 0x7c; + case '}': return 0x7d; + case '~': return 0x7e; + default: assert((tool_err(-1, "invalid codepoint: %hhx", c), 0)); + } + + return c; +} + +/* Printf variant which converts all output to ASCII. */ +static int fprintf_ascii(FILE *f, char *fmt, ...) +{ + unsigned char *buf; + va_list ap; + int rc, len; + size_t ret; + + va_start(ap, fmt); + rc = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + if (rc < 0) + return -1; + + assert(rc < SIZE_MAX); + buf = malloc(rc+1u); + if (!buf) + return -1; + + va_start(ap, fmt); + len = vsprintf((char *)buf, fmt, ap); + va_end(ap); + + assert(rc == len); + for (int i = 0; i < len; i++) { + buf[i] = to_ascii(buf[i]); + } + + ret = fwrite(buf, 1, len, f); + free(buf); + + if (ret < len) + return -(int)ret; + return ret; +} + +/* + * Output filter for Netpbm's "plain" PBM format. This is a bitmap format + * which is not really suitable for image data, but it can output the image + * transparency mask, and thus complements the PPM output. The image colour + * data is lost in the conversion. + */ +int img_output_pbm(FILE *f, const char *filename, + unsigned width, unsigned height, + unsigned char **framedata, unsigned char **mask, + struct lbx_colour *palette) +{ + unsigned x, y; + + if (fprintf_ascii(f, "P1\n%u %u", width, height) < 0) + goto err; + + for (x = y = 0; y < height; ++x < width || (x = 0, y++)) { + if (fputc(to_ascii(x > 0 ? ' ' : '\n'), f) == EOF) + goto err; + + if (mask[y][x]) { + if (fputc(to_ascii('0'), f) == EOF) + goto err; + } else { + if (fputc(to_ascii('1'), f) == EOF) + goto err; + } + } + + if (fputc(to_ascii('\n'), f) == EOF) + goto err; + return 0; +err: + tool_err(0, "error writing %s", filename); + return -1; +} + +/* + * Helper for Netpbm's "plain" PGM output. This is only meant for no-palette + * mode as normally LBX images are not grayscale, so it is not available as a + * normal output format. + */ +static int write_pgm(FILE *f, unsigned width, unsigned height, + unsigned char **framedata, unsigned char **mask) +{ + unsigned x, y; + + if (fprintf_ascii(f, "P2\n%u %u\n255", width, height) < 0) + return -1; + + for (x = y = 0; y < height; ++x < width || (x = 0, y++)) { + if (fputc(to_ascii(x > 0 ? ' ' : '\n'), f) == EOF) + return -1; + + if (!mask[y][x]) { + if (fprintf_ascii(f, " 0") < 0) + return -1; + } else { + if (fprintf_ascii(f, "%3hhu", framedata[y][x]) < 0) + return -1; + } + } + + if (fputc(to_ascii('\n'), f) == EOF) + return -1; + + return 0; +} + +/* + * Output filter for Netpbm's "plain" PPM format. This supports RGB but not + * transparency. As a pure text format, it is useful for testing but is not + * particularly efficient in terms of storage. The image mask is lost in + * the conversion (output pixels will be black). + */ +int img_output_ppm(FILE *f, const char *filename, + unsigned width, unsigned height, + unsigned char **framedata, unsigned char **mask, + struct lbx_colour *palette) +{ + unsigned x, y; + + if (!palette) { + /* + * For no-palette mode, write a PGM instead which is basically + * the same format but has only one value per pixel. + */ + if (write_pgm(f, width, height, framedata, mask) < 0) + goto err; + return 0; + } + + if (fprintf_ascii(f, "P3\n%u %u\n63", width, height) < 0) + goto err; + + for (x = y = 0; y < height; ++x < width || (x = 0, y++)) { + if (fputc(to_ascii(x > 0 ? ' ' : '\n'), f) == EOF) + goto err; + + if (!mask[y][x]) { + if (fprintf_ascii(f, " 0 0 0") < 0) + goto err; + } else { + struct lbx_colour *c = &palette[framedata[y][x]]; + + if (fprintf_ascii(f, "%2d %2d %2d", + c->red, c->green, c->blue) < 0) + goto err; + } + } + + if (fputc(to_ascii('\n'), f) == EOF) + goto err; + return 0; +err: + tool_err(0, "error writing %s", filename); + return -1; +} + +/* + * Output filter for Netpbm's PAM format. This format combines the features + * of all the other Netpbm formats, supporting RGB and grayscale images with + * or without alpha channels. This format supports all lbximg output. + */ +int img_output_pam(FILE *f, const char *filename, + unsigned width, unsigned height, + unsigned char **framedata, unsigned char **mask, + struct lbx_colour *palette) +{ + bool masked = img_is_masked(mask, width, height); + unsigned x, y; + size_t depth; + + if (fprintf_ascii(f, "P7\nWIDTH %u\nHEIGHT %u\n", width, height) < 0) + goto err; + + if (palette) { + depth = masked ? 4 : 3; + + if (fprintf_ascii(f, "DEPTH %zu\nMAXVAL 63\n" + "TUPLTYPE RGB%s\nENDHDR\n", + depth, masked ? "_ALPHA" : "") < 0) + goto err; + + for (x = y = 0; y < height; ++x < width || (x = 0, y++)) { + struct lbx_colour *c = &palette[framedata[y][x]]; + unsigned char buf[4]; + + buf[0] = c->red; + buf[1] = c->green; + buf[2] = c->blue; + buf[3] = mask[y][x] ? 63 : 0; + + if (fwrite(buf, 1, depth, f) < depth) + goto err; + } + } else { + depth = masked ? 2 : 1; + + if (fprintf_ascii(f, "DEPTH %zu\nMAXVAL 255\n" + "TUPLTYPE GRAYSCALE%s\nENDHDR\n", + depth, masked ? "_ALPHA" : "") < 0) + goto err; + + for (x = y = 0; y < height; ++x < width || (x = 0, y++)) { + unsigned char buf[2]; + + buf[0] = framedata[y][x]; + buf[1] = mask[y][x] ? 0xff : 0; + + if (fwrite(buf, 1, depth, f) < depth) + goto err; + } + } + + return 0; +err: + tool_err(0, "error writing %s", filename); + return -1; +} -- 2.43.0