view tools/libgfx.c @ 1896:f80b2dc77c30

Work begins on IFF ILBM/PBM image writer. It is pretty broken, some things will not work and some things are hardcoded. The ByteRun1 compression implementation is somewhat inefficient. Interleaved files do not work yet.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 26 Jun 2018 03:13:38 +0300
parents eb03869a10d3
children 50dbfc10f49f
line wrap: on
line source

/*
 * Functions for reading and converting various graphics file formats
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2012-2018 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "libgfx.h"

#ifdef DM_USE_LIBPNG
#include <png.h>
#endif


int dmGFXErrorMode = DM_ERRMODE_FAIL;


void dmInitBitStreamContext(DMBitStreamContext *ctx)
{
    ctx->outBuf       = 0;
    ctx->outByteCount = 0;
    ctx->outBitCount  = 8;
}


BOOL dmPutBits(DMBitStreamContext *ctx, const int val, const int n)
{
    unsigned int mask = 1 << (n - 1);

    for (int i = 0; i < n; i++)
    {
        ctx->outBuf <<= 1;

        if (val & mask)
          ctx->outBuf |= 1;

        mask >>= 1;
        ctx->outBitCount--;

        if (ctx->outBitCount == 0)
        {
            if (!ctx->putByte(ctx, ctx->outBuf & 0xff))
                return FALSE;

            ctx->outBitCount = 8;
            ctx->outByteCount++;
        }
    }

    return TRUE;
}


int dmFlushBitStream(DMBitStreamContext *ctx)
{
  if (ctx == NULL)
      return DMERR_NULLPTR;

  if (ctx->outBitCount != 8)
      dmPutBits(ctx, 0, ctx->outBitCount);

  return 0;
}


static BOOL dmPutByteFILE(DMBitStreamContext *ctx, const Uint8 val)
{
    return dmf_write_byte((DMResource *) ctx->handle, val);
}


int dmInitBitStreamFILE(DMBitStreamContext *ctx, DMResource *fp)
{
    if (ctx == NULL || fp == NULL)
        return DMERR_NULLPTR;

    ctx->putByte = dmPutByteFILE;
    ctx->handle  = (void *) fp;

    dmInitBitStreamContext(ctx);

    return DMERR_OK;
}


BOOL dmCompareColor(const DMColor *c1, const DMColor *c2, BOOL alpha)
{
    if (c1->r == c2->r &&
        c1->g == c2->g &&
        c1->b == c2->b)
        return alpha ? (c1->a == c2->a) : TRUE;
    else
        return FALSE;
}


int dmImageGetBytesPerPixel(const int format)
{
    switch (format)
    {
        case DM_COLFMT_PALETTE   : return 1;
        case DM_COLFMT_RGB       : return 3;
        case DM_COLFMT_RGBA      : return 4;
        default                  : return -1;
    }
}


DMImage * dmImageAlloc(const int width, const int height, const int format, const int bpp)
{
    DMImage *img = dmMalloc0(sizeof(DMImage));
    if (img == NULL)
        return NULL;

    img->width   = width;
    img->height  = height;
    img->format  = format;
    img->bpp     = (bpp <= 0) ? dmImageGetBytesPerPixel(format) * 8 : bpp;
    img->pitch   = width * img->bpp;
    img->size    = img->pitch * img->height;
    img->ctransp = -1;
    img->aspect  = -1;

    if ((img->data = dmMalloc(img->size)) == NULL)
    {
        dmFree(img);
        return NULL;
    }

    return img;
}


void dmImageFree(DMImage *img)
{
    if (img != NULL)
    {
        if (!img->constpal)
        {
            dmFree(img->pal);
        }
        dmFree(img->data);
        dmFree(img);
    }
}


BOOL dmPaletteAlloc(DMColor **ppal, int ncolors, int ctransp)
{
    if (ppal == NULL)
        return FALSE;

    // Allocate desired amount of palette
    if ((*ppal = dmCalloc(ncolors, sizeof(DMColor))) == NULL)
        return FALSE;

    // Set alpha values to max, except for transparent color
    for (int i = 0; i < ncolors; i++)
    {
        (*ppal)[i].a = (i == ctransp) ? 0x00 : 0xff;
    }

    return TRUE;
}


BOOL dmImagePaletteAlloc(DMImage *img, int ncolors, int ctransp)
{
    if (img == NULL)
        return FALSE;

    img->ncolors = ncolors;
    img->ctransp = ctransp;
    img->constpal = FALSE;

    return dmPaletteAlloc(&(img->pal), ncolors, ctransp);
}


static BOOL dmPaletteReadData(DMResource *fp, DMColor *pal, int ncolors)
{
    for (int i = 0; i < ncolors; i++)
    {
        Uint8 colR, colG, colB;
        if (!dmf_read_byte(fp, &colR) ||
            !dmf_read_byte(fp, &colG) ||
            !dmf_read_byte(fp, &colB))
            return FALSE;

        pal[i].r = colR;
        pal[i].g = colG;
        pal[i].b = colB;
    }

    return TRUE;
}


int dmWriteImageData(const DMImage *img, void *cbdata, int (*writeRowCB)(void *, const Uint8 *, const size_t), const DMImageConvSpec *spec)
{
    int x, y, yscale, xscale, res = 0, rowSize, rowWidth;
    Uint8 *row = NULL;

    // Allocate memory for row buffer
    rowWidth = img->width * spec->scaleX;
    rowSize = rowWidth * dmImageGetBytesPerPixel(spec->format);

    if ((row = dmMalloc(rowSize + 16)) == NULL)
    {
        res = DMERR_MALLOC;
        goto done;
    }

    // Generate the image
    for (y = 0; y < img->height; y++)
    {
        Uint8   *ptr1 = row,
                *ptr2 = ptr1 + rowWidth,
                *ptr3 = ptr2 + rowWidth,
                *ptr4 = ptr3 + rowWidth;

        for (x = 0; x < img->width; x++)
        {
            Uint8 c = img->data[(y * img->pitch) + (x * img->bpp) / 8],
                  qr, qg, qb, qa;

            switch (spec->format)
            {
                case DM_COLFMT_PALETTE:
                    for (xscale = 0; xscale < spec->scaleX; xscale++)
                        *ptr1++ = c;
                    break;

                case DM_COLFMT_RGBA:
                    qr = img->pal[c].r;
                    qg = img->pal[c].g;
                    qb = img->pal[c].b;
                    qa = img->pal[c].a;

                    if (spec->planar)
                    {
                        for (xscale = 0; xscale < spec->scaleX; xscale++)
                        {
                            *ptr1++ = qr;
                            *ptr2++ = qg;
                            *ptr3++ = qb;
                            *ptr4++ = qa;
                        }
                    }
                    else
                    {
                        for (xscale = 0; xscale < spec->scaleX; xscale++)
                        {
                            *ptr1++ = qr;
                            *ptr1++ = qg;
                            *ptr1++ = qb;
                            *ptr1++ = qa;
                        }
                    }
                    break;

                case DM_COLFMT_RGB:
                    qr = img->pal[c].r;
                    qg = img->pal[c].g;
                    qb = img->pal[c].b;

                    if (spec->planar)
                    {
                        for (xscale = 0; xscale < spec->scaleX; xscale++)
                        {
                            *ptr1++ = qr;
                            *ptr2++ = qg;
                            *ptr3++ = qb;
                        }
                    }
                    else
                    {
                        for (xscale = 0; xscale < spec->scaleX; xscale++)
                        {
                            *ptr1++ = qr;
                            *ptr1++ = qg;
                            *ptr1++ = qb;
                        }
                    }
                    break;
            }
        }

        for (yscale = 0; yscale < spec->scaleY; yscale++)
        {
            if ((res = writeRowCB(cbdata, row, rowSize)) != DMERR_OK)
                goto done;
        }
    }

done:
    dmFree(row);
    return res;
}


#define DMCOL(x) (((x) >> 4) & 0xf)

int dmWriteIFFMasterRAWHeader(
    DMResource *fp, const char *filename, const char *prefix,
    const DMImage *img, const DMImageConvSpec *spec, const int fmtid)
{
    dmfprintf(fp,
        "%s_width: dw.w %d\n"
        "%s_height: dw.w %d\n"
        "%s_nplanes: dw.w %d\n",
        prefix, img->width * spec->scaleX,
        prefix, img->height * spec->scaleY,
        prefix, spec->nplanes);

    if (fmtid == DM_IMGFMT_ARAW)
    {
        dmfprintf(fp,
            "%s_ncolors: dw.w %d\n"
            "%s_palette:\n",
            prefix, img->ncolors,
            prefix);

        for (int i = 0; i < (1 << spec->nplanes); i++)
        {
            Uint32 color;
            if (i < img->ncolors)
            {
                color = (DMCOL(img->pal[i].r) << 8) |
                        (DMCOL(img->pal[i].g) << 4) |
                        (DMCOL(img->pal[i].b));
            }
            else
                color = 0;

            dmfprintf(fp,
                "\tdc.w $%04X\n",
                color);
        }

        dmfprintf(fp,
            "%s: incbin \"%s\"\n",
            prefix, filename);
    }

    return DMERR_OK;
}


static BOOL dmWriteRAWRow(DMBitStreamContext *bs, const DMImage *img, const DMImageConvSpec *spec, const int yc, const int plane)
{
    const Uint8 *sp = img->data + (yc * img->pitch);
    for (int xc = 0; xc < img->width; xc++)
    {
        for (int xscale = 0; xscale < spec->scaleX; xscale++)
        if (!dmPutBits(bs, (sp[xc] & (1 << plane)) ? 1 : 0, 1))
            return FALSE;
    }
    return TRUE;
}


int dmWriteRAWImage(DMResource *fp, const DMImage *img, const DMImageConvSpec *spec)
{
    int res;
    DMBitStreamContext bs;

    if ((res = dmInitBitStreamFILE(&bs, fp)) != DMERR_OK)
        return res;

    if (spec->planar)
    {
        // Output bitplanes in planar format
        // (each plane of line sequentially)
        for (int yc = 0; yc < img->height; yc++)
        for (int yscale = 0; yscale < spec->scaleY; yscale++)
        for (int plane = 0; plane < spec->nplanes; plane++)
        {
            if (!dmWriteRAWRow(&bs, img, spec, yc, plane))
                return DMERR_FWRITE;
        }
    }
    else
    {
        // Output each bitplane in sequence
        for (int plane = 0; plane < spec->nplanes; plane++)
        for (int yc = 0; yc < img->height; yc++)
        for (int yscale = 0; yscale < spec->scaleY; yscale++)
        {
            if (!dmWriteRAWRow(&bs, img, spec, yc, plane))
                return DMERR_FWRITE;
        }
    }

    return dmFlushBitStream(&bs);
}


static int dmWritePPMRow(void *cbdata, const Uint8 *row, const size_t len)
{
    if (dmf_write_str((DMResource *) cbdata, row, len))
        return DMERR_OK;
    else
        return DMERR_FWRITE;
}


int dmWritePPMImage(DMResource *fp, const DMImage *img, const DMImageConvSpec *spec)
{
    DMImageConvSpec tmpSpec;

    // Write PPM header
    char *tmp = dm_strdup_printf(
        "P6\n%d %d\n255\n",
        img->width * spec->scaleX,
        img->height * spec->scaleY);

    if (tmp == NULL)
        return DMERR_MALLOC;

    dmfputs(tmp, fp);
    dmFree(tmp);

    // Write image data
    memcpy(&tmpSpec, spec, sizeof(DMImageConvSpec));
    tmpSpec.format = DM_COLFMT_RGB;
    return dmWriteImageData(img, (void *) fp, dmWritePPMRow, &tmpSpec);
}


#ifdef DM_USE_LIBPNG
static int fmtProbePNG(const Uint8 *buf, const size_t len)
{
    if (len > 64 && buf[0] == 0x89 &&
        buf[1] == 'P' && buf[2] == 'N' && buf[3] == 'G' &&
        buf[4] == 0x0d && buf[5] == 0x0a)
    {
        if (buf[12] == 'I' && buf[13] == 'H' &&
            buf[14] == 'D' && buf[15] == 'R')
            return DM_PROBE_SCORE_MAX;
        else
            return DM_PROBE_SCORE_GOOD;
    }

    return DM_PROBE_SCORE_FALSE;
}


static int dmWritePNGRow(void *cbdata, const Uint8 *row, const size_t len)
{
    png_structp png_ptr = cbdata;
    (void) len;

    if (setjmp(png_jmpbuf(png_ptr)))
        return DMERR_INTERNAL;

    png_write_row(png_ptr, row);

    return DMERR_OK;
}


static void dmPNGWriteData(png_structp png_ptr, png_bytep data, png_size_t length)
{
    DMResource *res = (DMResource *) png_get_io_ptr(png_ptr);

    // XXX TODO: How the fuck does one do error handling here?
    dmf_write_str(res, data, length);
}


static void dmPNGWriteFlush(png_structp png_ptr)
{
    (void) png_ptr;
}


int dmWritePNGImage(DMResource *fp, const DMImage *img, const DMImageConvSpec *spec)
{
    png_structp png_ptr = NULL;
    png_infop info_ptr = NULL;
    int fmt, res;

    // Create PNG structures
    png_ptr = png_create_write_struct(
        PNG_LIBPNG_VER_STRING,
        NULL, NULL, NULL);

    if (png_ptr == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "PNG: png_create_write_struct() failed.\n");
        goto error;
    }

    info_ptr = png_create_info_struct(png_ptr);
    if (info_ptr == NULL)
    {
        res = dmError(DMERR_INIT_FAIL,
            "PNG: png_create_info_struct(%p) failed.\n",
            png_ptr);
        goto error;
    }

    if (setjmp(png_jmpbuf(png_ptr)))
    {
        res = dmError(DMERR_INIT_FAIL,
            "PNG: Error during image writing..\n");
        goto error;
    }

    res = DMERR_OK;
    png_set_write_fn(png_ptr, fp, dmPNGWriteData, dmPNGWriteFlush);

    // Write PNG header info
    switch (spec->format)
    {
        case DM_COLFMT_PALETTE: fmt = PNG_COLOR_TYPE_PALETTE; break;
        case DM_COLFMT_RGB    : fmt = PNG_COLOR_TYPE_RGB; break;
        case DM_COLFMT_RGBA   : fmt = PNG_COLOR_TYPE_RGB_ALPHA; break;
        default:
            res = dmError(DMERR_NOT_SUPPORTED,
                "PNG: Unsupported image format %d.\n",
                spec->format);
            goto error;
    }

    png_set_IHDR(png_ptr, info_ptr,
        img->width * spec->scaleX,
        img->height * spec->scaleY,
        8,                    /* bits per component */
        fmt,
        PNG_INTERLACE_NONE,
        PNG_COMPRESSION_TYPE_DEFAULT,
        PNG_FILTER_TYPE_DEFAULT);

    dmMsg(2, "PNG: %d x %d, depth=%d, type=%d\n",
        img->width * spec->scaleX,
        img->height * spec->scaleY,
        8, fmt);

    // Palette
    if (spec->format == DM_COLFMT_PALETTE)
    {
        int i;
        png_colorp palette = png_malloc(png_ptr, PNG_MAX_PALETTE_LENGTH * sizeof(png_color));

        if (palette == NULL)
        {
            res = dmError(DMERR_MALLOC,
                "PNG: Could not allocate palette structure.");
            goto error;
        }

        dmMemset(palette, 0, PNG_MAX_PALETTE_LENGTH * sizeof(png_color));

        for (i = 0; i < img->ncolors; i++)
        {
            palette[i].red   = img->pal[i].r;
            palette[i].green = img->pal[i].g;
            palette[i].blue  = img->pal[i].b;
        }

        png_set_PLTE(png_ptr, info_ptr, palette, img->ncolors);
    }

