view tools/libgfx.c @ 2208:90ec1ec89c56

Revamp the palette handling in lib64gfx somewhat, add helper functions to lib64util for handling external palette file options and add support for specifying one of the "internal" palettes or external (.act) palette file to gfxconv and 64vw.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 14 Jun 2019 05:01:12 +0300
parents 1ea48084055e
children 7a0af15fbe97
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  = 0;

    ctx->inBuf        = 0;
    ctx->inByteCount  = 0;
    ctx->inBitCount   = 0;
}


int dmPutBits(DMBitStreamContext *ctx, const unsigned int val, const unsigned int n)
{
    for (unsigned int i = 0; i < n; i++)
    {
        ctx->outBuf <<= 1;

        if (val & (1 << (n - 1 - i)))
            ctx->outBuf |= 1;

        ctx->outBitCount++;
        if (ctx->outBitCount == 8)
        {
            int ret;
            if ((ret = ctx->putByte(ctx)) != DMERR_OK)
                return ret;

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

    return DMERR_OK;
}


int dmGetBits(DMBitStreamContext *ctx, unsigned int *val, const unsigned int n)
{
    *val = 0;

    for (unsigned int i = 0; i < n; i++)
    {
        if (ctx->inBitCount == 0)
        {
            int ret;
            if ((ret = ctx->getByte(ctx)) != DMERR_OK)
                return ret;

            ctx->inBitCount = 8;
            ctx->inByteCount++;
        }

        *val <<= 1;
        *val |= ctx->inBuf >> 7 & 1;

        ctx->inBuf <<= 1;
        ctx->inBitCount--;
    }

    return DMERR_OK;
}


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

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

  return DMERR_OK;
}


int dmGetNPlanesFromNColors(const int ncolors)
{
    if (ncolors <= 0)
        return -1;

    for (int n = 8; n >= 0;)
    {
        if ((ncolors - 1) & (1 << n))
            return n + 1;
        else
            n--;
    }

    return -2;
}


BOOL dmCompareColor(const DMColor *c1, const DMColor *c2, const 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_PIXFMT_GRAYSCALE : return 1;
        case DM_PIXFMT_PALETTE   : return 1;
        case DM_PIXFMT_RGB       : return 3;
        case DM_PIXFMT_RGBA      : return 4;
        default                  : return -1;
    }
}


int dmImageGetBitsPerPixel(const int format)
{
    return dmImageGetBytesPerPixel(format) * 8;
}


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->pixfmt  = format;
    img->bpp     = (bpp <= 0) ? dmImageGetBitsPerPixel(format) : bpp;
    img->pitch   = (width * img->bpp) / 8;
    img->size    = img->pitch * img->height;
    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)
            dmPaletteFree(img->pal);

        dmFree(img->data);
        dmFree(img);
    }
}


int dmPaletteAlloc(DMPalette **ppal, const int ncolors, const int ctransp)
{
    DMPalette *pal;
    if (ppal == NULL)
        return DMERR_NULLPTR;

    *ppal = NULL;

    // Allocate palette struct
    if ((pal = dmMalloc0(sizeof(DMPalette))) == NULL)
        return DMERR_MALLOC;

    pal->ncolors = ncolors;
    pal->ctransp = ctransp;

    // Allocate desired amount of palette
    if ((pal->colors = dmCalloc(pal->ncolors, sizeof(DMColor))) == NULL)
    {
        dmFree(pal);
        return DMERR_MALLOC;
    }

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

    *ppal = pal;

    return DMERR_OK;
}


int dmPaletteResize(DMPalette **ppal, const int ncolors)
{
    DMPalette *pal;

    if (ppal == NULL)
        return DMERR_NULLPTR;

    if (ncolors <= 0 || ncolors > 256)
        return DMERR_INVALID_ARGS;

    if (*ppal == NULL)
        return dmPaletteAlloc(ppal, ncolors, -1);

    pal = *ppal;

    if (pal->ncolors == ncolors)
        return DMERR_OK;

    if ((pal->colors = dmRealloc(pal->colors, sizeof(DMColor) * ncolors)) == NULL)
        return DMERR_MALLOC;

    if (ncolors - pal->ncolors > 0)
        memset(&(pal->colors[pal->ncolors]), 0, sizeof(DMColor) * (ncolors - pal->ncolors));

    pal->ncolors = ncolors;

    return DMERR_OK;
}


int dmPaletteCopy(DMPalette **pdst, const DMPalette *src)
{
    DMPalette *pal;
    if (pdst == NULL)
        return DMERR_NULLPTR;

    *pdst = NULL;

    // Allocate palette struct
    if ((pal = dmMalloc(sizeof(DMPalette))) == NULL)
        return DMERR_MALLOC;

    memcpy(pal, src, sizeof(DMPalette));

    // Allocate desired amount of palette
    if ((pal->colors = dmCalloc(pal->ncolors, sizeof(DMColor))) == NULL)
    {
        dmFree(pal);
        return DMERR_MALLOC;
    }

    memcpy(pal->colors, src->colors, sizeof(DMColor) * pal->ncolors);
    *pdst = pal;

    return DMERR_OK;
}


void dmPaletteFree(DMPalette *pal)
{
    if (pal != NULL)
    {
        dmFree(pal->colors);
        dmFree(pal);
    }
}


static int dmPaletteReadData(DMResource *fp, DMPalette *pal, const int ncolors)
{
    if (pal->ncolors < ncolors)
        return DMERR_INVALID_ARGS;

    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 DMERR_FREAD;

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

    return DMERR_OK;
}


