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