//    png_set_gAMA(png_ptr, info_ptr, 2.2);

    png_write_info(png_ptr, info_ptr);


    // Write compressed image data
    dmWriteImageData(img, (void *) png_ptr, dmWritePNGRow, spec);

    // Write footer
    png_write_end(png_ptr, NULL);

error:
    if (png_ptr && info_ptr)
        png_destroy_write_struct(&png_ptr, &info_ptr);

    return res;
}


void dmPNGReadData(png_structp png_ptr, png_bytep data, png_size_t length)
{
    DMResource *res = (DMResource *) png_get_io_ptr(png_ptr);

    // XXX TODO: How the fuck does one do error handling here?
    dmf_read_str(res, data, length);
}


int dmReadPNGImage(DMResource *fp, DMImage **pimg)
{
    png_structp png_ptr = NULL;
    png_infop info_ptr = NULL;
    png_colorp palette = NULL;
    png_bytep *row_pointers = NULL;
    png_bytep trans = NULL;
    png_uint_32 width, height, res_x, res_y;
    int i, bit_depth, color_type, ncolors, ntrans, unit_type;
    int res = DMERR_OK;
    DMImage *img;

    // Create PNG structures
    png_ptr = png_create_read_struct(
        PNG_LIBPNG_VER_STRING,
        NULL, NULL, NULL);

    if (png_ptr == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "PNG: png_create_write_struct() failed.\n");
        goto error;
    }

    info_ptr = png_create_info_struct(png_ptr);
    if (info_ptr == NULL)
    {
        res = dmError(DMERR_INIT_FAIL,
            "PNG: png_create_info_struct(%p) failed.\n",
            png_ptr);
        goto error;
    }

    if (setjmp(png_jmpbuf(png_ptr)))
    {
        res = dmError(DMERR_INIT_FAIL,
            "PNG: Error during image reading.\n");
        goto error;
    }

    png_set_read_fn(png_ptr, fp, dmPNGReadData);

    // Read image information
    png_read_info(png_ptr, info_ptr);

    png_get_IHDR(png_ptr, info_ptr, &width, &height,
        &bit_depth, &color_type, NULL, NULL, NULL);

    if (width < 1 || height < 1)
    {
        res = dmError(DMERR_INVALID_DATA,
            "PNG: Invalid width or height (%d x %d)\n",
            width, height);
        goto error;
    }

    switch (color_type)
    {
        case PNG_COLOR_TYPE_GRAY:
            if (bit_depth < 8)
                png_set_expand_gray_1_2_4_to_8(png_ptr);

            if (bit_depth > 8)
            {
                res = dmError(DMERR_NOT_SUPPORTED,
                    "PNG: Unsupported bit depth for grayscale image: %d\n",
                    bit_depth);
                goto error;
            }
            break;

        case PNG_COLOR_TYPE_PALETTE:
            png_set_packing(png_ptr);
            break;

        default:
            res = dmError(DMERR_NOT_SUPPORTED,
                "PNG: RGB/RGBA images not supported for loading.\n");
            goto error;
    }

    // Allocate image
    dmMsg(2, "PNG: %d x %d, depth=%d, type=%d\n",
        width, height, bit_depth, color_type);

    if ((*pimg = img = dmImageAlloc(width, height,
        DM_COLFMT_PALETTE,
        // XXX TODO? When/if we ever handle < 8bit indexed correctly, we can use the actual bpp
        -1 /* bit_depth */)) == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "PNG: Could not allocate image data.\n");
        goto error;
    }

    // Set image aspect ratio
    png_get_pHYs(png_ptr, info_ptr, &res_x, &res_y, &unit_type);

    if (res_x > 0 && res_y > 0 &&
        res_y / res_x != (unsigned) (img->height / img->width))
        img->aspect = (float) res_y / (float) res_x;

    // ...
    row_pointers = png_malloc(png_ptr, height * sizeof(png_bytep));
    for (i = 0; i < img->height; i++)
        row_pointers[i] = img->data + (i * img->pitch);

    png_read_image(png_ptr, row_pointers);

    png_read_end(png_ptr, NULL);

    // Create palette
    switch (color_type)
    {
        case PNG_COLOR_TYPE_GRAY:
            ncolors = 256;
            dmMsg(2, "PNG: Generating %d color grayscale palette.\n", ncolors);

            if (!dmImagePaletteAlloc(img, ncolors, -1))
            {
                res = DMERR_MALLOC;
                goto error;
            }

            for (i = 0; i < img->ncolors; i++)
            {
                img->pal[i].r = img->pal[i].g = img->pal[i].b = i;
            }
            break;

        case PNG_COLOR_TYPE_PALETTE:
            png_get_PLTE(png_ptr, info_ptr, &palette, &ncolors);
            dmMsg(2, "PNG: Palette of %d colors found.\n", ncolors);
            if (ncolors > 0 && palette != NULL)
            {
                if (!dmImagePaletteAlloc(img, ncolors, -1))
                {
                    res = DMERR_MALLOC;
                    goto error;
                }

                for (i = 0; i < img->ncolors; i++)
                {
                    img->pal[i].r = palette[i].red;
                    img->pal[i].g = palette[i].green;
                    img->pal[i].b = palette[i].blue;
                }
            }
            break;
    }

    if (color_type == PNG_COLOR_TYPE_PALETTE ||
        color_type == PNG_COLOR_TYPE_GRAY)
    {
        png_get_tRNS(png_ptr, info_ptr, &trans, &ntrans, NULL);
        if (trans != NULL && ntrans > 0)
        {
            for (i = 0; i < img->ncolors && i < ntrans; i++)
            {
                img->pal[i].a = trans[i];
                if (img->ctransp < 0 && trans[i] == 0)
                    img->ctransp = i;
            }
        }
    }

