]> git.draconx.ca Git - liblbx.git/commitdiff
lbximg: Add support for Netpbm output.
authorNick Bowler <nbowler@draconx.ca>
Fri, 14 Jun 2013 22:11:46 +0000 (18:11 -0400)
committerNick Bowler <nbowler@draconx.ca>
Fri, 14 Jun 2013 22:11:46 +0000 (18:11 -0400)
These netpbm formats are extremely simple image formats which can
be written with a very small amount of code.  Implement the ASCII
PBM and PPM formats for eventual use in test scripts, and PAM as a
fully-featured format so that we can make libpng optional.

Makefile.am
doc/man/lbximg.1
src/imgoutput.h
src/lbximg.c
src/png.c
src/pnm.c [new file with mode: 0644]

index 8cfec29a86307182b9258e4fd4c7a22753307844..48398a646e64c202bfb5ad3d471baccdf67ea7f4 100644 (file)
@@ -37,7 +37,7 @@ lbxtool_SOURCES = src/lbxtool.c src/tools.c
 lbxtool_LDADD = liblbx.la libgnu.la
 $(lbxtool_OBJECTS): $(gnulib_headers)
 
-lbximg_SOURCES = src/lbximg.c src/tools.c src/png.c
+lbximg_SOURCES = src/lbximg.c src/tools.c src/png.c src/pnm.c
 lbximg_LDADD = liblbx.la libgnu.la $(LIBPNG_LIBS)
 $(lbximg_OBJECTS): $(gnulib_headers)
 
index 87bd3ee8f524a2726e0374fa79b90cc70be910db..f3ecece9de0d1dd5f16c6d72dc02a9a6166f1d65 100644 (file)
@@ -1,9 +1,4 @@
-.\" Copyright (C) 2008-2010 Nick Bowler
-.\" Copying and distribution of this file, with or without modification,
-.\" are permitted in any medium without royalty provided the copyright
-.\" notice and this notice are preserved.  This file is offered as-is,
-.\" without any warranty.
-.Dd February 9, 2010
+.Dd June 14, 2013
 .Os liblbx
 .Dt LBXIMG \&1 "2ooM Reference Manual"
 .Sh NAME
@@ -14,6 +9,7 @@
 .Op Fl i Ns | Ns Fl d
 .Op Fl v
 .Op Fl n
+.Op Fl F Ar format
 .Op Fl p Ar palette_file
 .Op Fl O Ar override_file
 .Op Fl f Ar path
@@ -24,7 +20,8 @@ identifies and decodes LBX image files, using
 .Em liblbx .
 LBX images are multi-frame, 256-colour paletted images with transparency.
 .Nm
-can be used to convert some or all of the frames of an LBX image to PNG.
+can be used to convert some or all of the frames of an LBX image to other image
+formats.
 .Sh OPTIONS
 .Bl -tag -width indent
 .It Fl i , -ident
@@ -33,14 +30,44 @@ Sets the operating mode to identify the image format.
 Sets the operating mode to decode frames to PNG.
 .It Fl v , -verbose
 Output additional information on standard output.
+.It Fl F , -format Ar format
+Select the desired output format.  Some formats may not be available depending
+on the compile-time settings of
+.Nm .
+If this option is not specified, the default is the first in the following list
+which is enabled at build time.
+.Bl -column -offset indent ".Em Format"
+.It Em Format Ta Em Description
+.It png Ta
+Output images in Portable Network Graphics (PNG) format.  This is is a
+compressed format which is well-supported by other tools.  All features of
+.Nm
+are supported with this format.
+.It pam Ta
+Output images in Netpbm PAM format.  This is a simple uncompressed binary image
+format supporting RGB and alpha channels.  All features of
+.Nm
+are supported with this format.
+.It ppm Ta
+Output image colour data in Netpbm "plain" PPM format.  This is a simple
+7-bit clean uncompressed RGB format.  It does not support transparency, so
+images will have transparent pixels replaced with black.  This format is
+rather inefficient and provided mainly for testing.
+.It pbm Ta
+Output image mask data in Netpbm "plain" PBM format.  This is a simple 7-bit
+clean uncompressed bitmap format.  It does not support colour data; instead,
+bitmap values represent whether or not a pixel is transparent.  Black (1)
+pixels are transparent, white (0) pixels are opaque.  This format is extremely
+inefficient and provided mainly for testing.
+.El
 .It Fl f , -file Ar path
 Read from the specified
 .Ar path
 instead of standard input.
 .It Fl n , -no-palette
