]> git.draconx.ca Git - liblbx.git/blob - src/png.c
ee5d0a52644d5e12e3285817312d7dc2da79f228
[liblbx.git] / src / png.c
1 /*
2  * 2ooM: The Master of Orion II Reverse Engineering Project
3  * PNG output routines for lbximg extration.
4  * Copyright © 2013 Nick Bowler
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #include <config.h>
21 #include <stdio.h>
22 #include <string.h>
23 #include <stdarg.h>
24 #include <setjmp.h>
25
26 #include <png.h>
27
28 #include "image.h"
29 #include "tools.h"
30
31 #include "imgoutput.h"
32
33 struct file_info {
34         const char *filename;
35         FILE *f;
36 };
37
38 static int is_masked(unsigned char **mask, unsigned width, unsigned height)
39 {
40         for (unsigned y = 0; y < height; y++) {
41                 for (unsigned x = 0; x < width; x++) {
42                         if (mask[y][x] == 0)
43                                 return 1;
44                 }
45         }
46
47         return 0;
48 }
49
50 static void fatal(png_structp png, int err, const char *fmt, ...)
51 {
52         va_list ap;
53
54         va_start(ap, fmt);
55         tool_verr(err, fmt, ap);
56         va_end(ap);
57
58         png_longjmp(png, 1);
59 }
60
61 static void warning_fn(png_structp png, const char *msg)
62 {
63         tool_err(-1, "libpng warning: %s", msg);
64 }
65
66 static void error_fn(png_structp png, const char *msg)
67 {
68         fatal(png, -1, "libpng error: %s", msg);
69 }
70
71 /*
72  * We implement our own I/O routines because libpng's internal stdio calls do
73  * not handle errors particularly well.
74  */
75 static void write_data(png_structp png, png_bytep data, png_size_t len)
76 {
77         struct file_info *file = png_get_io_ptr(png);
78         size_t ret;
79
80         ret = fwrite(data, 1, len, file->f);
81         if (ret < len) {
82                 fatal(png, 0, "error writing %s", file->filename);
83         }
84 }
85
86 static void flush_data(png_structp png)
87 {
88         struct file_info *file = png_get_io_ptr(png);
89
90         if (fflush(file->f) == EOF) {
91                 fatal(png, 0, "error writing %s", file->filename);
92         }
93 }
94
95 static void
96 write_rgba_frame(png_structp png, png_infop info,
97                  unsigned char **framedata, unsigned char **mask,
98                  struct lbx_colour *palette)
99 {
100         png_uint_32 width = png_get_image_width(png, info);
101         png_uint_32 height = png_get_image_height(png, info);
102         unsigned char (*row)[4];
103         jmp_buf parent;
104
105         if (width >= PNG_UINT_32_MAX / sizeof *row)
106                 fatal(png, -1, "image too wide to allocate row buffer");
107
108         row = png_malloc(png, width * sizeof *row);
109
110         /*
111          * We need to establish our own error handler to free the row buffer.
112          * Some care must be taken to save/restore the caller's handler.
113          */
114         memcpy(&parent, &png_jmpbuf(png), sizeof parent);
115         if (setjmp(png_jmpbuf(png))) {
116                 png_free(png, row);
117
118                 memcpy(&png_jmpbuf(png), &parent, sizeof parent);
119                 png_longjmp(png, 1);
120         }
121
122         png_write_info(png, info);
123         for (png_uint_32 y = 0; y < height; y++) {
124                 for (png_uint_32 x = 0; x < width; x++) {
125                         row[x][0] = palette[framedata[y][x]].red;
126                         row[x][1] = palette[framedata[y][x]].green;
127                         row[x][2] = palette[framedata[y][x]].blue;
128                         row[x][3] = mask[y][x] ? -1 : 0;
129                 }
130                 png_write_row(png, (void *)row);
131         }
132         png_write_end(png, NULL);
133
134         memcpy(&png_jmpbuf(png), &parent, sizeof parent);
135 }
136
137 static void
138 write_index_frame(png_structp png, png_infop info, unsigned char **framedata,
139                                                    struct lbx_colour *palette)
140 {
141         png_color png_palette[256];
142
143         for (unsigned i = 0; i < 256; i++) {
144                 png_palette[i] = (png_color) {
145                         .red   = palette[i].red,
146                         .green = palette[i].green,
147                         .blue  = palette[i].blue,
148                 };
149         }
150
151         png_set_PLTE(png, info, png_palette, 256);
152         png_set_rows(png, info, framedata);
153         png_write_png(png, info, PNG_TRANSFORM_IDENTITY, NULL);
154 }
155
156 int img_output_png(FILE *f, const char *filename,
157                    unsigned width, unsigned height,
158                    unsigned char **framedata, unsigned char **mask,
159                    struct lbx_colour *palette)
160 {
161         struct file_info file = { filename, f };
162         png_structp png;
163         png_infop info;
164
165         png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
166         if (!png) {
167                 tool_err(-1, "failed to init libpng.");
168                 return -1;
169         }
170
171         info = png_create_info_struct(png);
172         if (!info) {
173                 tool_err(-1, "failed to init libpng.");
174                 png_destroy_write_struct(&png, NULL);
175                 return -1;
176         }
177
178         if (setjmp(png_jmpbuf(png))) {
179                 png_destroy_write_struct(&png, &info);
180                 return -1;
181         }
182
183         png_set_error_fn(png, NULL, error_fn, warning_fn);
184         png_set_write_fn(png, &file, write_data, flush_data);
185
186         if (is_masked(mask, width, height)) {
187                 /*
188                  * Indexed colour mode can only be used for images without
189                  * transparency, since indexed PNG requires sacrificing one or
190                  * more palette colours to indicate transparency, while LBX
191                  * does not.  So we use full-up RGBA in this case.
192                  */
193                 png_set_IHDR(png, info, width, height, 8,
194                              PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE,
195                              PNG_COMPRESSION_TYPE_DEFAULT,
196                              PNG_FILTER_TYPE_DEFAULT);
197
198                 write_rgba_frame(png, info, framedata, mask, palette);
199         } else {
200                 png_set_IHDR(png, info, width, height, 8,
201                              PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
202                              PNG_COMPRESSION_TYPE_DEFAULT,
203                              PNG_FILTER_TYPE_DEFAULT);
204
205                 write_index_frame(png, info, framedata, palette);
206         }
207
208         png_destroy_write_struct(&png, &info);
209         return 0;
210 }