error:
//    png_free(png_ptr, palette);

    if (png_ptr && info_ptr)
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);

    return res;
}
#endif


//
// Z-Soft PCX format
//
#define DMPCX_PAL_COLORS  16 // Number of internal palette colors

typedef struct
{
    Uint8 r,g,b;
} DMPCXColor;


typedef struct
{
    Uint8 manufacturer,     // always 0x0a
            version,        // Z-Soft PC Paintbrush version:
                            // 0 = v2.5
                            // 2 = v2.8 with palette,
                            // 3 = v2.8 without palette
                            // 4 = PC Paintbrush for Windows
                            // 5 = v3.0 or better
            encoding,       // usually 0x01 = RLE, 0x00 = uncompressed
            bitsPerPlane;   // bits per pixel per plane

    Uint16 xmin, ymin, xmax, ymax;
    Uint16 hres, vres;      // resolution in DPI, can be image dimensions as well.
    DMPCXColor colorMap[DMPCX_PAL_COLORS];
    Uint8 reserved;         // should be set to 0
    Uint8 nplanes;          // number of planes
    Uint16 bpl;             // bytes per plane LINE
    Uint16 palInfo;         // 1 = color/BW, 2 = grayscale
    Uint16 hScreenSize, vScreenSize;
    Uint8 filler[54];
} DMPCXHeader;


typedef struct
{
    DMPCXHeader *header;
    Uint8 *buf;
    size_t bufLen, bufOffs;
    DMResource *fp;
} DMPCXData;


static int fmtProbePCX(const Uint8 *buf, const size_t len)
{
    if (len > 128 + 32 &&

        (buf[1] == 5 || buf[1] == 2 || buf[1] == 3) &&
        buf[2] == 1 &&
        (buf[3] == 8 || buf[3] == 4 || buf[3] == 3 || buf[3] == 1) &&
        buf[65] >= 1 && buf[65] <= 4)
        return DM_PROBE_SCORE_GOOD;

    return DM_PROBE_SCORE_FALSE;
}


// Returns one byte from row buffer (of length len) at offset soffs,
// OR zero if the offset is outside buffer.
static inline Uint8 dmPCXGetByte(const Uint8 *row, const size_t len, const size_t soffs)
{
    return (soffs < len) ? row[soffs] : 0;
}

static BOOL dmPCXFlush(DMPCXData *pcx)
{
    BOOL ret = TRUE;
    if (pcx->bufOffs > 0)
        ret = dmf_write_str(pcx->fp, pcx->buf, pcx->bufOffs);

    pcx->bufOffs = 0;
    return ret;
}

static inline BOOL dmPCXPutByte(DMPCXData *pcx, const Uint8 val)
{
    if (pcx->bufOffs < pcx->bufLen)
    {
        pcx->buf[pcx->bufOffs++] = val;
        return TRUE;
    }
    else
        return dmPCXFlush(pcx);
}


static int dmPCXPutData(DMPCXData *pcx, const Uint8 data, const int count)
{
    if (count == 1 && (data & 0xC0) != 0xC0)
    {
        if (!dmPCXPutByte(pcx, data))
            return DMERR_FWRITE;
    }
    else
    {
        if (!dmPCXPutByte(pcx, 0xC0 | count) ||
            !dmPCXPutByte(pcx, data))
            return DMERR_FWRITE;
    }
    return DMERR_OK;
}


static int dmWritePCXRow(void *cbdata, const Uint8 *row, const size_t len)
{
    DMPCXData *pcx = (DMPCXData *) cbdata;
    int err;
    size_t soffs = 0;

    pcx->bufOffs = 0;

    for (int plane = 0; plane < pcx->header->nplanes; plane++)
    {
        Uint8 data = dmPCXGetByte(row, len, soffs++),
              count = 1;

        size_t blen = pcx->header->bpl * pcx->header->nplanes;
        while (soffs < blen)
        {
            if (data == dmPCXGetByte(row, len, soffs) && count < 0x3F)
            {
                count++;
                soffs++;
            }
            else
            {
                if ((err = dmPCXPutData(pcx, data, count)) != DMERR_OK)
                    return err;

                data = dmPCXGetByte(row, len, soffs++);
                count = 1;
            }
        }

        if ((err = dmPCXPutData(pcx, data, count)) != DMERR_OK)
            return err;

        if (!dmPCXFlush(pcx))
            return DMERR_FWRITE;
    }


    return DMERR_OK;
}