static int dmPaletteWriteData(DMResource *fp, const DMPalette *pal,
    const int ncolors, const int npad)
{
    int i;

    if (pal == NULL || fp == NULL)
        return DMERR_NULLPTR;

    if (pal->ncolors < ncolors)
        return DMERR_INVALID_ARGS;

    for (i = 0; i < ncolors; i++)
    {
        DMColor *col = &pal->colors[i];
        if (!dmf_write_byte(fp, col->r) ||
            !dmf_write_byte(fp, col->g) ||
            !dmf_write_byte(fp, col->b))
            return DMERR_FWRITE;
    }

    for (; i < npad; i++)
    {
        if (!dmf_write_byte(fp, 0) ||
            !dmf_write_byte(fp, 0) ||
            !dmf_write_byte(fp, 0))
            return DMERR_FWRITE;
    }

    return DMERR_OK;
}


static int fmtProbeACT(const Uint8 *buf, const size_t len)
{
    if (len == 0x304 &&
        buf[0x300] == 0)
        return DM_PROBE_SCORE_MAX;

    return DM_PROBE_SCORE_FALSE;
}


int dmReadACTPalette(DMResource *fp, DMPalette **pdst)
{
    int res;
    Uint16 tmp1, tmp2;

    if ((res = dmPaletteAlloc(pdst, 256, -1)) != DMERR_OK)
        return res;

    if ((res = dmPaletteReadData(fp, *pdst, 256)) != DMERR_OK)
        goto error;

    if (!dmf_read_be16(fp, &tmp1) ||
        !dmf_read_be16(fp, &tmp2))
    {
        res = DMERR_FREAD;
        goto error;
    }

    if (tmp1 == 0 || tmp1 > 256)
    {
        res = DMERR_INVALID_DATA;
        goto error;
    }

    if ((res = dmPaletteResize(pdst, tmp1)) != DMERR_OK)
        goto error;

    (*pdst)->ctransp = tmp2 < 256 ? tmp2 : -1;

    return res;

error:
    dmPaletteFree(*pdst);
    *pdst = NULL;
    return res;
}


int dmWriteACTPalette(DMResource *fp, const DMPalette *ppal)
{
    int res;

    if ((res = dmPaletteWriteData(fp, ppal, ppal->ncolors, 256)) != DMERR_OK)
        return res;

    if (!dmf_write_be16(fp, ppal->ncolors) ||
        !dmf_write_be16(fp, ppal->ctransp >= 0 ? ppal->ctransp : 0xffff))
        return DMERR_FWRITE;

    return DMERR_OK;
}


int dmWriteImageData(const DMImage *img, void *cbdata,
    int (*writeRowCB)(void *, const Uint8 *, const size_t),
    const DMImageWriteSpec *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->pixfmt);

    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;

        if (img->pixfmt == DM_PIXFMT_GRAYSCALE)
        {
            for (x = 0; x < img->width; x++)
            {
                Uint8 c = img->data[(y * img->pitch) + (x * img->bpp) / 8];

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

                    case DM_PIXFMT_RGBA:
                        if (spec->planar)
                        {
                            for (xscale = 0; xscale < spec->scaleX; xscale++)
                            {
                                *ptr1++ = c;
                                *ptr2++ = c;
                                *ptr3++ = c;
                                *ptr4++ = 0xff;
                            }
                        }
                        else
                        {
                            for (xscale = 0; xscale < spec->scaleX; xscale++)
                            {
                                *ptr1++ = c;
                                *ptr1++ = c;
                                *ptr1++ = c;
                                *ptr1++ = 0xff;
                            }
                        }
                        break;

                    case DM_PIXFMT_RGB:
                        if (spec->planar)
                        {
                            for (xscale = 0; xscale < spec->scaleX; xscale++)
                            {
                                *ptr1++ = c;
                                *ptr2++ = c;
                                *ptr3++ = c;
                            }
                        }
                        else
                        {
                            for (xscale = 0; xscale < spec->scaleX; xscale++)
                            {
                                *ptr1++ = c;
                                *ptr1++ = c;
                                *ptr1++ = c;
                            }
                        }
                        break;

                    default:
                        res = DMERR_NOT_SUPPORTED;
                        goto done;
                }
            }
        }
        else
        if (img->pixfmt == DM_PIXFMT_PALETTE)
        {
            for (x = 0; x < img->width; x++)
            {
                Uint8 c = img->data[(y * img->pitch) + (x * img->bpp) / 8],
                    qr, qg, qb, qa;

                if (c >= img->pal->ncolors)
                {
                    res = DMERR_INVALID_DATA;
                    goto done;
                }

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

                    case DM_PIXFMT_RGBA:
                        qr = img->pal->colors[c].r;
                        qg = img->pal->colors[c].g;
                        qb = img->pal->colors[c].b;
                        qa = img->pal->colors[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_PIXFMT_RGB:
                        qr = img->pal->colors[c].r;
                        qg = img->pal->colors[c].g;
                        qb = img->pal->colors[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;

                    default:
                        res = DMERR_NOT_SUPPORTED;
                        goto done;
                }
            }
        }
        else
        if (img->pixfmt == DM_PIXFMT_RGB ||
            img->pixfmt == DM_PIXFMT_RGBA)
        {
            Uint8 *sp = img->data + (y * img->pitch);

            for (x = 0; x < img->width; x++, sp += img->bpp / 8)
            switch (spec->pixfmt)
            {
                 case DM_PIXFMT_RGBA:
                    if (spec->planar)
                    {
                        for (xscale = 0; xscale < spec->scaleX; xscale++)
                        {
                            *ptr1++ = sp[0];
                            *ptr2++ = sp[1];
                            *ptr3++ = sp[2];
                            *ptr4++ = sp[3];
                        }
                    }
                    else
                    {
                        for (xscale = 0; xscale < spec->scaleX; xscale++)
                        {
                            *ptr1++ = sp[0];
                            *ptr1++ = sp[1];
                            *ptr1++ = sp[2];
                            *ptr1++ = sp[3];
                        }
                    }
                    break;

                case DM_PIXFMT_RGB:
                    if (spec->planar)
                    {
                        for (xscale = 0; xscale < spec->scaleX; xscale++)
                        {
                            *ptr1++ = sp[0];
                            *ptr2++ = sp[1];
                            *ptr3++ = sp[2];
                        }
                    }
                    else
                    {
                        for (xscale = 0; xscale < spec->scaleX; xscale++)
                        {
                            *ptr1++ = sp[0];
                            *ptr1++ = sp[1];
                            *ptr1++ = sp[2];
                        }
                    }
                    break;

                default:
                    res = DMERR_NOT_SUPPORTED;
                    goto done;
            }
        }

        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 DMImageWriteSpec *spec)
{
    if (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) < 0)
        return dmferror(fp);

    if (spec->fmtid == DM_IMGFMT_ARAW &&
        img->pixfmt == DM_PIXFMT_PALETTE &&
        img->pal != NULL)
    {
        if (dmfprintf(fp,
            "%s_ncolors: dw.w %d\n"
            "%s_palette:\n",
            prefix, img->pal->ncolors,
            prefix) < 0)
            return dmferror(fp);

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

            if (dmfprintf(fp,
                "\tdc.w $%04X\n",
                color) < 0)
                return dmferror(fp);
        }

        if (dmfprintf(fp,
            "%s: incbin \"%s\"\n",
            prefix, filename) < 0)
            return dmferror(fp);
    }

    return DMERR_OK;
}


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


