/*
* 2ooM: The Master of Orion II Reverse Engineering Project
* PNG 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
#include "image.h"
#include "tools.h"
#include "imgoutput.h"
struct file_info {
const char *filename;
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;
va_start(ap, fmt);
tool_verr(err, fmt, ap);
va_end(ap);
png_longjmp(png, 1);
}
static void warning_fn(png_structp png, const char *msg)
{
tool_err(-1, "libpng warning: %s", msg);
}
static void error_fn(png_structp png, const char *msg)
{
fatal(png, -1, "libpng error: %s", msg);
}
/*
* We implement our own I/O routines because libpng's internal stdio calls do
* not handle errors particularly well.
*/
static void write_data(png_structp png, png_bytep data, png_size_t len)
{
struct file_info *file = png_get_io_ptr(png);
size_t ret;
ret = fwrite(data, 1, len, file->f);
if (ret < len) {
fatal(png, 0, "error writing %s", file->filename);
}
}
static void flush_data(png_structp png)
{
struct file_info *file = png_get_io_ptr(png);
if (fflush(file->f) == EOF) {
fatal(png, 0, "error writing %s", file->filename);
}
}
static inline unsigned scale6to8(unsigned x)
{
assert(x <= 0x3f);
return x*0xff / 0x3f;
}
/* Scale the 18-bit LBX palette into a 24-bit PNG palette. */
static void fill_png_palette(png_color *out, const struct lbx_colour *in)
{
for (unsigned i = 0; i < 256; i++) {
out[i] = (struct png_color_struct) {
.red = scale6to8(in[i].red),
.green = scale6to8(in[i].green),
.blue = scale6to8(in[i].blue),
};
}
}
static void
write_rgba_frame(png_structp png, png_infop info,
unsigned char **framedata, unsigned char **mask,
struct lbx_colour *lbx_palette)
{
png_uint_32 width = png_get_image_width(png, info);
png_uint_32 height = png_get_image_height(png, info);
png_color png_palette[256];
unsigned char (*row)[4];
jmp_buf parent;
if (width >= PNG_UINT_32_MAX / sizeof *row)
fatal(png, -1, "image too wide to allocate row buffer");
row = png_malloc(png, width * sizeof *row);
/*
* We need to establish our own error handler to free the row buffer.
* Some care must be taken to save/restore the caller's handler.
*/
memcpy(&parent, &png_jmpbuf(png), sizeof parent);
if (setjmp(png_jmpbuf(png))) {
png_free(png, row);
memcpy(&png_jmpbuf(png), &parent, sizeof parent);
png_longjmp(png, 1);
}
png_write_info(png, info);
fill_png_palette(png_palette, lbx_palette);
for (png_uint_32 y = 0; y < height; y++) {
for (png_uint_32 x = 0; x < width; x++) {
row[x][0] = png_palette[framedata[y][x]].red;
row[x][1] = png_palette[framedata[y][x]].green;
row[x][2] = png_palette[framedata[y][x]].blue;
row[x][3] = mask[y][x] ? -1 : 0;
}
png_write_row(png, (void *)row);
}
png_write_end(png, NULL);
memcpy(&png_jmpbuf(png), &parent, sizeof parent);
}
static void
write_masked_index_frame(png_structp png, png_infop info,
unsigned char **framedata, unsigned char **mask)
{
png_uint_32 width = png_get_image_width(png, info);
png_uint_32 height = png_get_image_height(png, info);
unsigned char (*row)[2];
jmp_buf parent;
if (width >= PNG_UINT_32_MAX / sizeof *row)
fatal(png, -1, "image too wide to allocate row buffer");
row = png_malloc(png, width * sizeof *row);
/*
* We need to establish our own error handler to free the row buffer.
* Some care must be taken to save/restore the caller's handler.
*/
memcpy(&parent, &png_jmpbuf(png), sizeof parent);
if (setjmp(png_jmpbuf(png))) {
png_free(png, row);
memcpy(&png_jmpbuf(png), &parent, sizeof parent);
png_longjmp(png, 1);
}
png_write_info(png, info);
for (png_uint_32 y = 0; y < height; y++) {
for (png_uint_32 x = 0; x < width; x++) {
row[x][0] = framedata[y][x];
row[x][1] = mask[y][x] ? -1 : 0;
}
png_write_row(png, (void *)row);
}
png_write_end(png, NULL);
memcpy(&png_jmpbuf(png), &parent, sizeof parent);
}
static void
write_palette_frame(png_structp png, png_infop info,
unsigned char **framedata, struct lbx_colour *lbx_palette)
{
png_color png_palette[256];
fill_png_palette(png_palette, lbx_palette);
png_set_PLTE(png, info, png_palette, 256);
png_set_rows(png, info, framedata);
png_write_png(png, info, PNG_TRANSFORM_IDENTITY, NULL);
}
static void
write_index_frame(png_structp png, png_infop info, unsigned char **framedata)
{
png_set_rows(png, info, framedata);
png_write_png(png, info, PNG_TRANSFORM_IDENTITY, NULL);
}
int img_output_png(FILE *f, const char *filename,
unsigned width, unsigned height,
unsigned char **framedata, unsigned char **mask,
struct lbx_colour *palette)
{
bool masked = is_masked(mask, width, height);
struct file_info file = { filename, f };
png_structp png;
png_infop info;
png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) {
tool_err(-1, "failed to init libpng.");
return -1;
}
info = png_create_info_struct(png);
if (!info) {
tool_err(-1, "failed to init libpng.");
png_destroy_write_struct(&png, NULL);
return -1;
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
return -1;
}
png_set_error_fn(png, NULL, error_fn, warning_fn);
png_set_write_fn(png, &file, write_data, flush_data);
if (palette && masked) {
/*
* Indexed colour mode can only be used for images without
* transparency, since indexed PNG requires sacrificing one or
* more palette colours to indicate transparency, while LBX
* does not. So we use full-up RGBA in this case.
*/
png_set_IHDR(png, info, width, height, 8,
PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
write_rgba_frame(png, info, framedata, mask, palette);
} else if (palette) {
png_set_IHDR(png, info, width, height, 8,
PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
write_palette_frame(png, info, framedata, palette);
} else if (masked) {
png_set_IHDR(png, info, width, height, 8,
PNG_COLOR_TYPE_GRAY_ALPHA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
write_masked_index_frame(png, info, framedata, mask);
} else {
png_set_IHDR(png, info, width, height, 8,
PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
write_index_frame(png, info, framedata);
}
png_destroy_write_struct(&png, &info);
return 0;
}