/* * 2ooM: The Master of Orion II Reverse Engineering Project * PNG 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 "image.h" #include "tools.h" #include "imgoutput.h" struct file_info { const char *filename; FILE *f; }; 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 *pixels, 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++) { unsigned long offset = (unsigned long) y * width + x; png_color *c = &png_palette[pixels[offset]]; bool vis; vis = mask[offset/CHAR_BIT] & (1u << offset%CHAR_BIT); row[x][0] = c->red; row[x][1] = c->green; row[x][2] = c->blue; row[x][3] = -vis; } 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 *pixels, 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++) { unsigned long offset = (unsigned long) y * width + x; bool vis; vis = mask[offset/CHAR_BIT] & (1u << offset%CHAR_BIT); row[x][0] = pixels[offset]; row[x][1] = -vis; } png_write_row(png, (void *)row); } png_write_end(png, NULL); memcpy(&png_jmpbuf(png), &parent, sizeof parent); } static void write_index_frame(png_structp png, png_infop info, unsigned char *pixels) { png_uint_32 width = png_get_image_width(png, info); png_uint_32 height = png_get_image_height(png, info); png_write_info(png, info); for (png_uint_32 y = 0; y < height; y++) { unsigned long offset = (unsigned long) y * width; png_write_row(png, (void *)(pixels+offset)); } png_write_end(png, NULL); } static void write_palette_frame(png_structp png, png_infop info, unsigned char *pixels, 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); write_index_frame(png, info, pixels); } int img_output_png(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); 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, pixels, 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, pixels, 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, pixels, 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, pixels); } png_destroy_write_struct(&png, &info); return 0; }