int dmWritePCXImage(DMResource *fp, const DMImage *img, const DMImageConvSpec *pspec)
{
    DMPCXData pcx;
    DMPCXHeader hdr;
    DMImageConvSpec spec;
    int res;

    // Always force planar for PCX
    memcpy(&spec, pspec, sizeof(DMImageConvSpec));
    spec.planar = TRUE;

    // XXX: 24bit PCX does not work yet ..
    if (!spec.paletted)
    {
        return dmError(DMERR_NOT_SUPPORTED,
            "24bit PCX not supported yet.\n");
    }

    if (spec.paletted && img->pal == NULL)
    {
        return dmError(DMERR_NULLPTR,
            "Image spec says paletted/indexed image, but palette pointer is NULL.\n");
    }

    // Create output file
    pcx.buf    = NULL;
    pcx.header = &hdr;
    pcx.fp     = fp;

    // Create PCX header
    dmMemset(&hdr, 0, sizeof(hdr));
    if (spec.paletted)
    {
        for (int i = 0; i < (img->ncolors > DMPCX_PAL_COLORS ? DMPCX_PAL_COLORS : img->ncolors); i++)
        {
            hdr.colorMap[i].r = img->pal[i].r;
            hdr.colorMap[i].g = img->pal[i].g;
            hdr.colorMap[i].b = img->pal[i].b;
        }
    }
    hdr.manufacturer = 10;
    hdr.version      = 5;
    hdr.encoding     = 1;
    hdr.hres         = img->width * spec.scaleX;
    hdr.vres         = img->height * spec.scaleY;
    hdr.xmin         = hdr.ymin = 0;
    hdr.xmax         = (img->width * spec.scaleX) - 1;
    hdr.ymax         = (img->height * spec.scaleY) - 1;
    hdr.palInfo      = 1;
    hdr.hScreenSize  = hdr.hres;
    hdr.vScreenSize  = hdr.vres;

    // TODO XXX .. maybe actually compute these asdf
    hdr.bitsPerPlane = 8;
    hdr.nplanes      = dmImageGetBytesPerPixel(spec.format);

    res = img->width * spec.scaleX;
    hdr.bpl = res / 2;
    if (res % 2) hdr.bpl++;
    hdr.bpl *= 2;

    dmMsg(2,
        "PCX: xmin=%d, ymin=%d, xmax=%d, ymax=%d, res=%dx%d, scr=%dx%d\n",
        hdr.xmin, hdr.ymin, hdr.xmax, hdr.ymax,
        hdr.hres, hdr.vres,
        hdr.hScreenSize, hdr.vScreenSize);

    dmMsg(2, "PCX: nplanes=%d, bpp=%d, bpl=%d, isPaletted=%s, planar=%s\n",
        hdr.nplanes, hdr.bitsPerPlane, hdr.bpl,
        spec.paletted ? "yes" : "no",
        spec.planar ? "yes" : "no"
        );

    // TODO XXX this is also bogus
    pcx.bufLen       = hdr.bpl * 4;
    if ((pcx.buf = dmMalloc(pcx.bufLen)) == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "PCX: Could not allocate %d bytes for RLE compression buffer.\n",
            pcx.bufLen);
        goto error;
    }

    // Write PCX header
    if (!dmf_write_byte(pcx.fp, hdr.manufacturer) ||
        !dmf_write_byte(pcx.fp, hdr.version) ||
        !dmf_write_byte(pcx.fp, hdr.encoding) ||
        !dmf_write_byte(pcx.fp, hdr.bitsPerPlane))
    {
        res = dmError(DMERR_FWRITE,
            "PCX: Could not write basic header data.\n");
        goto error;
    }

    if (!dmf_write_le16(pcx.fp, hdr.xmin) ||
        !dmf_write_le16(pcx.fp, hdr.ymin) ||
        !dmf_write_le16(pcx.fp, hdr.xmax) ||
        !dmf_write_le16(pcx.fp, hdr.ymax) ||
        !dmf_write_le16(pcx.fp, hdr.hres) ||
        !dmf_write_le16(pcx.fp, hdr.vres))
    {
        res = dmError(DMERR_FWRITE,
            "PCX: Could not write image dimensions.\n");
        goto error;
    }

    if (!dmf_write_str(pcx.fp, (Uint8 *) &hdr.colorMap, sizeof(hdr.colorMap)))
    {
        res = dmError(DMERR_FWRITE,
            "PCX: Could not write colormap.\n");
        goto error;
    }

    if (!dmf_write_byte(pcx.fp, hdr.reserved) ||
        !dmf_write_byte(pcx.fp, hdr.nplanes) ||
        !dmf_write_le16(pcx.fp, hdr.bpl) ||
        !dmf_write_le16(pcx.fp, hdr.palInfo) ||
        !dmf_write_le16(pcx.fp, hdr.hScreenSize) ||
        !dmf_write_le16(pcx.fp, hdr.vScreenSize) ||
        !dmf_write_str(pcx.fp, (Uint8 *) &hdr.filler, sizeof(hdr.filler)))
    {
        res = dmError(DMERR_FWRITE,
            "PCX: Could not write header remainder.\n");
        goto error;
    }

    // Write image data
    res = dmWriteImageData(img, (void *) &pcx, dmWritePCXRow, &spec);

    // Write VGA palette
    if (spec.paletted)
    {
        int i;
        dmMsg(2, "PCX: Writing palette of %d active entries.\n", img->ncolors);

        dmf_write_byte(pcx.fp, 0x0C);

        for (i = 0; i < img->ncolors; i++)
        {
            if (!dmf_write_byte(pcx.fp, img->pal[i].r) ||
                !dmf_write_byte(pcx.fp, img->pal[i].g) ||
                !dmf_write_byte(pcx.fp, img->pal[i].b))
            {
                res = dmError(DMERR_FWRITE,
                    "PCX: Could not write palette data.\n");
                goto error;
            }
        }

        // Pad the palette, if necessary
        for (; i < 256; i++)
        {
            if (!dmf_write_byte(pcx.fp, 0) ||
                !dmf_write_byte(pcx.fp, 0) ||
                !dmf_write_byte(pcx.fp, 0))
            {
                res = dmError(DMERR_FWRITE,
                    "PCX: Could not write palette data.\n");
                goto error;
            }
        }
    }

error:
    dmFree(pcx.buf);
    return res;
}


static BOOL dmPCXDecodeRLERow(DMResource *fp, Uint8 *buf, const size_t bufLen)
{
    size_t offs = 0;
    do
    {
        int count;
        Uint8 data;

        if (!dmf_read_byte(fp, &data))
            return FALSE;

        if ((data & 0xC0) == 0xC0)
        {
            BOOL skip = FALSE;
            count = data & 0x3F;
            if (count == 0)
            {
                switch (dmGFXErrorMode)
                {
                    case DM_ERRMODE_RECOV_1:
                        // Use as literal
                        skip = TRUE;
                        count = 1;
                        break;

                    case DM_ERRMODE_RECOV_2:
                        // Ignore completely
                        skip = TRUE;
                        break;

                    case DM_ERRMODE_FAIL:
                    default:
                        // Error out on "invalid" data
                        return FALSE;
                }
            }

            if (!skip && !dmf_read_byte(fp, &data))
                return FALSE;
        }
        else
            count = 1;

        while (count-- && offs < bufLen)
            buf[offs++] = data;

        // Check for remaining output count, error out if we wish to
        if (count > 0 && dmGFXErrorMode == DM_ERRMODE_FAIL)
            return FALSE;

    } while (offs < bufLen);

    return TRUE;
}