-Instead of looking up colour indices in the palette, use the index itself for
-each of the red, green and blue components of the output.  This is mainly
-useful for debugging
+Instead of looking up colour indices in the palette, emit a grayscale image
+where the intensity value of a pixel is equal to the palette index itself.
+This is mainly useful for debugging the image decoder in
 .Em liblbx .
 .It Fl p , -palette Ar palette_file
 Read the base palette from
@@ -90,6 +117,13 @@ Decodes all the frames of the Microprose logo into a series of PNGs.
 .It Nm Li -dvf ships.lbx.042 -p fonts.lbx.012 -O ships.lbx.049
 Decodes an image of a red star fortress.
 .El
+.Sh AUTHORS
+Nick Bowler <nbowler@draconx.ca>
+.Sh COPYRIGHT
+Copyright \(co 2008\(en2010, 2013 Nick Bowler
+.Pp
+Permission is granted to copy, distribute and/or modify this manual under the
+terms of the Do What The Fuck You Want To Public License, version 2.
 .Sh SEE ALSO
 .Xr lbxtool 1 ,
 .Xr lbxgui 1
index 46d9c9a1f115300c6d347e8690603a6ba139aa06..bb0b2a529bb60aeb6b20c9a8aca0f392ad2eb89a 100644 (file)
@@ -1,9 +1,16 @@
 #ifndef IMGOUTPUT_H_
 #define IMGOUTPUT_H_
 
-int img_output_png(FILE *f, const char *filename,
-                   unsigned width, unsigned height,
-                   unsigned char **framedata, unsigned char **mask,
-                  struct lbx_colour *palette);
+#include "image.h"
+
+typedef int img_output_func(FILE *f, const char *filename,
+                            unsigned width, unsigned height,
+                            unsigned char **framedata, unsigned char **mask,
+                            struct lbx_colour *palette);
+
+img_output_func img_output_pbm, img_output_ppm, img_output_pam;
+img_output_func img_output_png;
+
+_Bool img_is_masked(unsigned char **mask, unsigned width, unsigned height);
 
 #endif
index 6c54b0f3b659bbb79371d63efe88252f8053858b..ada16929a32cf400797b089e849c4bdb371eabf0 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  2ooM: The Master of Orion II Reverse Engineering Project
- *  Simple command-line tool to convert an LBX image to a set of PNGs.
+ *  Simple command-line tool to convert an LBX image to other formats.
  *  Copyright © 2006-2011, 2013 Nick Bowler
  *
  *  This program is free software: you can redistribute it and/or modify
@@ -19,6 +19,7 @@
 #include <config.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <stdbool.h>
 #include <string.h>
 #include <limits.h>
 #include <assert.h>
@@ -41,7 +42,7 @@ static void printusage(void)
 {
        puts("usage: lbximg [-i|-d] [-v] [-p palette_file] [-O override_file]"
                          " [-f path]");
-       puts("              [frameno ...]");
+       puts("              [-F format] [frameno ...]");
 }
 
 static void printhelp(void)
@@ -56,6 +57,52 @@ enum {
        MODE_IDENT,
 };
 
+static const struct img_format {
+       img_output_func *output;
+       char name[4];
+       bool enabled;
+} formats[] = {
+       { img_output_png, "png", 1 },
+       { img_output_pam, "pam", 1 },
+       { img_output_ppm, "ppm", 1 },
+       { img_output_pbm, "pbm", 1 },
+};
+
+static int lookup_format(const char *fmt)
+{
+       for (size_t i = 0; i < sizeof formats / sizeof formats[0]; i++) {
+               assert(!formats[i].name[sizeof formats[i].name - 1]);
+
+               if (!fmt && formats[i].enabled)
+                       return i;
+
+               if (strcmp(formats[i].name, fmt))
+                       continue;
+
+               if (!formats[i].enabled) {
+                       tool_err(-1, "%s support disabled at build time", fmt);
+                       return -1;
+               }
+
+               return i;
+       }
+
+       tool_err(-1, "unknown format %s", fmt);
+       return -1;
+}
+
+bool img_is_masked(unsigned char **mask, unsigned width, unsigned height)
+{
+       unsigned x, y;
+
+       for (x = y = 0; y < height; ++x < width || (x = 0, y++)) {
+               if (mask[y][x] == 0)
+                       return true;
+       }
+
+       return false;
+}
+
 int parserange(unsigned frames, char *str, unsigned char *bits)
 {
        unsigned long start, end;
@@ -104,7 +151,7 @@ int parserange(unsigned frames, char *str, unsigned char *bits)
        return 0;
 }
 
-int outpng(unsigned int frameno,
+int output(unsigned int frameno, const struct img_format *fmt,
            unsigned char **framedata, unsigned char **mask,
            unsigned int width, unsigned int height,
            struct lbx_colour palette[static 256])
@@ -113,8 +160,9 @@ int outpng(unsigned int frameno,
        FILE *of;
        int rc;
 
+       assert(fmt->output != NULL);
        assert(frameno < 65536);
-       snprintf(name, sizeof name, "%s.%03d.png", outname, frameno);
+       snprintf(name, sizeof name, "%s.%03d.%s", outname, frameno, fmt->name);
 
        of = fopen(name, "wb");
        if (!of) {
@@ -122,7 +170,7 @@ int outpng(unsigned int frameno,
                return -1;
        }
 
-       rc = img_output_png(of, name, width, height, framedata, mask, palette);
+       rc = fmt->output(of, name, width, height, framedata, mask, palette);
        if (rc < 0) {
                fclose(of);
                return -1;
@@ -203,7 +251,8 @@ static int loadpalette(struct lbx_image *img, struct lbx_imginfo *info,
        return 0;
 }
 
-int decode(struct lbx_image *img, FILE *palf, FILE *override, char **argv)
+static int
+decode(struct lbx_image *img, FILE *palf, FILE *override, int fmt, char **argv)
 {
        unsigned char *framebits;
        struct lbx_colour palette[256];
@@ -211,6 +260,8 @@ int decode(struct lbx_image *img, FILE *palf, FILE *override, char **argv)
        int extracted = 0;
        unsigned int i;
 
+       assert(fmt >= 0 && fmt < sizeof formats / sizeof formats[0]);
+
        lbx_img_getinfo(img, &info);
 
        framebits = calloc(1, img->frames / CHAR_BIT + 1);
@@ -250,7 +301,8 @@ int decode(struct lbx_image *img, FILE *palf, FILE *override, char **argv)
 
                mask = lbx_img_getmask(img);
 
-               if (!outpng(i, data, mask, img->width, img->height,
+               if (!output(i, &formats[fmt], data, mask,
+                           img->width, img->height,
                            usepalette ? palette : NULL)) {
                        extracted = 1;
                }
@@ -270,18 +322,19 @@ err:
 
 int main(int argc, char **argv)
 {
-       int mode = MODE_NONE, opt, rc = EXIT_FAILURE;
+       int mode = MODE_NONE, fmt, opt, rc = EXIT_FAILURE;
        struct lbx_pipe_state stdin_handle = { .f = stdin };
+       const char *file = NULL, *fmtstring = NULL;
        FILE *palf = NULL, *overf = NULL;
-       const char *file = NULL;
        struct lbx_image *img;
 
-       static const char *sopts = "idnvf:p:O:V";
+       static const char sopts[] = "idnvF:f:p:O:VH";
        static const struct option lopts[] = {
                { "ident",      0, NULL, 'i' },
                { "decode",     0, NULL, 'd' },
                { "verbose",    0, NULL, 'v' },
                { "file",       1, NULL, 'f' },
+               { "format",     1, NULL, 'F' },
                { "palette",    1, NULL, 'p' },
                { "override",   1, NULL, 'p' },
 
@@ -306,6 +359,9 @@ int main(int argc, char **argv)
                case 'v':
                        verbose = 1;
                        break;
+               case 'F':
+                       fmtstring = optarg;
+                       break;
                case 'f':
                        file = optarg;
                        break;
@@ -336,8 +392,7 @@ int main(int argc, char **argv)
                case 'H':
                        printhelp();
                        return EXIT_SUCCESS;
-               case '?':
-               case ':':
+               default:
                        return EXIT_FAILURE;
                }
        }
@@ -347,6 +402,10 @@ int main(int argc, char **argv)
                return EXIT_FAILURE;
        }
 
+       fmt = lookup_format(fmtstring);
+       if (fmt < 0)
+               return EXIT_FAILURE;
+
        if (file)
                img = lbx_img_fopen(file);
        else
@@ -373,7 +432,7 @@ int main(int argc, char **argv)
 
        switch (mode) {
        case MODE_DECODE:
-               rc = decode(img, palf, overf, &argv[optind]);
+               rc = decode(img, palf, overf, fmt, &argv[optind]);
                break;
        }
 
index 83aeec43583a354834a8e49bd525113fc50a6876..c52db03e599ef7f7e48d5982272c8ff5eadb8229 100644 (file)
--- a/src/png.c
+++ b/src/png.c
@@ -37,18 +37,6 @@ struct file_info {
        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;
@@ -220,7 +208,7 @@ int img_output_png(FILE *f, const char *filename,
                    unsigned char **framedata, unsigned char **mask,
                   struct lbx_colour *palette)
 {
-       bool masked = is_masked(mask, width, height);
+       bool masked = img_is_masked(mask, width, height);
        struct file_info file = { filename, f };
        png_structp png;
        png_infop info;
diff --git a/src/pnm.c b/src/pnm.c
new file mode 100644 (file)
index 0000000..865d2a1
--- /dev/null
+++ b/src/pnm.c
@@ -0,0 +1,357 @@
+/*
+ * 2ooM: The Master of Orion II Reverse Engineering Project
+ * Netpbm 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <inttypes.h>
+
+#include "image.h"
+#include "tools.h"
+#include "imgoutput.h"
+
+/* Convert c from the basic execution character set to ASCII. */
+static unsigned char to_ascii(unsigned char c)
+{
+       switch (c) {
+       case'\a': return 0x07;
+       case'\b': return 0x08;
+       case'\t': return 0x09;
+       case'\n': return 0x0a;
+       case'\v': return 0x0b;
+       case'\f': return 0x0c;
+       case'\r': return 0x0d;
+       case ' ': return 0x20;
+       case '!': return 0x21;
+       case '"': return 0x22;
+       case '#': return 0x23;
+       case '%': return 0x25;
+       case '&': return 0x26;
+       case'\'': return 0x27;
+       case '(': return 0x28;
+       case ')': return 0x29;
+       case '*': return 0x2a;
+       case '+': return 0x2b;
+       case ',': return 0x2c;
+       case '-': return 0x2d;
+       case '.': return 0x2e;
+       case '/': return 0x2f;
+       case '0': return 0x30;
+       case '1': return 0x31;
+       case '2': return 0x32;
+       case '3': return 0x33;
+       case '4': return 0x34;
+       case '5': return 0x35;
+       case '6': return 0x36;
+       case '7': return 0x37;
+       case '8': return 0x38;
+       case '9': return 0x39;
+       case ':': return 0x3a;
+       case ';': return 0x3b;
+       case '<': return 0x3c;
+       case '=': return 0x3d;
+       case '>': return 0x3e;
+       case '?': return 0x3f;
+       case 'A': return 0x41;
+       case 'B': return 0x42;
+       case 'C': return 0x43;
+       case 'D': return 0x44;
+       case 'E': return 0x45;
+       case 'F': return 0x46;
+       case 'G': return 0x47;
+       case 'H': return 0x48;
+       case 'I': return 0x49;
+       case 'J': return 0x4a;
+       case 'K': return 0x4b;
+       case 'L': return 0x4c;
+       case 'M': return 0x4d;
+       case 'N': return 0x4e;
+       case 'O': return 0x4f;
+       case 'P': return 0x50;
+       case 'Q': return 0x51;
+       case 'R': return 0x52;
+       case 'S': return 0x53;
+       case 'T': return 0x54;
+       case 'U': return 0x55;
+       case 'V': return 0x56;
+       case 'W': return 0x57;
+       case 'X': return 0x58;
+       case 'Y': return 0x59;
+       case 'Z': return 0x5a;
+       case '[': return 0x5b;
+       case'\\': return 0x5c;
+       case ']': return 0x5d;
+       case '^': return 0x5e;
+       case '_': return 0x5f;
+       case 'a': return 0x61;
+       case 'b': return 0x62;
+       case 'c': return 0x63;
+       case 'd': return 0x64;
+       case 'e': return 0x65;
+       case 'f': return 0x66;
+       case 'g': return 0x67;
+       case 'h': return 0x68;
+       case 'i': return 0x69;
+       case 'j': return 0x6a;
+       case 'k': return 0x6b;
+       case 'l': return 0x6c;
+       case 'm': return 0x6d;
+       case 'n': return 0x6e;
+       case 'o': return 0x6f;
+       case 'p': return 0x70;
+       case 'q': return 0x71;
+       case 'r': return 0x72;
+       case 's': return 0x73;
+       case 't': return 0x74;
+       case 'u': return 0x75;
+       case 'v': return 0x76;
+       case 'w': return 0x77;
+       case 'x': return 0x78;
+       case 'y': return 0x79;
+       case 'z': return 0x7a;
+       case '{': return 0x7b;
+       case '|': return 0x7c;
+       case '}': return 0x7d;
+       case '~': return 0x7e;
+       default: assert((tool_err(-1, "invalid codepoint: %hhx", c), 0));
+       }
+
+       return c;
+}
+
+/* Printf variant which converts all output to ASCII. */
+static int fprintf_ascii(FILE *f, char *fmt, ...)
+{
+       unsigned char *buf;
+       va_list ap;
+       int rc, len;
+       size_t ret;
+
+       va_start(ap, fmt);
+       rc = vsnprintf(NULL, 0, fmt, ap);
+       va_end(ap);
+
+       if (rc < 0)
+               return -1;
+
+       assert(rc < SIZE_MAX);
+       buf = malloc(rc+1u);
+       if (!buf)
+               return -1;
+
+       va_start(ap, fmt);
+       len = vsprintf((char *)buf, fmt, ap);
+       va_end(ap);
+
+       assert(rc == len);
+       for (int i = 0; i < len; i++) {
+               buf[i] = to_ascii(buf[i]);
+       }
+
+       ret = fwrite(buf, 1, len, f);
+       free(buf);
+
+       if (ret < len)
+               return -(int)ret;
+       return ret;
+}
+
+/*
+ * Output filter for Netpbm's "plain" PBM format.  This is a bitmap format
+ * which is not really suitable for image data, but it can output the image
+ * transparency mask, and thus complements the PPM output.  The image colour
+ * data is lost in the conversion.
+ */
+int img_output_pbm(FILE *f, const char *filename,
+                   unsigned width, unsigned height,
+                   unsigned char **framedata, unsigned char **mask,
+                  struct lbx_colour *palette)
+{
+       unsigned x, y;
+
+       if (fprintf_ascii(f, "P1\n%u %u", width, height) < 0)
+               goto err;
+
+       for (x = y = 0; y < height; ++x < width || (x = 0, y++)) {
+               if (fputc(to_ascii(x > 0 ? ' ' : '\n'), f) == EOF)
+                       goto err;
+
+               if (mask[y][x]) {
+                       if (fputc(to_ascii('0'), f) == EOF)
+                               goto err;
+               } else {
+                       if (fputc(to_ascii('1'), f) == EOF)
+                               goto err;
+               }
+       }
+
+       if (fputc(to_ascii('\n'), f) == EOF)
+               goto err;
+       return 0;
+err:
+       tool_err(0, "error writing %s", filename);
+       return -1;
+}
+
+/*
+ * Helper for Netpbm's "plain" PGM output.  This is only meant for no-palette
+ * mode as normally LBX images are not grayscale, so it is not available as a
+ * normal output format.
+ */
+static int write_pgm(FILE *f, unsigned width, unsigned height,
+                     unsigned char **framedata, unsigned char **mask)
+{
+       unsigned x, y;
+
+       if (fprintf_ascii(f, "P2\n%u %u\n255", width, height) < 0)
+               return -1;
+
+       for (x = y = 0; y < height; ++x < width || (x = 0, y++)) {
+               if (fputc(to_ascii(x > 0 ? ' ' : '\n'), f) == EOF)
+                       return -1;
+
+               if (!mask[y][x]) {
+                       if (fprintf_ascii(f, "  0") < 0)
+                               return -1;
+               } else {
+                       if (fprintf_ascii(f, "%3hhu", framedata[y][x]) < 0)
+                               return -1;
+               }
+       }
+
+       if (fputc(to_ascii('\n'), f) == EOF)
+               return -1;
+
+       return 0;
+}
+
+/*
+ * Output filter for Netpbm's "plain" PPM format.  This supports RGB but not
+ * transparency.  As a pure text format, it is useful for testing but is not
+ * particularly efficient in terms of storage.  The image mask is lost in
+ * the conversion (output pixels will be black).
+ */
+int img_output_ppm(FILE *f, const char *filename,
+                   unsigned width, unsigned height,
+                   unsigned char **framedata, unsigned char **mask,
+                  struct lbx_colour *palette)
+{
+       unsigned x, y;
+
+       if (!palette) {
+               /*
+                * For no-palette mode, write a PGM instead which is basically
+                * the same format but has only one value per pixel.
+                */
+               if (write_pgm(f, width, height, framedata, mask) < 0)
+                       goto err;
+               return 0;
+       }
+
+       if (fprintf_ascii(f, "P3\n%u %u\n63", width, height) < 0)
+               goto err;
+
+       for (x = y = 0; y < height; ++x < width || (x = 0, y++)) {
+               if (fputc(to_ascii(x > 0 ? ' ' : '\n'), f) == EOF)
+                       goto err;
+
+               if (!mask[y][x]) {
+                       if (fprintf_ascii(f, " 0  0  0") < 0)
+                               goto err;
+               } else {
+                       struct lbx_colour *c = &palette[framedata[y][x]];
+
+                       if (fprintf_ascii(f, "%2d %2d %2d",
+                                         c->red, c->green, c->blue) < 0)
+                               goto err;
+               }
+       }
+
+       if (fputc(to_ascii('\n'), f) == EOF)
+               goto err;
+       return 0;
+err:
+       tool_err(0, "error writing %s", filename);
+       return -1;
+}
+
+/*
+ * Output filter for Netpbm's PAM format.  This format combines the features
+ * of all the other Netpbm formats, supporting RGB and grayscale images with
+ * or without alpha channels.  This format supports all lbximg output.
+ */
+int img_output_pam(FILE *f, const char *filename,
+                   unsigned width, unsigned height,
+                   unsigned char **framedata, unsigned char **mask,
+                  struct lbx_colour *palette)
+{
+       bool masked = img_is_masked(mask, width, height);
+       unsigned x, y;
+       size_t depth;
+
+       if (fprintf_ascii(f, "P7\nWIDTH %u\nHEIGHT %u\n", width, height) < 0)
+               goto err;
+
+       if (palette) {
+               depth = masked ? 4 : 3;
+
+               if (fprintf_ascii(f, "DEPTH %zu\nMAXVAL 63\n"
+                                    "TUPLTYPE RGB%s\nENDHDR\n",
+                                    depth, masked ? "_ALPHA" : "") < 0)
+                       goto err;
+
+               for (x = y = 0; y < height; ++x < width || (x = 0, y++)) {
+                       struct lbx_colour *c = &palette[framedata[y][x]];
+                       unsigned char buf[4];
+
+                       buf[0] = c->red;
+                       buf[1] = c->green;
+                       buf[2] = c->blue;
+                       buf[3] = mask[y][x] ? 63 : 0;
+
+                       if (fwrite(buf, 1, depth, f) < depth)
+                               goto err;
+               }
+       } else {
+               depth = masked ? 2 : 1;
+
+               if (fprintf_ascii(f, "DEPTH %zu\nMAXVAL 255\n"
+                                    "TUPLTYPE GRAYSCALE%s\nENDHDR\n",
+                                    depth, masked ? "_ALPHA" : "") < 0)
+                       goto err;
+
+               for (x = y = 0; y < height; ++x < width || (x = 0, y++)) {
+                       unsigned char buf[2];
+
+                       buf[0] = framedata[y][x];
+                       buf[1] = mask[y][x] ? 0xff : 0;
+
+                       if (fwrite(buf, 1, depth, f) < depth)
+                               goto err;
+               }
+       }
+
+       return 0;
+err:
+       tool_err(0, "error writing %s", filename);
+       return -1;
+}