static int dmPutByteRes(DMBitStreamContext *ctx)
{
    DMResource *fp = (DMResource *) ctx->handle;

    if (!dmf_write_byte(fp, ctx->outBuf & 0xff))
        return dmferror(fp);
    else
        return DMERR_OK;
}


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

    dmInitBitStreamContext(&ctx);
    ctx.putByte = dmPutByteRes;
    ctx.handle  = (void *) fp;

    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 ((res = dmWriteRAWRow(&ctx, img, spec, yc, plane)) != DMERR_OK)
                return res;
        }
    }
    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 ((res = dmWriteRAWRow(&ctx, img, spec, yc, plane)) != DMERR_OK)
                return res;
        }
    }

    return dmFlushBitStream(&ctx);
}


static int dmPutByteCDump(DMBitStreamContext *ctx)
{
    DMResource *fp = (DMResource *) ctx->handle;

    if (dmfprintf(fp, "0x%02x, ", ctx->outBuf & 0xff) < 2)
        return dmferror(fp);
    else
        return DMERR_OK;
}


int dmWriteCDumpImage(DMResource *fp, const DMImage *img, const DMImageWriteSpec *spec)
{
    int res;
    DMBitStreamContext ctx;

    if (dmfprintf(fp,
        "#define SET_WIDTH %d\n"
        "#define SET_HEIGHT %d\n"
        "\n"
        "Uint8 img_data[] = {\n",
        img->width * spec->scaleX,
        img->height * spec->scaleY) < 0)
        return dmferror(fp);

    dmInitBitStreamContext(&ctx);
    ctx.putByte = dmPutByteCDump;
    ctx.handle = (void *) fp;

    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 ((res = dmWriteRAWRow(&ctx, img, spec, yc, plane)) != DMERR_OK)
                return res;

            if (dmfprintf(fp, "\n") < 0)
                return dmferror(fp);
        }
    }
    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 ((res = dmWriteRAWRow(&ctx, img, spec, yc, plane)) != DMERR_OK)
                return res;

            if (dmfprintf(fp, "\n") < 0)
                return dmferror(fp);
        }
    }

    res = dmFlushBitStream(&ctx);

    if (res == DMERR_OK &&
        dmfprintf(fp, "};\n") < 0)
        res = dmferror(fp);

    return res;
}


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 DMImageWriteSpec *spec)
{
    DMImageWriteSpec tmpSpec;
    char *tmpFmt;

    // Check if we can do this
    if ((spec->pixfmt == DM_PIXFMT_PALETTE || spec->pixfmt == DM_PIXFMT_GRAYSCALE) &&
        img->pixfmt != spec->pixfmt)
    {
        return dmError(DMERR_NOT_SUPPORTED,
            "Conversion of non-indexed image to indexed not supported yet.\n");
    }

    memcpy(&tmpSpec, spec, sizeof(DMImageWriteSpec));

    switch (spec->pixfmt)
    {
        case DM_PIXFMT_RGB:
        case DM_PIXFMT_RGBA:
        case DM_PIXFMT_PALETTE:
            tmpSpec.pixfmt = DM_PIXFMT_RGB;
            tmpFmt = "6";
            break;

        case DM_PIXFMT_GRAYSCALE:
            tmpFmt = "5";
            break;

        default:
            return dmError(DMERR_NOT_SUPPORTED,
                "PPM: Not a supported color format for PPM/PGM format image.\n");
    }

    // Write PPM header
    char *tmp = dm_strdup_printf(
        "P%s\n%d %d\n255\n",
        tmpFmt,
        img->width * tmpSpec.scaleX,
        img->height * tmpSpec.scaleY);

    if (tmp == NULL)
        return DMERR_MALLOC;

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

    // Write image data
    return dmWriteImageData(img, (void *) fp, dmWritePPMRow, &tmpSpec);
}


// Read a PPM/PGM/PNM header line, skipping comments
static BOOL dmReadPPMHeader(DMResource *fp, char *buf, const size_t bufLen)
{
    BOOL end = FALSE, comment = FALSE;
    size_t offs = 0;

    do
    {
        int ch = dmfgetc(fp);
        if (ch == EOF)
            return FALSE;
        else
        if (ch == '#')
            comment = TRUE;
        else
        if (ch == '\n')
        {
            if (!comment)
                end = TRUE;
            else
                comment = FALSE;
        }
        else
        if (!comment)
        {
            if (offs < bufLen - 1)
                buf[offs++] = ch;
        }
    } while (!end);

    buf[offs] = 0;
    return TRUE;
}


