]> git.draconx.ca Git - liblbx.git/blob - src/png.c
lbximg: Push no-palette mode down into the PNG writer.
[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 <stdbool.h>
25 #include <setjmp.h>
26
27 #include <png.h>
28
29 #include "image.h"
30 #include "tools.h"
31
32 #include "imgoutput.h"
33
34 struct file_info {
35         const char *filename;
36         FILE *f;
37 };
38
39 static int is_masked(unsigned char **mask, unsigned width, unsigned height)
40 {
41         for (unsigned y = 0; y < height; y++) {
42                 for (unsigned x = 0; x < width; x++) {
43                         if (mask[y][x] == 0)
44                                 return 1;
45                 }
46         }
47
48         return 0;
49 }
50
51 static void fatal(png_structp png, int err, const char *fmt, ...)
52 {
53         va_list ap;
54
55         va_start(ap, fmt);
56         tool_verr(err, fmt, ap);
57         va_end(ap);
58
59         png_longjmp(png, 1);
60 }
61
62 static void warning_fn(png_structp png, const char *msg)
63 {
64         tool_err(-1, "libpng warning: %s", msg);
65 }
66
67 static void error_fn(png_structp png, const char *msg)
68 {
69         fatal(png, -1, "libpng error: %s", msg);
70 }
71
72 /*
73  * We implement our own I/O routines because libpng's internal stdio calls do
74  * not handle errors particularly well.
75  */
76 static void write_data(png_structp png, png_bytep data, png_size_t len)
77 {
78         struct file_info *file = png_get_io_ptr(png);
79         size_t ret;
80
81         ret = fwrite(data, 1, len, file->f);
82         if (ret < len) {
83                 fatal(png, 0, "error writing %s", file->filename);
84         }
85 }
86
87 static void flush_data(png_structp png)
88 {
89         struct file_info *file = png_get_io_ptr(png);
90
91         if (fflush(file->f) == EOF) {
92                 fatal(png, 0, "error writing %s", file->filename);
93         }
94 }
95
96 static void
97 write_rgba_frame(png_structp png, png_infop info,
98                  unsigned char **framedata, unsigned char **mask,
99                  struct lbx_colour *palette)
100 {
101         png_uint_32 width = png_get_image_width(png, info);
102         png_uint_32 height = png_get_image_height(png, info);
103         unsigned char (*row)[4];
104         jmp_buf parent;
105
106         if (width >= PNG_UINT_32_MAX / sizeof *row)
107                 fatal(png, -1, "image too wide to allocate row buffer");
108
109         row = png_malloc(png, width * sizeof *row);
110
111         /*
112          * We need to establish our own error handler to free the row buffer.
113          * Some care must be taken to save/restore the caller's handler.
114          */
115         memcpy(&parent, &png_jmpbuf(png), sizeof parent);
116         if (setjmp(png_jmpbuf(png))) {
117                 png_free(png, row);
118
119                 memcpy(&png_jmpbuf(png), &parent, sizeof parent);
120                 png_longjmp(png, 1);
121         }
122
123         png_write_info(png, info);
124         for (png_uint_32 y = 0; y < height; y++) {
125                 for (png_uint_32 x = 0; x < width; x++) {
126                         row[x][0] = palette[framedata[y][x]].red;
127                         row[x][1] = palette[framedata[y][x]].green;
128                         row[x][2] = palette[framedata[y][x]].blue;
129                         row[x][3] = mask[y][x] ? -1 : 0;
130                 }
131                 png_write_row(png, (void *)row);
132         }
133         png_write_end(png, NULL);
134
135         memcpy(&png_jmpbuf(png), &parent, sizeof parent);
136 }
137
138 static void
139 write_masked_index_frame(png_structp png, png_infop info,
140                          unsigned char **framedata, unsigned char **mask)
141 {
142         png_uint_32 width = png_get_image_width(png, info);
143         png_uint_32 height = png_get_image_height(png, info);
144         unsigned char (*row)[2];
145         jmp_buf parent;
146
147         if (width >= PNG_UINT_32_MAX / sizeof *row)
148                 fatal(png, -1, "image too wide to allocate row buffer");
149
150         row = png_malloc(png, width * sizeof *row);
151
152         /*
153          * We need to establish our own error handler to free the row buffer.
154          * Some care must be taken to save/restore the caller's handler.
155          */
156         memcpy(&parent, &png_jmpbuf(png), sizeof parent);
157         if (setjmp(png_jmpbuf(png))) {
158                 png_free(png, row);
159
160                 memcpy(&png_jmpbuf(png), &parent, sizeof parent);
161                 png_longjmp(png, 1);
162         }
163
164         png_write_info(png, info);
165         for (png_uint_32 y = 0; y < height; y++) {
166                 for (png_uint_32 x = 0; x < width; x++) {
167                         row[x][0] = framedata[y][x];
168                         row[x][1] = mask[y][x] ? -1 : 0;
169                 }
170                 png_write_row(png, (void *)row);
171         }
172         png_write_end(png, NULL);
173
174         memcpy(&png_jmpbuf(png), &parent, sizeof parent);
175 }
176
177 static void
178 write_palette_frame(png_structp png, png_infop info,
179                     unsigned char **framedata, struct lbx_colour *palette)
180 {
181         png_color png_palette[256];
182
183         for (unsigned i = 0; i < 256; i++) {
184                 png_palette[i] = (png_color) {
185                         .red   = palette[i].red,
186                         .green = palette[i].green,
187                         .blue  = palette[i].blue,
188                 };
189         }
190
191         png_set_PLTE(png, info, png_palette, 256);
192         png_set_rows(png, info, framedata);
193         png_write_png(png, info, PNG_TRANSFORM_IDENTITY, NULL);
194 }
195
196 static void
197 write_index_frame(png_structp png, png_infop info, unsigned char **framedata)
198 {
199         png_set_rows(png, info, framedata);
200         png_write_png(png, info, PNG_TRANSFORM_IDENTITY, NULL);
201 }
202
203 int img_output_png(FILE *f, const char *filename,
204                    unsigned width, unsigned height,
205                    unsigned char **framedata, unsigned char **mask,
206                    struct lbx_colour *palette)
207 {
208         bool masked = is_masked(mask, width, height);
209         struct file_info file = { filename, f };
210         png_structp png;
211         png_infop info;
212
213         png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
214         if (!png) {
215                 tool_err(-1, "failed to init libpng.");
216                 return -1;
217         }
218
219         info = png_create_info_struct(png);
220         if (!info) {
221                 tool_err(-1, "failed to init libpng.");
222                 png_destroy_write_struct(&png, NULL);
223                 return -1;
224         }
225
226         if (setjmp(png_jmpbuf(png))) {
227                 png_destroy_write_struct(&png, &info);
228                 return -1;
229         }
230
231         png_set_error_fn(png, NULL, error_fn, warning_fn);
232         png_set_write_fn(png, &file, write_data, flush_data);
233
234         if (palette && masked) {
235                 /*
236                  * Indexed colour mode can only be used for images without
237                  * transparency, since indexed PNG requires sacrificing one or
238                  * more palette colours to indicate transparency, while LBX
239                  * does not.  So we use full-up RGBA in this case.
240                  */
241                 png_set_IHDR(png, info, width, height, 8,
242                              PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE,
243                              PNG_COMPRESSION_TYPE_DEFAULT,
244                              PNG_FILTER_TYPE_DEFAULT);
245
246                 write_rgba_frame(png, info, framedata, mask, palette);
247         } else if (palette) {
248                 png_set_IHDR(png, info, width, height, 8,
249                              PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
250                              PNG_COMPRESSION_TYPE_DEFAULT,
251                              PNG_FILTER_TYPE_DEFAULT);
252
253                 write_palette_frame(png, info, framedata, palette);
254         } else if (masked) {
255                 png_set_IHDR(png, info, width, height, 8,
256                              PNG_COLOR_TYPE_GRAY_ALPHA, PNG_INTERLACE_NONE,
257                              PNG_COMPRESSION_TYPE_DEFAULT,
258                              PNG_FILTER_TYPE_DEFAULT);
259
260                 write_masked_index_frame(png, info, framedata, mask);
261         } else {
262                 png_set_IHDR(png, info, width, height, 8,
263                              PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE,
264                              PNG_COMPRESSION_TYPE_DEFAULT,
265                              PNG_FILTER_TYPE_DEFAULT);
266
267                 write_index_frame(png, info, framedata);
268         }
269
270         png_destroy_write_struct(&png, &info);
271         return 0;
272 }