+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <inttypes.h>
+
+#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;
+}