int dmReadPPMImage(DMResource *fp, DMImage **pimg)
{
    DMImage *img = NULL;
    unsigned int width, height;
    int itype, res = DMERR_OK;
    char hdr1[8], hdr2[32], hdr3[16];

    // Read PPM header
    if (!dmReadPPMHeader(fp, hdr1, sizeof(hdr1)) ||
        !dmReadPPMHeader(fp, hdr2, sizeof(hdr2)) ||
        !dmReadPPMHeader(fp, hdr3, sizeof(hdr3)))
    {
        res = dmError(DMERR_FREAD,
            "PPM: Could not read image header data.\n");
        goto error;
    }

    if (hdr1[0] != 'P' || !isdigit(hdr1[1]) ||
        !isdigit(hdr2[0]) || !isdigit(hdr3[0]))
    {
        res = dmError(DMERR_NOT_SUPPORTED,
            "PPM: Not a supported PPM/PGM format image.\n");
        goto error;
    }

    switch (hdr1[1])
    {
        case '6': itype = DM_PIXFMT_RGB; break;
        case '5': itype = DM_PIXFMT_GRAYSCALE; break;
        default:
            res = dmError(DMERR_NOT_SUPPORTED,
                "PPM: Unsupported PPM/PNM/PGM subtype.\n");
            goto error;
    }

    if (sscanf(hdr2, "%u %u", &width, &height) != 2)
    {
        res = dmError(DMERR_INVALID_DATA,
            "PPM: Invalid PPM/PGM image dimensions.\n");
        goto error;
    }

    dmMsg(2, "PPM: %d x %d, type=%d\n",
        width, height, itype);

    if ((*pimg = img = dmImageAlloc(width, height, itype, -1)) == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "PPM: Could not allocate image data.\n");
        goto error;
    }

    if (!dmf_read_str(fp, img->data, img->size))
    {
        res = dmError(DMERR_FREAD,
            "PPM: Could not read image data.\n");
        goto error;
    }

error:

    return res;
}


static int fmtProbePPM(const Uint8 *buf, const size_t len)
{
    if (len > 32 &&
        buf[0] == 'P' &&
        (buf[1] == '5' || buf[1] == '6'))
        return DM_PROBE_SCORE_MAX;

    return DM_PROBE_SCORE_FALSE;
}


#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 DMImageWriteSpec *spec)
{
    png_structp png_ptr = NULL;
    png_infop info_ptr = NULL;
    int fmt, res;

    // Check if we can do this
    if ((spec->pixfmt == DM_PIXFMT_PALETTE || spec->pixfmt == DM_PIXFMT_GRAYSCALE) &&
        img->pixfmt != spec->pixfmt)
    {
        return dmError(DMERR_NOT_SUPPORTED,
            "Conversion of non-indexed image to indexed not supported yet.\n");
    }

    // 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->pixfmt)
    {
        case DM_PIXFMT_PALETTE   : fmt = PNG_COLOR_TYPE_PALETTE; break;
        case DM_PIXFMT_GRAYSCALE : fmt = PNG_COLOR_TYPE_GRAY; break;
        case DM_PIXFMT_RGB       : fmt = PNG_COLOR_TYPE_RGB; break;
        case DM_PIXFMT_RGBA      : fmt = PNG_COLOR_TYPE_RGB_ALPHA; break;
        default:
            res = dmError(DMERR_NOT_SUPPORTED,
                "PNG: Unsupported image format %d.\n",
                spec->pixfmt);
            goto error;
    }

    png_set_compression_level(png_ptr, spec->compression);

    png_set_IHDR(png_ptr, info_ptr,
        img->width * spec->scaleX,
        img->height * spec->scaleY,
        8, // bits per component: TODO: FIXME: calculate and use
        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->pixfmt == DM_PIXFMT_PALETTE)
    {
        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 (int i = 0; i < img->pal->ncolors; i++)
        {
            palette[i].red   = img->pal->colors[i].r;
            palette[i].green = img->pal->colors[i].g;
            palette[i].blue  = img->pal->colors[i].b;
        }

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

//    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 = 0, res_y = 0;
    int res, itype, bit_depth, color_type, ncolors, ntrans, unit_type;
    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)
            {
                res = dmError(DMERR_NOT_SUPPORTED,
                    "PNG: Unsupported bit depth for grayscale image: %d\n",
                    bit_depth);
                goto error;
            }

            if (bit_depth < 8)
                png_set_expand_gray_1_2_4_to_8(png_ptr);

            itype = DM_PIXFMT_GRAYSCALE;
            break;

        case PNG_COLOR_TYPE_PALETTE:
            png_set_packing(png_ptr);
            itype = DM_PIXFMT_PALETTE;
            break;

        case PNG_COLOR_TYPE_RGB:
            itype = DM_PIXFMT_RGB;
            break;

        case PNG_COLOR_TYPE_RGBA:
            itype = DM_PIXFMT_RGBA;
            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,
        itype,
        // 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;

    // ...
    if ((row_pointers = png_malloc(png_ptr, height * sizeof(png_bytep))) == NULL)
        goto error;

    for (int 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);
    png_free(png_ptr, row_pointers);

    // Create palette
    if (color_type == PNG_COLOR_TYPE_PALETTE)
    {
        png_get_PLTE(png_ptr, info_ptr, &palette, &ncolors);
        if (ncolors > 0 && palette != NULL)
        {
            dmMsg(2, "PNG: Palette of %d colors found.\n", ncolors);

            if ((res = dmPaletteAlloc(&(img->pal), ncolors, -1)) != DMERR_OK)
                goto error;

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

        png_get_tRNS(png_ptr, info_ptr, &trans, &ntrans, NULL);
        if (trans != NULL && ntrans > 0)
        {
            dmMsg(2, "PNG: %d transparent colors.\n", ntrans);
            for (int i = 0; i < img->pal->ncolors && i < ntrans; i++)
            {
                img->pal->colors[i].a = trans[i];
                if (img->pal->ctransp < 0 && trans[i] == 0)
                    img->pal->ctransp = i;
            }
        }
    }

    res = DMERR_OK;

error:
    if (png_ptr != NULL && info_ptr != NULL)
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);

    return res;
}
#endif


