/*
* 2ooM: The Master of Orion II Reverse Engineering Project
* Netpbm output routines for lbximg extration.
* Copyright © 2013-2014 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 "image.h"
#include "tools.h"
#include "imgoutput.h"
/* PNM formats are ASCII, check if C exec charset is too */
#if '\a' == 0x07 && '\b' == 0x08 && '\t' == 0x09 && '\n' == 0x0a && \
'\v' == 0x0b && '\f' == 0x0c && '\r' == 0x0d && ' ' == 0x20 && \
'!' == 0x21 && '"' == 0x22 && '#' == 0x23 && '%' == 0x25 && \
'&' == 0x26 && '\'' == 0x27 && '(' == 0x28 && ')' == 0x29 && \
'*' == 0x2a && '+' == 0x2b && ',' == 0x2c && '-' == 0x2d && \
'.' == 0x2e && '/' == 0x2f && '0' == 0x30 && '1' == 0x31 && \
'2' == 0x32 && '3' == 0x33 && '4' == 0x34 && '5' == 0x35 && \
'6' == 0x36 && '7' == 0x37 && '8' == 0x38 && '9' == 0x39 && \
':' == 0x3a && ';' == 0x3b && '<' == 0x3c && '=' == 0x3d && \
'>' == 0x3e && '?' == 0x3f && 'A' == 0x41 && 'B' == 0x42 && \
'C' == 0x43 && 'D' == 0x44 && 'E' == 0x45 && 'F' == 0x46 && \
'G' == 0x47 && 'H' == 0x48 && 'I' == 0x49 && 'J' == 0x4a && \
'K' == 0x4b && 'L' == 0x4c && 'M' == 0x4d && 'N' == 0x4e && \
'O' == 0x4f && 'P' == 0x50 && 'Q' == 0x51 && 'R' == 0x52 && \
'S' == 0x53 && 'T' == 0x54 && 'U' == 0x55 && 'V' == 0x56 && \
'W' == 0x57 && 'X' == 0x58 && 'Y' == 0x59 && 'Z' == 0x5a && \
'[' == 0x5b && '\\' == 0x5c && ']' == 0x5d && '^' == 0x5e && \
'_' == 0x5f && 'a' == 0x61 && 'b' == 0x62 && 'c' == 0x63 && \
'd' == 0x64 && 'e' == 0x65 && 'f' == 0x66 && 'g' == 0x67 && \
'h' == 0x68 && 'i' == 0x69 && 'j' == 0x6a && 'k' == 0x6b && \
'l' == 0x6c && 'm' == 0x6d && 'n' == 0x6e && 'o' == 0x6f && \
'p' == 0x70 && 'q' == 0x71 && 'r' == 0x72 && 's' == 0x73 && \
't' == 0x74 && 'u' == 0x75 && 'v' == 0x76 && 'w' == 0x77 && \
'x' == 0x78 && 'y' == 0x79 && 'z' == 0x7a && '{' == 0x7b && \
'|' == 0x7c && '}' == 0x7d && '~' == 0x7e
# define NATIVE_ASCII 1
#else
# define NATIVE_ASCII 0
#endif
/* 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;
}
#if NATIVE_ASCII && defined(NDEBUG)
/* fprintf can be used directly. */
#define fprintf_ascii fprintf
#else
/* Variant of fprintf which converts all output to ASCII. */
static int fprintf_ascii(FILE *f, char *fmt, ...)
{
char buf[100];
va_list ap;
int i, len;
va_start(ap, fmt);
len = vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
if (len < 0 || len >= sizeof buf)
return -1;
for (i = 0; i < len; i++) {
if (fputc(to_ascii(buf[i]), f) == EOF)
return -1;
}
return len;
}
#endif
/*
* 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 *pixels, 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++)) {
unsigned long offset = (unsigned long) y * width + x;
bool vis = mask[offset/CHAR_BIT] & (1u << offset%CHAR_BIT);
if (fputc(to_ascii(x > 0 ? ' ' : '\n'), f) == EOF)
goto err;
if (fputc(to_ascii(vis ? '0' : '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 *pixels, 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++)) {
unsigned long offset = (unsigned long) y * width + x;
bool vis = mask[offset/CHAR_BIT] & (1u << offset%CHAR_BIT);
if (fputc(to_ascii(x > 0 ? ' ' : '\n'), f) == EOF)
return -1;
if (fprintf_ascii(f, "%3hhu", vis ? pixels[offset] : 0) < 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 *pixels, 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, pixels, 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++)) {
unsigned long offset = (unsigned long) y * width + x;
bool vis = mask[offset/CHAR_BIT] & (1u << offset%CHAR_BIT);
struct lbx_colour c = { 0 };
if (fputc(to_ascii(x > 0 ? ' ' : '\n'), f) == EOF)
goto err;
if (vis)
c = palette[pixels[offset]];
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;
}
static void pam_format_rgba(unsigned char *rowbuf, const unsigned char *pixels,
unsigned char *mask, unsigned width,
unsigned long offset, struct lbx_colour *palette)
{
unsigned x;
for (x = 0; x < width; x++) {
struct lbx_colour val = palette[pixels[offset+x]];
unsigned vis = 1 & ( mask[(offset+x)/CHAR_BIT]
>> ((offset+x)%CHAR_BIT) );
if (sizeof val == 4) {
memcpy(rowbuf+4ul*x, &val, 4);
} else {
rowbuf[4ul*x+0] = val.red;
rowbuf[4ul*x+1] = val.green;
rowbuf[4ul*x+2] = val.blue;
}
rowbuf[4ul*x+3] = -vis & 0x3f;
}
}
static void pam_format_rgb(unsigned char *rowbuf, const unsigned char *pixels,
unsigned width, unsigned long offset,
struct lbx_colour *palette)
{
unsigned x;
for (x = 0; x < width; x++) {
struct lbx_colour val = palette[pixels[offset+x]];
if (sizeof val == 4) {
memcpy(rowbuf+3ul*x, &val, 4);
} else {
rowbuf[3ul*x+0] = val.red;
rowbuf[3ul*x+1] = val.green;
rowbuf[3ul*x+2] = val.blue;
}
}
}
static void pam_format_ga(unsigned char *rowbuf,
const unsigned char *pixels, const unsigned char *mask,
unsigned width, unsigned long offset)
{
unsigned x;
for (x = 0; x < width; x++) {
unsigned vis = 1 & ( mask[(offset+x)/CHAR_BIT]
>> ((offset+x)%CHAR_BIT) );
rowbuf[2ul*x+0] = pixels[offset+x];
rowbuf[2ul*x+1] = -vis & 0xff;
}
}
static void pam_format_g(unsigned char *rowbuf, const unsigned char *pixels,
unsigned width, unsigned long offset)
{
memcpy(rowbuf, pixels+offset, width);
}
/*
* 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 *pixels, unsigned char *mask,
struct lbx_colour *palette)
{
bool masked = img_is_masked(mask, width, height);
unsigned char *rowbuf;
unsigned long offset;
unsigned y, depth;
int rc;
depth = (palette ? 3 : 1) + masked;
if (width >= (size_t)-1 / depth)
goto alloc_err;
rowbuf = malloc(width * depth + 1);
if (!rowbuf) {
alloc_err:
tool_err(-1, "failed to allocate memory");
return -1;
}
rc = fprintf_ascii(f, "P7\nWIDTH %u\nHEIGHT %u\nDEPTH %u\n",
width, height, depth);
if (rc < 0)
goto write_err;
rc = fprintf_ascii(f, "MAXVAL %d\nTUPLTYPE %s",
palette ? 63 : 255,
palette ? "RGB" : "GRAYSCALE");
if (rc < 0)
goto write_err;
rc = fprintf_ascii(f, "_ALPHA\nENDHDR\n" + 6*!masked);
if (rc < 0)
goto write_err;
for (offset = y = 0; y < height; y++, offset += width) {
switch (depth) {
case 4:
pam_format_rgba(rowbuf, pixels, mask, width, offset, palette);
break;
case 3:
pam_format_rgb(rowbuf, pixels, width, offset, palette);
break;
case 2:
pam_format_ga(rowbuf, pixels, mask, width, offset);
break;
case 1:
pam_format_g(rowbuf, pixels, width, offset);
break;
}
if (fwrite(rowbuf, depth, width, f) < width)
goto write_err;
}
free(rowbuf);
return 0;
write_err:
tool_err(0, "error writing %s", filename);
free(rowbuf);
return -1;
}