int dmReadPCXImage(DMResource *fp, DMImage **pimg)
{
    DMImage *img;
    DMPCXData pcx;
    DMPCXHeader hdr;
    int res = 0;
    BOOL isPaletted;
    pcx.buf = NULL;

    // Read PCX header
    if (!dmf_read_byte(fp, &hdr.manufacturer) ||
        !dmf_read_byte(fp, &hdr.version) ||
        !dmf_read_byte(fp, &hdr.encoding) ||
        !dmf_read_byte(fp, &hdr.bitsPerPlane) ||
        !dmf_read_le16(fp, &hdr.xmin) ||
        !dmf_read_le16(fp, &hdr.ymin) ||
        !dmf_read_le16(fp, &hdr.xmax) ||
        !dmf_read_le16(fp, &hdr.ymax) ||
        !dmf_read_le16(fp, &hdr.hres) ||
        !dmf_read_le16(fp, &hdr.vres) ||
        !dmf_read_str(fp, (Uint8 *) &hdr.colorMap, sizeof(hdr.colorMap)) ||
        !dmf_read_byte(fp, &hdr.reserved) ||
        !dmf_read_byte(fp, &hdr.nplanes) ||
        !dmf_read_le16(fp, &hdr.bpl) ||
        !dmf_read_le16(fp, &hdr.palInfo) ||
        !dmf_read_le16(fp, &hdr.hScreenSize) ||
        !dmf_read_le16(fp, &hdr.vScreenSize) ||
        !dmf_read_str(fp, (Uint8 *) &hdr.filler, sizeof(hdr.filler)))
    {
        res = dmError(DMERR_FREAD,
            "PCX: Could not read image header data.\n");
        goto error;
    }

    if (hdr.manufacturer != 10 ||
        hdr.version > 5 ||
        hdr.encoding != 1)
    {
        res = dmError(DMERR_NOT_SUPPORTED,
            "PCX: Not a PCX file, or unsupported variant.\n");
        goto error;
    }

    if (hdr.nplanes == 4 && hdr.bitsPerPlane == 4)
    {
        dmMsg(2,
            "PCX: Probably invalid combination of nplanes and bpp, attempting to fix ..\n");

        hdr.bitsPerPlane = 1;
    }

    isPaletted = (hdr.bitsPerPlane * hdr.nplanes) <= 8;

    dmMsg(2,
        "PCX: xmin=%d, ymin=%d, xmax=%d, ymax=%d, res=%dx%d, scr=%dx%d\n",
        hdr.xmin, hdr.ymin, hdr.xmax, hdr.ymax,
        hdr.hres, hdr.vres,
        hdr.hScreenSize, hdr.vScreenSize);

    dmMsg(2,
        "PCX: nplanes=%d, bpp=%d, bpl=%d, isPaletted=%s\n",
        hdr.nplanes, hdr.bitsPerPlane, hdr.bpl, isPaletted ? "yes" : "no");

    if (hdr.nplanes < 1 || hdr.nplanes > 8)
    {
        res = dmError(DMERR_NOT_SUPPORTED,
            "PCX: Unsupported number of bitplanes %d.\n",
            hdr.nplanes);
        goto error;
    }

    if (!isPaletted)
    {
        res = dmError(DMERR_NOT_SUPPORTED,
            "PCX: Non-indexed (truecolour) PCX images not supported for loading.\n");
        goto error;
    }

    // Allocate image
    if ((*pimg = img = dmImageAlloc(hdr.xmax - hdr.xmin + 1, hdr.ymax - hdr.ymin + 1,
        isPaletted ? DM_COLFMT_PALETTE : DM_COLFMT_RGBA,
        // XXX TODO? When/if we ever handle < 8bit indexed correctly, we can use the actual bpp
        // isPaletted ? (hdr.bitsPerPlane * hdr.nplanes) : -1
        -1
        )) == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "PCX: Could not allocate image structure.\n");
        goto error;
    }

    // Set image aspect ratio
    if (hdr.hScreenSize > 0 && hdr.vScreenSize > 0 &&
        hdr.vScreenSize / hdr.hScreenSize != img->height / img->width)
        img->aspect = (float) hdr.vScreenSize / (float) hdr.hScreenSize;

    // Sanity check bytes per line value
    if (hdr.bpl < (img->width * hdr.bitsPerPlane) / 8)
    {
        res = dmError(DMERR_MALLOC,
            "PCX: The bytes per plane line value %d is smaller than width*bpp/8 = %d!\n",
            hdr.bpl, (img->width * hdr.bitsPerPlane) / 8);
        goto error;
    }

    pcx.bufLen = hdr.nplanes * hdr.bpl;
    if ((pcx.buf = dmMalloc(pcx.bufLen)) == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "PCX: Could not allocate RLE buffer.\n");
        goto error;
    }

    dmMsg(2,
        "PCX: bufLen=%d\n",
        pcx.bufLen);

    // Read image data
    Uint8 *dp = img->data;
    for (int yc = 0; yc < img->height; yc++)
    {
        // Decode row of RLE'd data
        if (!dmPCXDecodeRLERow(fp, pcx.buf, pcx.bufLen))
        {
            res = dmError(DMERR_INVALID_DATA,
                "PCX: Error decoding RLE compressed data.\n");
            goto error;
        }

        // Decode bitplanes
        switch (hdr.bitsPerPlane)
        {
            case 32:
            case 24:
            case 16:
            case 8:
                {
                    // Actually bytes and bits per plane per pixel ..
                    const int bytesPerPlane = hdr.bitsPerPlane / 8;

                    for (int nplane = 0; nplane < hdr.nplanes; nplane++)
                    {
                        Uint8 *dptr = dp + (nplane * bytesPerPlane),
                              *sptr = pcx.buf + (hdr.bpl * nplane);

                        memcpy(dptr, sptr, img->width * bytesPerPlane);
                    }
                }
                break;

            case 1:
                dmMemset(dp, 0, img->width);

                for (int nplane = 0; nplane < hdr.nplanes; nplane++)
                {
                    Uint8 *sptr = pcx.buf + (hdr.bpl * nplane);

                    for (int xc = 0; xc < img->width; xc++)
                    {
                        const int px = 7 - (xc & 7);
                        dp[xc] |= ((sptr[xc / 8] & (1 << px)) >> px) << nplane;
                    }
                }
                break;

            default:
                res = dmError(DMERR_NOT_SUPPORTED,
                    "PCX: Unsupported number of bits per plane %d.\n",
                    hdr.bitsPerPlane);
                goto error;
        }

        dp += img->pitch;
    }

    // Read additional VGA palette, if available
    if (isPaletted)
    {
        int ncolors;
        Uint8 tmpb;
        BOOL read;

        if (!dmf_read_byte(fp, &tmpb) || tmpb != 0x0C)
        {
            read = FALSE;
            ncolors = DMPCX_PAL_COLORS;
        }
        else
        {
            read = TRUE;
            ncolors = 256;
        }

        if (!dmImagePaletteAlloc(img, ncolors, -1))
        {
            res = dmError(DMERR_MALLOC,
                "PCX: Could not allocate palette data!\n");
            goto error;
        }

        if (read)
        {
            // Okay, attempt to read the palette data
            dmMsg(2, "PCX: Reading palette of %d colors\n", ncolors);
            if (!dmPaletteReadData(fp, img->pal, ncolors))
            {
                res = dmError(DMERR_FREAD,
                    "PCX: Error reading palette.\n");
                goto error;
            }
        }
        else
        {
            // If the extra palette is not available, copy the colors from
            // the header palette to our internal palette structure.
            dmMsg(2, "PCX: Initializing palette from header of %d colors\n", ncolors);
            for (int i = 0; i < (img->ncolors > DMPCX_PAL_COLORS ? DMPCX_PAL_COLORS : img->ncolors); i++)
            {
                img->pal[i].r = hdr.colorMap[i].r;
                img->pal[i].g = hdr.colorMap[i].g;
                img->pal[i].b = hdr.colorMap[i].b;
            }
        }
    }

error:
    dmFree(pcx.buf);
    return res;
}


//
// IFF ILBM / PBM format
//
#define IFF_ID_FORM        0x464F524D // "FORM"
#define IFF_ID_ILBM        0x494C424D // "ILBM"
#define IFF_ID_PBM         0x50424D20 // "PBM "
#define IFF_ID_BMHD        0x424D4844 // "BMHD"
#define IFF_ID_CMAP        0x434D4150 // "CMAP"
#define IFF_ID_BODY        0x424F4459 // "BODY"
#define IFF_ID_CAMG        0x43414D47 // "CAMG"

#define IFF_MASK_NONE      0
#define IFF_MASK_HAS_MASK  1
#define IFF_MASK_TRANSP    2
#define IFF_MASK_LASSO     3

#define IFF_COMP_NONE      0
#define IFF_COMP_BYTERUN1  1

#define IFF_CAMG_LACE      0x00000004
#define IFF_CAMG_HALFBRITE 0x00000080
#define IFF_CAMG_HAM       0x00000800

typedef struct
{
    Uint32 id;
    Uint32 size;
    int count;
    char idStr[6];
    off_t offs;
} DMIFFChunk;


typedef struct
{
    Uint16 w, h;
    Sint16 x, y;
    Uint8  nplanes;
    Uint8  masking;
    Uint8  compression;
    Uint8  pad1;
    Uint16 transp;
    Uint8  xasp, yasp;
    Sint16 pagew, pageh;
} DMIFFBMHD;


typedef struct
{
    DMIFFChunk chFORM, chBMHD, chCMAP, chBODY;
    DMIFFBMHD bmhd;
    Uint32 camg;
    int ncolors;
    DMColor *pal;
} DMIFF;


static int fmtProbeIFF(const Uint8 *buf, const size_t len)
{
    if (len > 32 &&
        buf[ 0] == 'F' && buf[ 1] == 'O' &&
        buf[ 2] == 'R' && buf[ 3] == 'M' && (
        (buf[ 8] == 'I' && buf[ 9] == 'L' && buf[10] == 'B' && buf[11] == 'M') ||
        (buf[ 8] == 'P' && buf[ 9] == 'B' && buf[10] == 'M' && buf[11] == 0x20)
        ))
    {
        if (buf[12] == 'B' && buf[13] == 'M' &&
            buf[14] == 'H' && buf[15] == 'D')
            return DM_PROBE_SCORE_MAX;
        else
            return DM_PROBE_SCORE_GOOD;
    }

    return DM_PROBE_SCORE_FALSE;
}


static void dmMakeIFFChunkIDStr(DMIFFChunk *chunk)
{
    chunk->idStr[0] = (chunk->id >> 24) & 0xff;
    chunk->idStr[1] = (chunk->id >> 16) & 0xff;
    chunk->idStr[2] = (chunk->id >> 8) & 0xff;
    chunk->idStr[3] = (chunk->id) & 0xff;
    chunk->idStr[4] = 0;
}


static int dmReadIFFChunkHdr(DMResource *fp, DMIFFChunk *chunk)
{
    if (!dmf_read_be32(fp, &chunk->id) ||
        !dmf_read_be32(fp, &chunk->size))
    {
        return dmError(DMERR_FREAD,
            "IFF: Could not read IFF chunk header.\n");
    }
    else
    {
        chunk->offs = dmftell(fp);
        dmMakeIFFChunkIDStr(chunk);
        return DMERR_OK;
    }
}


static int dmSkipIFFChunkRest(DMResource *fp, const DMIFFChunk *chunk)
{
    off_t read = dmftell(fp) - chunk->offs;
    if (chunk->size > read)
    {
        dmMsg(4, "IFF: Skipping %d bytes (%d of %d consumed)\n",
            chunk->size - read, read, chunk->size);

        if (dmfseek(fp, chunk->size - read, SEEK_CUR) != 0)
        {
            return dmError(DMERR_FSEEK,
                "IFF: Failed to skip chunk end.\n");
        }
        else
            return DMERR_OK;
    }
    else
        return DMERR_OK;
}