//
// Z-Soft PCX format
//
#define PCX_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[PCX_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 DMImageWriteSpec *pspec)
{
    DMPCXData pcx;
    DMPCXHeader hdr;
    DMImageWriteSpec spec;
    int res;

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

    // XXX: 24bit PCX does not work yet ..
    if ((img->pixfmt != DM_PIXFMT_PALETTE && img->pixfmt != DM_PIXFMT_GRAYSCALE) ||
        (spec.pixfmt != DM_PIXFMT_PALETTE && spec.pixfmt != DM_PIXFMT_GRAYSCALE))
    {
        return dmError(DMERR_NOT_SUPPORTED,
            "24bit PCX not supported yet.\n");
    }

    if (spec.pixfmt == DM_PIXFMT_PALETTE && 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.pixfmt == DM_PIXFMT_PALETTE ||
        spec.pixfmt == DM_PIXFMT_GRAYSCALE)
    {
        const int ncolors = img->pal->ncolors > PCX_PAL_COLORS ? PCX_PAL_COLORS : img->pal->ncolors;
        for (int i = 0; i < ncolors; i++)
        {
            hdr.colorMap[i].r = img->pal->colors[i].r;
            hdr.colorMap[i].g = img->pal->colors[i].g;
            hdr.colorMap[i].b = img->pal->colors[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.pixfmt);

    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, colfmt=%d, planar=%s\n",
        hdr.nplanes, hdr.bitsPerPlane, hdr.bpl,
        spec.pixfmt,
        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.pixfmt == DM_PIXFMT_PALETTE ||
        spec.pixfmt == DM_PIXFMT_GRAYSCALE)
    {
        dmMsg(2, "PCX: Writing palette of %d active entries.\n", img->pal->ncolors);

        dmf_write_byte(pcx.fp, 0x0C);

        if ((res = dmPaletteWriteData(fp, img->pal, img->pal->ncolors, 256)) != DMERR_OK)
        {
            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=%d and bpp=%d, attempting to fix ..\n",
            hdr.nplanes, hdr.bitsPerPlane);

        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_PIXFMT_PALETTE : DM_PIXFMT_RGB,
        // 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 24:
            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 = PCX_PAL_COLORS;
        }
        else
        {
            read = TRUE;
            ncolors = 256;
        }

        if ((res = dmPaletteAlloc(&(img->pal), ncolors, -1)) != DMERR_OK)
        {
            res = dmError(res,
                "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 ((res = dmPaletteReadData(fp, img->pal, ncolors)) != DMERR_OK)
            {
                dmErrorMsg("PCX: Error reading palette.\n");
                goto error;
            }
        }
        else
        {
            const int nmax = img->pal->ncolors > PCX_PAL_COLORS ? PCX_PAL_COLORS : img->pal->ncolors;

            // 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 (%d)\n", ncolors, nmax);

            for (int i = 0; i < nmax; i++)
            {
                img->pal->colors[i].r = hdr.colorMap[i].r;
                img->pal->colors[i].g = hdr.colorMap[i].g;
                img->pal->colors[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_ID_ACBM        0x4143424D // "ACBM"
#define IFF_ID_ABIT        0x41424954 // "ABIT"

#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;
    DMPalette *pal;
    Uint32 idsig;
    char *idstr;
} DMIFF;


static int fmtProbeIFF(const Uint8 *buf, const size_t len, const Uint32 id)
{
    if (len > 32 &&
        DM_BE32_TO_NATIVE(*(Uint32 *) (buf + 0)) == IFF_ID_FORM &&
        DM_BE32_TO_NATIVE(*(Uint32 *) (buf + 8)) == id)
    {
        if (DM_BE32_TO_NATIVE(*(Uint32 *) (buf + 12)) == IFF_ID_BMHD)
            return DM_PROBE_SCORE_MAX;
        else
            return DM_PROBE_SCORE_GOOD;
    }

    return DM_PROBE_SCORE_FALSE;
}


static int fmtProbeIFF_ILBM(const Uint8 *buf, const size_t len)
{
    return fmtProbeIFF(buf, len, IFF_ID_ILBM);
}


static int fmtProbeIFF_PBM(const Uint8 *buf, const size_t len)
{
    return fmtProbeIFF(buf, len, IFF_ID_PBM);
}


static int fmtProbeIFF_ACBM(const Uint8 *buf, const size_t len)
{
    return fmtProbeIFF(buf, len, IFF_ID_ACBM);
}


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,
          size = chunk->size;

    if (size & 1)
    {
        dmMsg(3, "IFF: Chunk size %d is uneven, adjusting to %d.\n",
            size, size + 1);
        size++;
    }

    if (size > read)
    {
        dmMsg(3, "IFF: Skipping %d bytes (%d of %d consumed)\n",
            size - read, read, size);

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

    return DMERR_OK;
}


static int dmCheckIFFChunk(DMIFFChunk *dest, const 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 %s is too small (%d < %d).\n",
            chunk->idStr, chunk->size, minSize);
    }

    return DMERR_OK;
}


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

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

        if (ucount == 0x80)
        {
            if (!dmf_read_byte(fp, &data))
                return FALSE;
        }
        else
        if (ucount & 0x80)
        {
            Uint8 count = (ucount ^ 0xff) + 2;
            if (!dmf_read_byte(fp, &data))
                return FALSE;

            while (count-- && offs < bufLen)
                buf[offs++] = data;
        }
        else
        {
            Uint8 count = ucount + 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 int dmDecodeIFFBody(DMResource *fp, DMIFF *iff, DMImage *img)
{
    Uint8 *buf = NULL;
    size_t bufLen = 0;
    int res = DMERR_OK;
    const int nplanes = iff->bmhd.nplanes;

    if (iff->idsig == IFF_ID_ILBM)
    {
        bufLen = ((img->width + 15) / 16) * 2;
        dmMsg(2, "IFF: Line / plane row size %d bytes.\n", bufLen);

    }
    else
    if (iff->idsig == IFF_ID_ACBM)
    {
        bufLen = (img->width * img->height + 7) / 8;
        dmMsg(2, "IFF: Plane buffer size %d bytes.\n", bufLen);
    }

    if (bufLen > 0 && (buf = dmMalloc(bufLen)) == NULL)
        return DMERR_MALLOC;

    // Decode the chunk
    if (iff->idsig == IFF_ID_ACBM)
    {
        // Initialize destination image data
        dmMemset(img->data, 0, img->size);

        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 ACBM image plane #%d.\n",
                    plane);
                goto out;
            }

            for (int yc = 0; yc < img->height; yc++)
            {
                Uint8 *dp = img->data + (yc * img->pitch);
                Uint8 *sp = buf + (yc * img->width) / 8;
                for (int xc = 0; xc < img->width; xc++)
                {
                    dp[xc] |= dmDecodeBit(sp, xc) << plane;
                }
            }
        }
    }
    else
    for (int yc = 0; yc < img->height; yc++)
    {
        Uint8 *dp = img->data + (yc * img->pitch);

        if (iff->idsig == IFF_ID_ILBM)
        {
            // Clear planar decoding buffer
            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 ILBM image plane #%d @ %d.\n",
                        plane, yc);
                    goto out;
                }

                // Decode bitplane
                for (int xc = 0; xc < img->width; xc++)
                    dp[xc] |= dmDecodeBit(buf, xc) << 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 ILBM mask plane.\n");
                    goto out;
                }

                // 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->pal->ctransp < 0 ? 0 : img->pal->ctransp;
                }
            }
        }
        else
        if (iff->idsig == IFF_ID_PBM)
        {
            if (!dmIFFReadOneRow(fp, iff, dp, img->width))
            {
                res = dmError(DMERR_FREAD,
                    "IFF: Error reading PBM image row #%d.\n", yc);
                goto out;
            }
        }
    }

