view tools/libgfx.c @ 2310:75216bf67fd2

Bump copyright.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 09 Jul 2019 10:27:40 +0300
parents c7495fcaffa9
children 659c5f3d2e12
line wrap: on
line source

/*
 * Functions for reading and converting various graphics file formats
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2012-2019 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 fmtProbeACTPalette(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;
}


static int fmtProbeRAWPalette(const Uint8 *buf, const size_t len)
{
    (void) buf;

    if (len == 0x300)
        return DM_PROBE_SCORE_MAX;

    return DM_PROBE_SCORE_FALSE;
}


int dmReadRAWPalette(DMResource *fp, DMPalette **pdst)
{
    int res;

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

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

    return res;

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


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

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

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

    // Force non-planar etc
    memcpy(&tmpSpec, spec, sizeof(DMImageWriteSpec));
    tmpSpec.planar = FALSE;

    switch (tmpSpec.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);

    // Check image dimensions
    if (width == 0 || height == 0)
    {
        return dmError(DMERR_INVALID_DATA,
            "PPM: Invalid image dimensions.\n");
    }

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

    dmMsg(2, "PNG: %d x %d, bit_depth=%d, color_type=%d\n",
        width, height, bit_depth, color_type);

    if (width < 1 || height < 1)
    {
        res = dmError(DMERR_INVALID_DATA,
            "PNG: Invalid image dimensions.\n");
        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.hScreenSize  = hdr.hres;
    hdr.vScreenSize  = hdr.vres;

    switch (spec.pixfmt)
    {
        case DM_PIXFMT_GRAYSCALE: hdr.palInfo = 2; break;
        default: hdr.palInfo = 1; break;
    }

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

    // Compute bytes per line
    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) ||
        !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) ||
        !dmf_write_str(pcx.fp, (Uint8 *) &hdr.colorMap, sizeof(hdr.colorMap)) ||
        !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 data.\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");

    // Check image dimensions
    if (hdr.xmin > hdr.xmax || hdr.ymin > hdr.ymax)
    {
        res = dmError(DMERR_INVALID_DATA,
            "PCX: Invalid image dimensions.\n");
        goto error;
    }

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

                // Check image dimensions
                if (iff.bmhd.w == 0 || iff.bmhd.h == 0)
                {
                    return dmError(DMERR_INVALID_DATA,
                        "IFF: Invalid image dimensions.\n");
                }

                // 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", "Adobe Color Table palette",
        DM_PALFMT_ACT, DM_FMT_RDWR,
        fmtProbeACTPalette, dmReadACTPalette, dmWriteACTPalette,
    },
    {
        "rpl", "RAW binary palette (RGB, 768 bytes)",
        DM_PALFMT_RAW, DM_FMT_RDWR,
        fmtProbeRAWPalette, dmReadRAWPalette, dmWriteRAWPalette,
    },
};

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