static int dmCheckIFFChunk(DMIFFChunk *dest, DMIFFChunk *chunk,
    const BOOL multi, const Uint32 minSize)
{
    if (dest->count > 0 && !multi)
    {
        return dmError(DMERR_INVALID_DATA,
            "IFF: Multiple instances of chunk %s found.\n",
            chunk->idStr);
    }

    dest->count++;

    if (chunk->size < minSize)
    {
        return dmError(DMERR_OUT_OF_DATA,
            "IFF: Chunk is too small.\n");
    }

    return DMERR_OK;
}


static BOOL dmIFFDecodeByteRun1Row(DMResource *fp, Uint8 *buf, const size_t bufLen)
{
    size_t offs = 0;
    do
    {
        Sint8 dcount;
        Uint8 data;

        if (!dmf_read_byte(fp, (Uint8 *) &dcount))
            return FALSE;

        if (dcount == -128)
        {
            if (!dmf_read_byte(fp, &data))
                return FALSE;
        }
        else
        if (dcount < 0)
        {
            int count = (-dcount) + 1;
            if (!dmf_read_byte(fp, &data))
                return FALSE;

            while (count-- && offs < bufLen)
                buf[offs++] = data;
        }
        else
        {
            int count = dcount + 1;
            while (count-- && offs < bufLen)
            {
                if (!dmf_read_byte(fp, &data))
                    return FALSE;

                buf[offs++] = data;
            }
        }
    } while (offs < bufLen);

    return TRUE;
}


static BOOL dmIFFReadOneRow(DMResource *fp, DMIFF *iff, Uint8 *buf, const size_t bufLen)
{
    if (iff->bmhd.compression == IFF_COMP_BYTERUN1)
        return dmIFFDecodeByteRun1Row(fp, buf, bufLen);
    else
        return dmf_read_str(fp, buf, bufLen);
}


static inline Uint8 dmDecodeBit(const Uint8 *buf, const int xc)
{
    return (buf[xc / 8] >> (7 - (xc & 7))) & 1;
}


static void dmDecodeBitPlane(Uint8 *dst, const Uint8 *src, const int width, const int nplane)
{
    for (int xc = 0; xc < width; xc++)
        dst[xc] |= dmDecodeBit(src, xc) << nplane;
}


static int dmDecodeILBMBody(DMResource *fp, DMIFF *iff, DMImage *img)
{
    Uint8 *buf;
    size_t bufLen;
    int res = DMERR_OK;
    const int nplanes = iff->bmhd.nplanes;

    // Allocate planar decoding buffer
    bufLen = ((img->width + 15) / 16) * 2;
    if ((buf = dmMalloc(bufLen)) == NULL)
        return DMERR_MALLOC;

    dmMsg(2, "IFF: plane row size %d bytes.\n", bufLen);

    // Decode the chunk
    for (int yc = 0; yc < img->height; yc++)
    {
        Uint8 *dp = img->data + (yc * img->pitch);

        dmMemset(dp, 0, img->pitch);

        for (int plane = 0; plane < nplanes; plane++)
        {
            // Decompress or read data
            if (!dmIFFReadOneRow(fp, iff, buf, bufLen))
            {
                res = dmError(DMERR_FREAD,
                    "IFF: Error in reading image plane #%d @ %d.\n",
                    plane, yc);
                goto error;
            }

            // Decode bitplane
            dmDecodeBitPlane(dp, buf, img->width, plane);
        }

        // Read mask data
        if (iff->bmhd.masking == IFF_MASK_HAS_MASK)
        {
            // Decompress or read data
            if (!dmIFFReadOneRow(fp, iff, buf, bufLen))
            {
                res = dmError(DMERR_FREAD,
                    "IFF: Error in reading mask plane.\n");
                goto error;
            }

            // Decode mask
            for (int xc = 0; xc < img->width; xc++)
            {
                const Uint8 data = dmDecodeBit(buf, xc);

                // Black out any pixels with mask bit 0
                if (!data)
                    dp[xc] = img->ctransp < 0 ? 0 : img->ctransp;
            }
        }
    }

error:
    dmFree(buf);
    return res;
}


static int dmDecodePBMBody(DMResource *fp, DMIFF *iff, DMImage *img)
{
    for (int yc = 0; yc < img->height; yc++)
    {
        if (!dmIFFReadOneRow(fp, iff, img->data + (yc * img->pitch), img->width))
        {
            return dmError(DMERR_FREAD,
                "IFF: Error reading PBM image row #%d.\n", yc);
        }
    }

    return DMERR_OK;
}


int dmReadIFFImage(DMResource *fp, DMImage **pimg)
{
    DMIFFChunk chunk;
    DMIFF iff;
    Uint32 idsig;
    BOOL parsed = FALSE, planar;
    int res = DMERR_OK;

    dmMemset(&iff, 0, sizeof(iff));

    // Read IFF FORM header
    if ((res = dmReadIFFChunkHdr(fp, &chunk)) != DMERR_OK)
        return res;

    if (chunk.id != IFF_ID_FORM || chunk.size < 32)
    {
        return dmError(DMERR_NOT_SUPPORTED,
            "IFF: Not a IFF file (%08X vs %08X / %d).\n",
            chunk.id, IFF_ID_FORM, chunk.size);
    }

    // Check IFF ILBM/PBM signature
    if (!dmf_read_be32(fp, &idsig) ||
        (idsig != IFF_ID_ILBM && idsig != IFF_ID_PBM))
    {
        return dmError(DMERR_INVALID_DATA,
            "IFF: Not a IFF ILBM/PBM file.\n");
    }

    planar = (idsig == IFF_ID_ILBM);

    while (!parsed && !dmfeof(fp))
    {
        // Read chunk header
        if ((res = dmReadIFFChunkHdr(fp, &chunk)) != DMERR_OK)
            return res;

        switch (chunk.id)
        {
            case IFF_ID_BMHD:
                // Check for multiple occurences of BMHD
                if ((res = dmCheckIFFChunk(&iff.chBMHD, &chunk, FALSE, sizeof(iff.bmhd))) != DMERR_OK)
                    return res;

                // Read BMHD data
                if (!dmf_read_be16(fp, &iff.bmhd.w) ||
                    !dmf_read_be16(fp, &iff.bmhd.h) ||
                    !dmf_read_be16(fp, (Uint16 *) &iff.bmhd.x) ||
                    !dmf_read_be16(fp, (Uint16 *) &iff.bmhd.y) ||
                    !dmf_read_byte(fp, &iff.bmhd.nplanes) ||
                    !dmf_read_byte(fp, &iff.bmhd.masking) ||
                    !dmf_read_byte(fp, &iff.bmhd.compression) ||
                    !dmf_read_byte(fp, &iff.bmhd.pad1) ||
                    !dmf_read_be16(fp, &iff.bmhd.transp) ||
                    !dmf_read_byte(fp, &iff.bmhd.xasp) ||
                    !dmf_read_byte(fp, &iff.bmhd.yasp) ||
                    !dmf_read_be16(fp, (Uint16 *) &iff.bmhd.pagew) ||
                    !dmf_read_be16(fp, (Uint16 *) &iff.bmhd.pageh))
                {
                    return dmError(DMERR_FREAD,
                        "IFF: Error reading BMHD chunk.\n");
                }

                dmMsg(1, "IFF: BMHD %d x %d @ %d, %d : nplanes=%d, comp=%d, mask=%d, transp=%d\n",
                    iff.bmhd.w, iff.bmhd.h, iff.bmhd.x, iff.bmhd.y,
                    iff.bmhd.nplanes, iff.bmhd.compression, iff.bmhd.masking,
                    iff.bmhd.transp);

                // Sanity check
                if (iff.bmhd.nplanes < 1 || iff.bmhd.nplanes > 8 ||
                    (iff.bmhd.compression != IFF_COMP_NONE &&
                    iff.bmhd.compression != IFF_COMP_BYTERUN1) ||
                    (iff.bmhd.masking != IFF_MASK_NONE &&
                    iff.bmhd.masking != IFF_MASK_HAS_MASK &&
                    iff.bmhd.masking != IFF_MASK_TRANSP))
                {
                    return dmError(DMERR_NOT_SUPPORTED,
                        "IFF: Unsupported features, refusing to load.\n");
                }
                break;


            case IFF_ID_CMAP:
                // Check for multiple occurences of CMAP
                if ((res = dmCheckIFFChunk(&iff.chCMAP, &chunk, FALSE, 3)) != DMERR_OK)
                    return res;

                // Check for sanity
                if (chunk.size % 3 != 0)
                {
                    // Non-fatal
                    dmError(DMERR_INVALID_DATA,
                    "IFF: CMAP chunk size not divisible by 3, possibly broken file.\n");
                }

                iff.ncolors = chunk.size / 3;
                dmMsg(2, "IFF: CMAP %d entries (%d bytes)\n",
                    iff.ncolors, chunk.size);

                if (iff.bmhd.nplanes > 0 && iff.ncolors != 1 << iff.bmhd.nplanes)
                    dmMsg(2, "IFF: Expected %d entries in CMAP.\n", 1 << iff.bmhd.nplanes);

                // Read palette
                if (iff.ncolors > 0)
                {
                    if (!dmPaletteAlloc(&iff.pal, iff.ncolors,
                        (iff.bmhd.masking == IFF_MASK_TRANSP) ? iff.bmhd.transp : -1))
                    {
                        return dmError(DMERR_MALLOC,
                            "IFF: Could not allocate palette data.\n");
                    }
                    if (!dmPaletteReadData(fp, iff.pal, iff.ncolors))
                    {
                        return dmError(DMERR_FREAD,
                            "IFF: Error reading CMAP.\n");
                    }
                }

                if (iff.chBMHD.count && iff.chBODY.count)
                    parsed = TRUE;
                break;

            case IFF_ID_BODY:
                // Check for multiple occurences of BODY
                if ((res = dmCheckIFFChunk(&iff.chBODY, &chunk, FALSE, 1)) != DMERR_OK)
                    return res;

                // Check for sanity
                if (!iff.chBMHD.count)
                {
                    return dmError(DMERR_INVALID_DATA,
                        "IFF: BODY chunk before BMHD?\n");
                }

                dmMsg(2, "IFF: BODY chunk size %d bytes\n", chunk.size);

                // Allocate image
                if ((*pimg = dmImageAlloc(iff.bmhd.w, iff.bmhd.h,
                    iff.bmhd.nplanes <= 8 ? DM_COLFMT_PALETTE : DM_COLFMT_RGBA,
                    // XXX TODO? When/if we ever handle < 8bit indexed correctly, we can use the actual bpp
                    //iff->bmhd.nplanes <= 8 ? iff->bmhd.nplanes : -1
                    -1
                    )) == NULL)
                    return DMERR_MALLOC;

                // Set image aspect ratio
                if (iff.bmhd.xasp > 0 && iff.bmhd.yasp > 0)
                    (*pimg)->aspect = (float) iff.bmhd.yasp / (float) iff.bmhd.xasp;

                // Decode the body
                if (planar)
                {
                    if ((res = dmDecodeILBMBody(fp, &iff, *pimg)) != DMERR_OK)
                        return res;
                }
                else
                {
                    if ((res = dmDecodePBMBody(fp, &iff, *pimg)) != DMERR_OK)
                        return res;
                }

                if ((res = dmSkipIFFChunkRest(fp, &chunk)) != DMERR_OK)
                    return res;

                if (iff.chCMAP.count)
                    parsed = TRUE;
                break;


            case IFF_ID_CAMG:
                if (!dmf_read_be32(fp, &iff.camg))
                {
                    return dmError(DMERR_FREAD,
                        "IFF: Error reading CAMG chunk.\n");
                }

                dmMsg(2, "IFF: CAMG value 0x%08x\n", iff.camg);

                if ((iff.camg & IFF_CAMG_HAM))
                {
                    return dmError(DMERR_NOT_SUPPORTED,
                        "IFF: HAM files are not supported.\n");
                }
                break;


            default:
                {
                    dmMsg(4, "Unknown chunk ID '%s', size %d\n",
                        chunk.idStr, chunk.size);

                    if (dmfseek(fp, chunk.size, SEEK_CUR) != 0)
                    {
                        return dmError(DMERR_FSEEK,
                            "IFF: Error skipping in file.");
                    }
                }
                break;
        }

        if ((res = dmSkipIFFChunkRest(fp, &chunk)) != DMERR_OK)
            return res;
    }

    // Set colormap after finishing
    if (iff.pal != NULL && iff.ncolors > 0 && *pimg != NULL)
    {
        // If halfbrite is used, duplicate the palette
        if (iff.camg & IFF_CAMG_HALFBRITE)
        {
            void *ptmp;
            if (!planar)
            {
                dmErrorMsg("IFF: Non-planar PBM file with Halfbrite enabled! This might not work.\n");
            }

            if (iff.ncolors > 128)
            {
                return dmError(DMERR_NOT_SUPPORTED,
                    "IFF: Halfbrite enabled, but ncolors > 128.\n");
            }

            if ((ptmp = dmRealloc(iff.pal, sizeof(DMColor) * iff.ncolors * 2)) == NULL)
            {
                dmFree(iff.pal);
                iff.pal = NULL;
                return DMERR_MALLOC;
            }
            else
                iff.pal = ptmp;

            for (int i = 0; i < iff.ncolors; i++)
            {
                int i2 = iff.ncolors + i;
                iff.pal[i2].r = iff.pal[i].r / 2;
                iff.pal[i2].g = iff.pal[i].g / 2;
                iff.pal[i2].b = iff.pal[i].b / 2;
            }
        }

        (*pimg)->ncolors = iff.ncolors;
        (*pimg)->pal = iff.pal;
    }

    return res;
}