out:
    dmFree(buf);
    return res;
}


int dmReadIFFImage(DMResource *fp, DMImage **pimg)
{
    DMIFFChunk chunk;
    DMIFF iff;
    BOOL parsed = FALSE;
    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, &iff.idsig) ||
        (iff.idsig != IFF_ID_ILBM &&
         iff.idsig != IFF_ID_PBM &&
         iff.idsig != IFF_ID_ACBM))
    {
        return dmError(DMERR_INVALID_DATA,
            "IFF: Not a IFF ILBM/PBM/ACBM file.\n");
    }

    dmMsg(3, "IFF: FORM is %s format image, with size %d bytes.\n",
        iff.idsig == IFF_ID_ILBM ? "ILBM" :
        (iff.idsig == IFF_ID_PBM ? "PBM" : "ACBM"),
        chunk.size);

    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");
                }

                if (iff.idsig == IFF_ID_ACBM &&
                    iff.bmhd.compression != IFF_COMP_NONE)
                {
                    dmMsg(1, "IFF: ACBM image with compression != none (%d). Ignoring.\n",
                        iff.bmhd.compression);

                    iff.bmhd.compression = IFF_COMP_NONE;
                }
                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 ((res = dmPaletteAlloc(&iff.pal, iff.ncolors,
                        (iff.bmhd.masking == IFF_MASK_TRANSP) ? iff.bmhd.transp : -1)) != DMERR_OK)
                    {
                        dmErrorMsg("IFF: Could not allocate palette data.\n");
                        return res;
                    }
                    if ((res = dmPaletteReadData(fp, iff.pal, iff.ncolors)) != DMERR_OK)
                    {
                        dmErrorMsg("IFF: Error reading CMAP.\n");
                        return res;
                    }
                }

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

            case IFF_ID_BODY:
            case IFF_ID_ABIT:
                // 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: %s chunk before BMHD?\n", chunk.idStr);
                }

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

                // Allocate image
                if ((*pimg = dmImageAlloc(iff.bmhd.w, iff.bmhd.h,
                    iff.bmhd.nplanes <= 8 ? DM_PIXFMT_PALETTE : DM_PIXFMT_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 ((res = dmDecodeIFFBody(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(3, "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;
    }

    // Check if we should have a palette
    if (*pimg != NULL && (*pimg)->pixfmt == DM_PIXFMT_PALETTE)
    {
        // Check that we DO have a palette ..
        if (iff.pal == NULL || iff.pal->ncolors == 0)
        {
            return dmError(DMERR_INVALID_DATA,
                "IFF: A paletted/indexed color image with no CMAP. Bailing out.\n");
        }

        // If halfbrite is used, duplicate the palette
        if (iff.camg & IFF_CAMG_HALFBRITE)
        {
            int ncolors = iff.ncolors;
            if (iff.idsig != IFF_ID_ILBM)
            {
                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 ((res = dmPaletteResize(&iff.pal, ncolors * 2)) != DMERR_OK)
            {
                dmPaletteFree(iff.pal);
                return res;
            }

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

        (*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, 0))
    {
        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)
    {
        dmMsg(3, "Padding chunk ID '%s', size %d ++\n",
            chunk->idStr, chunk->size);

        if (!dmf_write_byte(fp, 0))
            return dmferror(fp);

        curr++;
    }

    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,
};


static BOOL dmIFFEncodeByteRun1LIT(DMResource *fp,
    const Uint8 *buf, const size_t offs,
    const size_t count)
{
    if (count <= 0)
        return TRUE;

    Uint8 tmp = count - 1;

    return
        dmf_write_byte(fp, tmp) &&
        dmf_write_str(fp, buf + offs, count);
}


static BOOL dmIFFEncodeByteRun1RLE(DMResource *fp,
    const Uint8 *buf, const size_t offs,
    const size_t count)
{
    if (count <= 0)
        return TRUE;

    Uint8
        tmp = ((Uint8) count - 2) ^ 0xff,
        data = buf[offs];

    return
        dmf_write_byte(fp, tmp) &&
        dmf_write_byte(fp, data);
}


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

    for (offs = l_offs = r_offs = 0; offs < bufLen; offs++)
    {
        Uint8 data = buf[offs];
        BOOL flush = FALSE;
        int pmode = mode;

        if (data == prev)
        {
            if (mode == DMODE_LIT &&
                offs - r_offs >= 2)
            {
                ret = dmIFFEncodeByteRun1LIT(fp, buf, l_offs, r_offs - l_offs);
                mode = DMODE_RLE;
            }
        }
        else
        {
            if (mode != DMODE_LIT)
            {
                ret = dmIFFEncodeByteRun1RLE(fp, buf, r_offs, offs - r_offs);
                mode = DMODE_LIT;
                l_offs = offs;
            }
            r_offs = offs;
        }

        if (!ret)
            goto out;

        // NOTE! RLE and LIT max are both 128, checked against DP2e
        flush =
            (pmode == DMODE_RLE && offs - r_offs >= 128) ||
            (pmode == DMODE_LIT && offs - l_offs >= 128);

        // Check for last byte of input
        if (offs == bufLen - 1)
        {
            offs++;
            flush = TRUE;
            pmode = mode;
        }

        if (flush)
        {
            if (pmode == DMODE_RLE)
                ret = dmIFFEncodeByteRun1RLE(fp, buf, r_offs, offs - r_offs);
            else
                ret = dmIFFEncodeByteRun1LIT(fp, buf, l_offs, offs - l_offs);

            r_offs = l_offs = offs;
            mode = DMODE_LIT;

            if (!ret)
                goto out;
        }

        prev = data;
    }

out:
    return ret;
}


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);
}


int dmWriteIFFImage(DMResource *fp, const DMImage *img, const DMImageWriteSpec *spec)
{
    DMIFF iff;
    Uint8 *buf = NULL;
    size_t bufLen;
    int res = DMERR_OK;

    // XXX: Non-paletted IFF not supported!
    if ((img->pixfmt != DM_PIXFMT_PALETTE && img->pixfmt != DM_PIXFMT_GRAYSCALE) ||
        (spec->pixfmt != DM_PIXFMT_PALETTE && spec->pixfmt != DM_PIXFMT_GRAYSCALE))
    {
        return dmError(DMERR_NOT_SUPPORTED,
            "Non-paletted IFF is not supported.\n");
    }

    switch (spec->fmtid)
    {
        case DM_IMGFMT_IFF_ILBM: iff.idsig = IFF_ID_ILBM; iff.idstr = "ILBM"; break;
        case DM_IMGFMT_IFF_PBM : iff.idsig = IFF_ID_PBM; iff.idstr = "PBM"; break;
        case DM_IMGFMT_IFF_ACBM: iff.idsig = IFF_ID_ACBM; iff.idstr = "ACBM"; break;
        default:
            return dmError(DMERR_NOT_SUPPORTED,
                "Invalid IFF format.\n");
    }

    // Setup headers
    iff.bmhd.x           = 0;
    iff.bmhd.y           = 0;
    iff.bmhd.w           = img->width * spec->scaleX;
    iff.bmhd.h           = img->height * spec->scaleY;
    iff.bmhd.pagew       = img->width * spec->scaleX;
    iff.bmhd.pageh       = img->height * spec->scaleY;
    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->pal && img->pal->ctransp < 0) ? IFF_MASK_NONE : spec->mask;
    iff.bmhd.transp      = (img->pal && img->pal->ctransp >= 0 && spec->mask == IFF_MASK_TRANSP) ? img->pal->ctransp : 0xffff;
    iff.bmhd.nplanes     = (iff.idsig == IFF_ID_PBM && spec->nplanes < 8) ? 8 : spec->nplanes;

    // Apparently ACBM can't/should not use compression .. even though
    // some files in the wild have bmhd.compression != 0 (but are not
    // actually compressed.) To be more compliant with the spec,
    iff.bmhd.compression = (spec->compression && iff.idsig != IFF_ID_ACBM) ? IFF_COMP_BYTERUN1 : IFF_COMP_NONE;

    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, iff.idsig))
    {
        res = dmError(DMERR_FWRITE,
            "IFF: Error writing %s signature.\n",
            iff.idstr);
        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 (spec->pixfmt == DM_PIXFMT_PALETTE &&
        img->pal != NULL &&
        img->pal->ncolors > 0)
    {
        if ((res = dmWriteIFFChunkHdr(fp, &iff.chCMAP, IFF_ID_CMAP)) != DMERR_OK)
            goto out;

        if ((res = dmPaletteWriteData(fp, img->pal, img->pal->ncolors, -1)) != DMERR_OK)
        {
            res = dmError(DMERR_FWRITE,
                "IFF: Could not write CMAP palette.\n");
            goto out;
        }

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

        dmMsg(2, "IFF: CMAP %d entries (%d bytes)\n",
            img->pal->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.idsig == IFF_ID_ACBM) ? IFF_ID_ABIT : IFF_ID_BODY)) != DMERR_OK)
        goto out;

    // Allocate encoding buffer
    if (iff.idsig == IFF_ID_ILBM)
        bufLen = ((img->width * spec->scaleX + 15) / 16) * 2;
    else
    if (iff.idsig == IFF_ID_ACBM)
        bufLen = (img->width * spec->scaleX * img->height * spec->scaleY + 7) / 8;
    else
        bufLen = img->width * spec->scaleX;

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

    if ((buf = dmMalloc(bufLen)) == NULL)
        return DMERR_MALLOC;

    // Encode the body
    if (iff.idsig == IFF_ID_ACBM)
    {
        for (int plane = 0; plane < iff.bmhd.nplanes; plane++)
        {
            // Encode bitplane
            dmMemset(buf, 0, bufLen);

            for (int yc = 0; yc < img->height * spec->scaleY; yc++)
            {
                Uint8 *sp = img->data + (yc * img->pitch);
                Uint8 *dp = buf + (yc * img->width * spec->scaleX) / 8;
                for (int xc = 0; xc < img->width * spec->scaleX; xc++)
                    dp[xc / 8] |= ((sp[xc / spec->scaleX] >> plane) & 1) << (7 - (xc & 7));
            }

            if (!dmIFFWriteOneRow(fp, &iff, buf, bufLen))
            {
                res = dmError(DMERR_FWRITE,
                    "IFF: Error writing ACBM image plane %d.\n",
                    plane);
                goto out;
            }
        }
    }
    else
    {
        for (int yc = 0; yc < img->height; yc++)
        for (int yscale = 0; yscale < spec->scaleY; yscale++)
        {
            const Uint8 *sp = img->data + (yc * img->pitch);

            if (iff.idsig == IFF_ID_ILBM)
            {
                for (int plane = 0; plane < spec->nplanes; plane++)
                {
                    // Encode bitplane
                    dmMemset(buf, 0, bufLen);

                    for (int xc = 0; xc < img->width * spec->scaleX; xc++)
                        buf[xc / 8] |= ((sp[xc / spec->scaleX] >> plane) & 1) << (7 - (xc & 7));

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

                // Write mask data, if any
                if (iff.bmhd.masking == IFF_MASK_HAS_MASK)
                {
                    dmMemset(buf, 0, bufLen);

                    for (int xc = 0; xc < img->width * spec->scaleX; xc++)
                        buf[xc / 8] |= (sp[xc / spec->scaleX] == img->pal->ctransp) << (7 - (xc & 7));

                    if (!dmIFFWriteOneRow(fp, &iff, buf, bufLen))
                    {
                        res = dmError(DMERR_FWRITE,
                            "IFF: Error writing ILBM mask plane %d.\n", yc);
                        goto out;
                    }
                }
            }
            else
            {
                for (int xc = 0; xc < img->width * spec->scaleX; xc++)
                    buf[xc] = sp[xc / spec->scaleX];

                if (!dmIFFWriteOneRow(fp, &iff, buf, bufLen))
                {
                    res = dmError(DMERR_FWRITE,
                        "IFF: Error writing PBM image row #%d.\n", yc);
                    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:
    dmFree(buf);
    return res;
}


//
// List of formats
//
const DMImageFormat dmImageFormatList[] =
{
#ifdef DM_USE_LIBPNG
    {
        "png", "Portable Network Graphics",
        DM_IMGFMT_PNG, DM_FMT_RDWR | DM_PIXFMT_ANY,
        fmtProbePNG, dmReadPNGImage, dmWritePNGImage,
    },
#endif
    {
        "ppm", "Portable PixMap",
        DM_IMGFMT_PPM, DM_FMT_RDWR | DM_PIXFMT_GRAYSCALE | DM_PIXFMT_RGB,
        fmtProbePPM, dmReadPPMImage, dmWritePPMImage,
    },
    {
        "pcx", "Z-Soft Paintbrush",
        DM_IMGFMT_PCX, DM_FMT_RDWR | DM_PIXFMT_PALETTE | DM_PIXFMT_RGB,
        fmtProbePCX, dmReadPCXImage, dmWritePCXImage,
    },
    {
        "ilbm", "IFF ILBM (interleaved/old DP2)",
        DM_IMGFMT_IFF_ILBM, DM_FMT_RDWR | DM_PIXFMT_PALETTE,
        fmtProbeIFF_ILBM, dmReadIFFImage, dmWriteIFFImage,
    },
    {
        "pbm", "IFF PBM (DP2e)",
        DM_IMGFMT_IFF_PBM, DM_FMT_RDWR | DM_PIXFMT_PALETTE,
        fmtProbeIFF_PBM, dmReadIFFImage, dmWriteIFFImage,
    },
    {
        "acbm", "IFF ACBM (Amiga Basic)",
        DM_IMGFMT_IFF_ACBM, DM_FMT_RDWR | DM_PIXFMT_PALETTE,
        fmtProbeIFF_ACBM, dmReadIFFImage, dmWriteIFFImage,
    },
    {
        "raw", "Plain bitplaned (planar or non-planar) RAW",
        DM_IMGFMT_RAW, DM_FMT_WR | DM_PIXFMT_PALETTE,
        NULL, NULL, dmWriteRAWImage,
    },
    {
        "araw", "IFFMaster Amiga RAW",
        DM_IMGFMT_ARAW, DM_FMT_WR | DM_PIXFMT_PALETTE,
        NULL, NULL, dmWriteRAWImage,
    },
    {
        "cdump", "'C' dump (image data only)",
        DM_IMGFMT_CDUMP, DM_FMT_WR | DM_PIXFMT_ANY,
        NULL, NULL, dmWriteCDumpImage,
    }
};

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;
}


//
// List of formats
//
const DMPaletteFormat dmPaletteFormatList[] =
{
    {
        "act", "ACT Palette",
        DM_PALFMT_ACT, DM_FMT_RDWR,
        fmtProbeACT, dmReadACTPalette, dmWriteACTPalette,
    },
};

const int ndmPaletteFormatList = sizeof(dmPaletteFormatList) / sizeof(dmPaletteFormatList[0]);


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

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

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