static int dmWriteIFFChunkHdr(DMResource *fp, DMIFFChunk *chunk, const Uint32 id)
{
    chunk->offs = dmftell(fp);
    chunk->id = id;
    dmMakeIFFChunkIDStr(chunk);

    if (!dmf_write_be32(fp, chunk->id) ||
        !dmf_write_be32(fp, chunk->size))
    {
        return dmError(DMERR_FREAD,
            "IFF: Could not write IFF '%s' chunk header.\n",
            chunk->idStr);
    }
    else
        return DMERR_OK;
}


static int dmWriteIFFChunkFinish(DMResource *fp, DMIFFChunk *chunk)
{
    off_t curr = dmftell(fp);
    if (curr < 0)
        return dmferror(fp);

    chunk->size = curr - chunk->offs - (sizeof(Uint32) * 2);
    if ((chunk->size & 1) && !dmf_write_byte(fp, 0))
        return dmferror(fp);

    if (dmfseek(fp, chunk->offs, SEEK_SET) < 0)
        return dmferror(fp);

    if (!dmf_write_be32(fp, chunk->id) ||
        !dmf_write_be32(fp, chunk->size))
    {
        return dmError(DMERR_FREAD,
            "IFF: Could not write IFF '%s' chunk header.\n",
            chunk->idStr);
    }

    if (dmfseek(fp, curr, SEEK_SET) < 0)
        return dmferror(fp);

    return DMERR_OK;
}


enum
{
    DMODE_LIT,
    DMODE_RLE,
};


BOOL dmIFFEncodeByteRun1Flush(
    DMResource *fp, const int mode, const BOOL flush,
    size_t *l_offs, const size_t offs, const Uint8 *buf,
    const Uint8 data, unsigned int *r_count)
{
    if (mode == DMODE_LIT)
    {
        if (offs - *l_offs > *r_count || flush)
        {
            size_t count = (offs - *l_offs) - *r_count;
            Sint8 tmp = count - 1;

            if (!dmf_write_byte(fp, tmp) ||
                !dmf_write_str(fp, buf + *l_offs, count))
                return FALSE;
        }
        (*r_count)++;
    }
    else
    {
        unsigned int count = *r_count;
        Sint8 tmp = -(count - 1);

        if (!dmf_write_byte(fp, tmp) ||
            !dmf_write_byte(fp, data))
            return FALSE;

        *r_count = 0;
        *l_offs = offs;
    }

    return TRUE;
}


BOOL dmIFFEncodeByteRun1Row(DMResource *fp, const Uint8 *buf, const size_t bufLen)
{
    size_t l_offs = 0, offs = 0;
    unsigned int r_count = 0;
    int prev = -1, mode = DMODE_LIT;

    for (offs = 0; offs < bufLen; offs++)
    {
        Uint8 data = buf[offs];
        int next_mode;

        if (data == prev)
        {
            r_count++;
            next_mode = DMODE_RLE;
        }
        else
        {
            next_mode = DMODE_LIT;
        }

        BOOL flush = offs - l_offs >= 126 || r_count >= 126;
        if ((next_mode != mode || flush) &&
            !dmIFFEncodeByteRun1Flush(fp, mode, flush, &l_offs, offs, buf, prev, &r_count))
            return FALSE;

        mode = next_mode;
        prev = data;
    }

    if (!dmIFFEncodeByteRun1Flush(fp, mode, TRUE, &l_offs, offs, buf, prev, &r_count))
        return FALSE;

    return TRUE;
}


static BOOL dmIFFWriteOneRow(DMResource *fp, DMIFF *iff, const Uint8 *buf, const size_t bufLen)
{
    if (iff->bmhd.compression == IFF_COMP_BYTERUN1)
        return dmIFFEncodeByteRun1Row(fp, buf, bufLen);
    else
        return dmf_write_str(fp, buf, bufLen);
}


static inline Uint8 dmEncodeBit(const Uint8 *buf, const int xc)
{
    return (buf[xc] >> (7 - (xc & 7))) & 1;
}


void dmEncodeBitPlane(Uint8 *dp, const Uint8 *src, const int width, const int nplane)
{
    for (int xc = 0; xc < width; xc++)
        dp[xc / 8] |= dmEncodeBit(src, xc) << nplane;
}


int dmEncodeILBMBody(DMResource *fp, DMIFF *iff, const DMImage *img)
{
    Uint8 *buf;
    size_t bufLen;
    int res = DMERR_OK;
    const int nplanes = iff->bmhd.nplanes;

    // Allocate planar encoding buffer
    bufLen = ((img->width + 15) / 16) * 2;
    if ((buf = dmMalloc(bufLen)) == NULL)
        return DMERR_MALLOC;

    dmMsg(2, "IFF: plane row size %d bytes.\n", bufLen);

    // Encode the chunk
    for (int yc = 0; yc < img->height; yc++)
    {
        const Uint8 *sp = img->data + (yc * img->pitch);

        dmMemset(buf, 0, bufLen);

        for (int plane = 0; plane < nplanes; plane++)
        {
            // Encode bitplane
            dmEncodeBitPlane(buf, sp, img->width, plane);

            // Compress / write data
            if (!dmIFFWriteOneRow(fp, iff, buf, bufLen))
            {
                res = dmError(DMERR_FWRITE,
                    "IFF: Error in writing image plane #%d @ %d.\n",
                    plane, yc);
                goto error;
            }
        }
    }

error:
    dmFree(buf);
    return res;
}


int dmEncodePBMBody(DMResource *fp, DMIFF *iff, const DMImage *img)
{
    for (int yc = 0; yc < img->height; yc++)
    {
        if (!dmIFFWriteOneRow(fp, iff, img->data + (yc * img->pitch), img->width))
        {
            return dmError(DMERR_FWRITE,
                "IFF: Error writing PBM image row #%d.\n", yc);
        }
    }

    return DMERR_OK;
}


int dmWriteIFFImage(DMResource *fp, const DMImage *img, const DMImageConvSpec *spec)
{
    Uint32 idsig;
    DMIFF iff;
    int res = DMERR_OK;

    // XXX: Non-paletted ILBM not supported!
    if (!spec->paletted)
    {
        return dmError(DMERR_NOT_SUPPORTED,
            "Non-paletted IFF is not supported.\n");
    }

    // Setup headers
    iff.bmhd.x           = 0;
    iff.bmhd.y           = 0;
    iff.bmhd.w           = img->width;
    iff.bmhd.h           = img->height;
    iff.bmhd.pagew       = img->width;
    iff.bmhd.pageh       = img->height;
    iff.bmhd.pad1        = 0;
    iff.bmhd.xasp        = 1; // XXX TODO: compute the xasp/yasp from the img->aspect
    iff.bmhd.yasp        = 1;

    iff.camg             = 0; // XXX TODO: when/if HAM support
    iff.bmhd.masking     = (img->ctransp < 0) ? IFF_MASK_NONE : spec->mask;
    iff.bmhd.compression = spec->compression ? IFF_COMP_BYTERUN1 : IFF_COMP_NONE;
    iff.bmhd.transp      = (img->ctransp >= 0 && spec->mask == IFF_MASK_TRANSP) ? img->ctransp : 0xffff;
    iff.bmhd.nplanes     = spec->nplanes;
    idsig                = spec->planar ? IFF_ID_ILBM : IFF_ID_PBM;

    dmMsg(2, "IFF: nplanes=%d, comp=%d, mask=%d\n",
        iff.bmhd.nplanes, iff.bmhd.compression, iff.bmhd.masking);

    // Write IFF FORM header
    if ((res = dmWriteIFFChunkHdr(fp, &iff.chFORM, IFF_ID_FORM)) != DMERR_OK)
        goto out;

    // Write IFF ILBM/PBM signature
    if (!dmf_write_be32(fp, idsig))
    {
        res = dmError(DMERR_FWRITE,
            "IFF: Error writing %s signature.\n",
            spec->planar ? "ILBM" : "PBM");
        goto out;
    }

    // Write BMHD chunk and data
    if ((res = dmWriteIFFChunkHdr(fp, &iff.chBMHD, IFF_ID_BMHD)) != DMERR_OK ||
        !dmf_write_be16(fp, iff.bmhd.w) ||
        !dmf_write_be16(fp, iff.bmhd.h) ||
        !dmf_write_be16(fp, iff.bmhd.x) ||
        !dmf_write_be16(fp, iff.bmhd.y) ||
        !dmf_write_byte(fp, iff.bmhd.nplanes) ||
        !dmf_write_byte(fp, iff.bmhd.masking) ||
        !dmf_write_byte(fp, iff.bmhd.compression) ||
        !dmf_write_byte(fp, iff.bmhd.pad1) ||
        !dmf_write_be16(fp, iff.bmhd.transp) ||
        !dmf_write_byte(fp, iff.bmhd.xasp) ||
        !dmf_write_byte(fp, iff.bmhd.yasp) ||
        !dmf_write_be16(fp, iff.bmhd.pagew) ||
        !dmf_write_be16(fp, iff.bmhd.pageh))
    {
        res = dmError(DMERR_FWRITE,
            "IFF: Error writing BMHD chunk.\n");
        goto out;
    }

    if ((res = dmWriteIFFChunkFinish(fp, &iff.chBMHD)) != DMERR_OK)
        goto out;

    //
    // CMAP
    //
    if (img->ncolors > 0 && spec->paletted)
    {
        if ((res = dmWriteIFFChunkHdr(fp, &iff.chCMAP, IFF_ID_CMAP)) != DMERR_OK)
            goto out;

        for (int i = 0; i < img->ncolors; i++)
        {
            DMColor *col = &img->pal[i];
            if (!dmf_write_byte(fp, col->r) ||
                !dmf_write_byte(fp, col->g) ||
                !dmf_write_byte(fp, col->b))
            {
                res = dmError(DMERR_FWRITE,
                    "IFF: Could not write CMAP palette entry #%d.\n", i);
                goto out;
            }
        }

        if ((res = dmWriteIFFChunkFinish(fp, &iff.chCMAP)) != DMERR_OK)
            goto out;

        dmMsg(2, "IFF: CMAP %d entries (%d bytes)\n",
            img->ncolors, iff.chCMAP.size);
    }

    //
    // CAMG
    //
    if ((res = dmWriteIFFChunkHdr(fp, &iff.chCMAP, IFF_ID_CAMG)) != DMERR_OK)
        goto out;

    if (!dmf_write_be32(fp, iff.camg))
    {
        return dmError(DMERR_FREAD,
            "IFF: Error writing CAMG chunk.\n");
    }

    if ((res = dmWriteIFFChunkFinish(fp, &iff.chCMAP)) != DMERR_OK)
        goto out;

    //
    // Encode the body
    //
    if ((res = dmWriteIFFChunkHdr(fp, &iff.chBODY, IFF_ID_BODY)) != DMERR_OK)
        goto out;

    if (spec->planar)
    {
        if ((res = dmEncodeILBMBody(fp, &iff, img)) != DMERR_OK)
            goto out;
    }
    else
    {
        if ((res = dmEncodePBMBody(fp, &iff, img)) != DMERR_OK)
            goto out;
    }

    if ((res = dmWriteIFFChunkFinish(fp, &iff.chBODY)) != DMERR_OK)
        goto out;

    // Finish the FORM chunk
    if ((res = dmWriteIFFChunkFinish(fp, &iff.chFORM)) != DMERR_OK)
        goto out;

out:
    return res;
}


//
// List of formats
//
const DMImageFormat dmImageFormatList[] =
{
#ifdef DM_USE_LIBPNG
    {
        "png", "Portable Network Graphics",
        DM_IMGFMT_PNG, DM_FMT_RDWR,
        fmtProbePNG, dmReadPNGImage, dmWritePNGImage,
    },
#endif
    {
        "ppm", "Portable PixMap",
        DM_IMGFMT_PPM, DM_FMT_WR,
        NULL, NULL, dmWritePPMImage,
    },
    {
        "pcx", "Z-Soft Paintbrush",
        DM_IMGFMT_PCX, DM_FMT_RDWR,
        fmtProbePCX, dmReadPCXImage, dmWritePCXImage,
    },
    {
        "iff", "IFF ILBM / PBM",
        DM_IMGFMT_IFF, DM_FMT_RDWR,
        fmtProbeIFF, dmReadIFFImage, dmWriteIFFImage,
    },
    {
        "raw", "Plain bitplaned (planar or non-planar) RAW",
        DM_IMGFMT_RAW, DM_FMT_WR,
        NULL, NULL, dmWriteRAWImage,
    },
    {
        "araw", "IFFMaster Amiga RAW",
        DM_IMGFMT_ARAW, DM_FMT_WR,
        NULL, NULL, dmWriteRAWImage,
    }
};

const int ndmImageFormatList = sizeof(dmImageFormatList) / sizeof(dmImageFormatList[0]);


int dmImageProbeGeneric(const Uint8 *buf, const size_t len, const DMImageFormat **pfmt, int *index)
{
    int scoreMax = DM_PROBE_SCORE_FALSE, scoreIndex = -1;

    for (int i = 0; i < ndmImageFormatList; i++)
    {
        const DMImageFormat *fmt = &dmImageFormatList[i];
        if (fmt->probe != NULL)
        {
            int score = fmt->probe(buf, len);
            if (score > scoreMax)
            {
                scoreMax = score;
                scoreIndex = i;
            }
        }
    }

    if (scoreIndex >= 0)
    {
        *pfmt = &dmImageFormatList[scoreIndex];
        *index = scoreIndex;
        return scoreMax;
    }
    else
        return DM_PROBE_SCORE_FALSE;
}