view tools/lib64gfx.c @ 1542:69fa95707e65

Implement dmGenericRLEAnalyze() and use it where appropriate.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 12 May 2018 05:31:40 +0300
parents 776aa43b2c57
children 20cd589366d7
line wrap: on
line source

/*
 * Functions for reading and converting various restricted
 * C64/etc and/or indexed/paletted graphics 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 "lib64gfx.h"

#define BUF_SIZE_INITIAL   (16*1024)
#define BUF_SIZE_GROW      (4*1024)


void dmC64ImageDump(FILE *fh, const DMC64Image *img, const DMC64ImageFormat *fmt)
{
    char typeStr[64];

    dmC64GetImageTypeString(typeStr, sizeof(typeStr), img->type, TRUE);

    if (fmt != NULL)
    {
        fprintf(fh,
            "Format              : %s [%s]\n",
            fmt->name, fmt->fext);
    }

    fprintf(fh,
        "Type                : %s\n"
        "Banks               : %d\n",
        typeStr,
        img->nbanks);

    if (img->type & D64_FMT_ILACE)
    {
        char *tmps;
        switch(img->laceType)
        {
            case D64_ILACE_COLOR: tmps = "color"; break;
            case D64_ILACE_RES: tmps = "resolution"; break;
            default: tmps = "ERROR"; break;
        }
        fprintf(fh,
            "Interlace type      : %s\n",
            tmps);
    }

    fprintf(fh,
        "Width x Height      : %d x %d\n"
        "CHwidth x CHheight  : %d x %d\n",
        img->width, img->height,
        img->chWidth, img->chHeight);
}


void dmSetDefaultC64Palette(DMImage *img)
{
    img->constpal = TRUE;
    img->pal      = dmDefaultC64Palette;
    img->ncolors  = C64_NCOLORS;
    img->ctransp  = 255;
}


char * dmC64GetImageTypeString(char *buf, const size_t len, const int type, const BOOL lng)
{
    snprintf(buf, len,
        "%s%s%s%s",
        (type & D64_FMT_MC)    ? (lng ? "MultiColor " : "MC ") : "HiRes ",
        (type & D64_FMT_ILACE) ? (lng ? "Interlaced " : "ILace ") : "",
        (type & D64_FMT_FLI)   ? "FLI " : "",
        (type & D64_FMT_CHAR)  ? "CHAR" : ""
        );

    return buf;
}


BOOL dmCompareAddr16(const Uint8 *buf, const size_t offs, const Uint16 addr)
{
    return buf[offs    ] == DM_GET_ADDR_LO(addr) &&
           buf[offs + 1] == DM_GET_ADDR_HI(addr);
}


int dmC64ImageGetNumBanks(const DMC64ImageFormat *fmt)
{
    int nbanks = 0;
    for (int i = 0; i < D64_MAX_ENCDEC_OPS; i++)
    {
        const DMC64EncDecOp *op = &fmt->encdecOps[i];
        if (op->type == DT_LAST)
            break;

        if (op->bank > nbanks)
            nbanks = op->bank;
    }

    return nbanks + 1;
}


DMC64Image *dmC64ImageAlloc(const DMC64ImageFormat *fmt)
{
    DMC64Image *img = dmMalloc0(sizeof(DMC64Image));

    if (img == NULL)
        return NULL;

    // Initialize image information
    img->type        = fmt->type;
    img->width       = fmt->width;
    img->height      = fmt->height;
    img->chWidth     = fmt->chWidth;
    img->chHeight    = fmt->chHeight;
    img->nbanks      = dmC64ImageGetNumBanks(fmt);

    img->screenSize  = img->chWidth * img->chHeight;
    img->bitmapSize  = img->screenSize * 8;
    img->charmemSize = C64_MAX_CHARS * C64_CHR_SIZE;

    // Allocate banks
    if ((img->color = dmCalloc(img->nbanks, sizeof(Uint8 *))) == NULL ||
        (img->bitmap = dmCalloc(img->nbanks, sizeof(Uint8 *))) == NULL ||
        (img->screen = dmCalloc(img->nbanks, sizeof(Uint8 *))) == NULL ||
        (img->charmem = dmCalloc(img->nbanks, sizeof(Uint8 *))) == NULL)
        goto err;

    for (int i = 0; i < img->nbanks; i++)
    {
        if ((img->color[i] = dmMalloc0(img->screenSize)) == NULL)
            goto err;

        if ((img->bitmap[i] = dmMalloc0(img->bitmapSize)) == NULL)
            goto err;

        if ((img->screen[i] = dmMalloc0(img->screenSize)) == NULL)
            goto err;

        if ((img->charmem[i] = dmMalloc0(img->charmemSize)) == NULL)
            goto err;
    }

    return img;

err:
    dmC64ImageFree(img);
    return NULL;
}


void dmC64ImageFree(DMC64Image *img)
{
    if (img != NULL)
    {
        // Free the allocated areas
        for (int i = 0; i < img->nbanks; i++)
        {
            dmFree(img->color[i]);
            dmFree(img->bitmap[i]);
            dmFree(img->screen[i]);
            dmFree(img->charmem[i]);
        }

        // Free the pointers to the areas
        dmFree(img->color);
        dmFree(img->bitmap);
        dmFree(img->screen);
        dmFree(img->charmem);

        // Extra data ..
        for (int i = 0; i < C64_MAX_EXTRA_DATA; i++)
            dmFree(img->extraData[i]);

        dmMemset(img, 0, sizeof(DMC64Image));
        dmFree(img);
    }
}


int dmC64ConvertCSDataToImage(DMImage *img,
    int xoffs, int yoffs, const Uint8 *buf,
    int width, int height, BOOL multicolor,
    int *colors)
{
    int yc, widthpx = width * 8;
    Uint8 *dp;

    if (img == NULL)
        return DMERR_NULLPTR;

    if (xoffs < 0 || yoffs < 0 ||
        xoffs > img->width - widthpx ||
        yoffs > img->height - height)
        return DMERR_INVALID_ARGS;

    dp = img->data + (yoffs * img->pitch) + xoffs;

    if (multicolor)
    {
        for (yc = 0; yc < height; yc++)
        {
            const int offs = yc * width;
            int xc;
            Uint8 *d = dp;

            for (xc = 0; xc < widthpx / 2; xc++)
            {
                const int b = buf[offs + (xc / 4)];
                const int v = 6 - ((xc * 2) & 6);
                const Uint8 c = colors[(b >> v) & 3];

                *d++ = c;
                *d++ = c;
            }

            dp += img->pitch;
        }
    }
    else
    {
        for (yc = 0; yc < height; yc++)
        {
            const int offs = yc * width;
            int xc;
            Uint8 *d = dp;

            for (xc = 0; xc < widthpx; xc++)
            {
                const int b = buf[offs + (xc / 8)];
                const int v = 7 - (xc & 7);
                const Uint8 c = colors[(b >> v) & 1];

                *d++ = c;
            }

            dp += img->pitch;
        }
    }

    return DMERR_OK;
}


void dmGenericRLEAnalyze(const DMGrowBuf *buf, Uint8 *rleMarker, const int rleType)
{
#define DM_STAT_MAX 256
    size_t *stats;

    // Allocate statistics counts buffer
    if ((stats = dmMalloc0(DM_STAT_MAX * sizeof(size_t))) == NULL)
        return;

    // Get statistics on the data
    for (size_t offs = 0; offs < buf->len; offs++)
        stats[buf->data[offs]]++;

    // According to compression type ..
    if (rleType == DM_COMP_RLE_MARKER)
    {
        size_t selected = 0,
            smallest = buf->len;

        // Find least used byte value
        for (size_t n = 0; n < DM_STAT_MAX; n++)
        {
            if (stats[n] < smallest)
            {
                selected = n;
                smallest = stats[n];
            }
        }

        *rleMarker = selected;
    }
    else
        *rleMarker = 0xC0;

    dmFree(stats);
}


int dmDecodeGenericRLE(DMGrowBuf *dst, const Uint8 *src, const Uint8 *srcEnd, const DMCompParams *cfg)
{
    int res;

    // Perform RLE decode
    while (src < srcEnd)
    {
        Uint8 data = *src++;
        int count = 1;

        switch (cfg->type)
        {
            case DM_COMP_RLE_MARKER:
                // A simple marker byte RLE variant: [Marker] [count] [data]
                if (data == cfg->rleMarker)
                {
                    if (srcEnd - src < 2)
                    {
                        res = DMERR_INVALID_DATA;
                        goto err;
                    }
                    count = *src++;
                    data = *src++;
                }
                break;

            case DM_COMP_RLE_MASK:
                // Mask marker RLE: usually high bit(s) of byte mark RLE sequence
                // and the lower bits contain the count: [Mask + count] [data]
                if ((data & cfg->rleMask1) == cfg->rleMarker)
                {
                    if (srcEnd - src < 1)
                    {
                        res = DMERR_INVALID_DATA;
                        goto err;
                    }

                    count = data & cfg->rleMask2;
                    data = *src++;
                }
                break;
        }

        while (count--)
        {
            if (!dmGrowBufPutU8(dst, data))
            {
                res = DMERR_MALLOC;
                goto err;
            }
        }
    }

    res = DMERR_OK;

err:
    return res;
}


int dmDecodeGenericRLEAlloc(DMGrowBuf *dst, const Uint8 *src, const Uint8 *srcEnd, const DMCompParams *cfg)
{
    int res;
    if ((res = dmGrowBufAlloc(dst, BUF_SIZE_INITIAL, BUF_SIZE_GROW)) != DMERR_OK)
        return res;

    return dmDecodeGenericRLE(dst, src, srcEnd, cfg);
}


static BOOL dmEncodeGenericRLESequence(DMGrowBuf *dst, const Uint8 data, Uint8 count, const DMCompParams *cfg)
{
    BOOL copyOnly = FALSE;

    switch (cfg->type)
    {
        case DM_COMP_RLE_MARKER:
            if (count >= cfg->rleMinCount || data == cfg->rleMarker)
            {
                // A simple marker byte RLE variant: [Marker] [count] [data]
                if (!dmGrowBufPutU8(dst, cfg->rleMarker) ||
                    !dmGrowBufPutU8(dst, count) ||
                    !dmGrowBufPutU8(dst, data))
                    return FALSE;
            }
            else
                copyOnly = TRUE;
            break;

        case DM_COMP_RLE_MASK:
            if (count >= cfg->rleMinCount || (data & cfg->rleMarker) == cfg->rleMarker)
            {
                // Mask marker RLE: usually high bit(s) of byte mark RLE sequence
                // and the lower bits contain the count: [Mask + count] [data]
                if (!dmGrowBufPutU8(dst, cfg->rleMarker | count) ||
                    !dmGrowBufPutU8(dst, data))
                    return FALSE;
            }
            else
                copyOnly = TRUE;
            break;
    }

    if (copyOnly)
    {
        while (count--)
        {
            if (!dmGrowBufPutU8(dst, data))
                return FALSE;
        }
    }

    return TRUE;
}


int dmEncodeGenericRLE(DMGrowBuf *dst, const Uint8 *src, const Uint8 *srcEnd, const DMCompParams *cfg)
{
    // Perform RLE encoding
    int count = 0, prev = -1;
    while (src < srcEnd)
    {
        Uint8 data = *src++;

        if (data != prev || count >= cfg->rleMaxCount)
        {
            if (!dmEncodeGenericRLESequence(dst, prev, count, cfg))
                goto err;

            count = 1;
        }
        else
            count++;

        prev = data;
    }

    if (!dmEncodeGenericRLESequence(dst, prev, count, cfg))
        goto err;

    return DMERR_OK;

err:
     return dmError(DMERR_MALLOC,
        "Could reallocate memory for RLE encoding buffer.\n");
}


int dmEncodeGenericRLEAlloc(DMGrowBuf *dst, const Uint8 *src, const Uint8 *srcEnd, const DMCompParams *cfg)
{
    int res;
    if ((res = dmGrowBufAlloc(dst, BUF_SIZE_INITIAL, BUF_SIZE_GROW)) != DMERR_OK)
        return res;

    return dmEncodeGenericRLE(dst, src, srcEnd, cfg);
}


// Perform probing of the given data buffer, trying to determine
// if it contains a supported "C64" image format. Returns the
// "probe score", see libgfx.h for list of values. If a match
// is found, pointer to format description is set to *pfmt.
int dmC64ProbeBMP(const Uint8 *buf, const size_t len, const DMC64ImageFormat **pfmt)
{
    int scoreMax = DM_PROBE_SCORE_FALSE, scoreIndex = -1;

    for (int i = 0; i < ndmC64ImageFormats; i++)
    {
        const DMC64ImageFormat *fmt = &dmC64ImageFormats[i];
        int score = DM_PROBE_SCORE_FALSE;
        if (fmt->probe == NULL && fmt->size > 0 && fmt->addr > 0)
        {
            // Generic probe just checks matching size and load address
            if (len == fmt->size && dmCompareAddr16(buf, 0, fmt->addr))
                score = DM_PROBE_SCORE_GOOD;
        }
        else
        if (fmt->probe != NULL)
            score = fmt->probe(buf, len, fmt);

        if (score > scoreMax)
        {
            scoreMax = score;
            scoreIndex = i;
        }
    }

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


int dmC64SanityCheckEncDecOp(const int i, const DMC64EncDecOp *op, const DMC64Image *img)
{
    switch (op->type)
    {
        case DT_COLOR_RAM:
        case DT_BITMAP:
        case DT_SCREEN_RAM:
        case DT_CHAR_DATA:
            if (op->bank < 0 || op->bank > img->nbanks)
            {
                return dmError(DMERR_INTERNAL,
                    "Invalid bank %d / %d definition in generic encode/decode operator %d @ #%d.\n",
                    op->bank, img->nbanks, op->type, i);
            }
            break;

        case DT_EXTRA_DATA:
            if (op->bank < 0 || op->bank >= C64_MAX_EXTRA_DATA)
            {
                return dmError(DMERR_INTERNAL,
                    "Invalid bank %d definition in generic encode/decode operator %d @ #%d.\n",
                    op->bank, op->type, i);
            }
            break;
    }

    if (op->type < 0 || op->type >= DT_LAST)
    {
        return dmError(DMERR_INTERNAL,
            "Invalid encode/decode operator type %d @ #%d.\n",
            op->type, i);
    }

    return DMERR_OK;
}


BOOL dmC64GetOpSize(const DMC64EncDecOp *op, const DMC64ImageFormat *fmt, size_t *size)
{
    switch (op->type)
    {
        case DT_SCREEN_RAM:
        case DT_COLOR_RAM:
            *size = fmt->chHeight * fmt->chWidth;
            break;

        case DT_BITMAP:
            *size = fmt->chHeight * fmt->chWidth * 8;
            break;

        case DT_CHAR_DATA:
            *size = C64_MAX_CHARS * C64_CHR_SIZE;
            break;

        case DT_COLOR_REG:
            *size = 1;
            break;

        case DT_DEC_FUNCTION:
        case DT_ENC_FUNCTION:
        case DT_EXTRA_DATA:
            *size = op->size;
            break;

        default:
            *size = 0;
    }

    return TRUE;
}


int dmC64DecodeGenericBMP(DMC64Image *img, const Uint8 *buf,
    const size_t len, const DMC64ImageFormat *fmt)
{
    int res = DMERR_OK;

    if (buf == NULL || img == NULL || fmt == NULL)
        return DMERR_NULLPTR;

    // Clear the image structure, set basics
    img->type      = fmt->type;
    img->width     = fmt->width;
    img->height    = fmt->height;
    img->chWidth   = fmt->chWidth;
    img->chHeight  = fmt->chHeight;
    img->nbanks    = dmC64ImageGetNumBanks(fmt);

    // Perform decoding
    for (int i = 0; i < D64_MAX_ENCDEC_OPS; i++)
    {
        const DMC64EncDecOp *op = &fmt->encdecOps[i];
        const Uint8 *src;
        size_t size;

        // Check for last operator
        if (op->type == DT_LAST)
            break;

        // Check operation validity
        if ((res = dmC64SanityCheckEncDecOp(i, op, img)) != DMERR_OK)
            return res;

        // Check size
        if (!dmC64GetOpSize(op, fmt, &size))
        {
            return dmError(DMERR_INVALID_DATA,
                "Decode op SIZE out of bounds, op #%d type=%d, offs=%d ($%04x), "
                "bank=%d, size=%d ($%04x) vs. allocated %d ($%04x)\n",
                i, op->type, op->offs, op->offs, op->bank, size, size, op->size, op->size);
        }

        // Do we need to reallocate some more space?
        if (op->offs + size > len + 1)
        {
            return dmError(DMERR_INVALID_DATA,
                "Decode out of bounds, op #%d type=%d, offs=%d ($%04x), "
                "bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                i, op->type, op->offs, op->offs, op->bank, size, size, len, len);
        }

        src = buf + op->offs;

        // Perform operation
        switch (op->type)
        {
            case DT_COLOR_RAM:   memcpy(img->color[op->bank], src, size); break;
            case DT_BITMAP:      memcpy(img->bitmap[op->bank], src, size); break;
            case DT_SCREEN_RAM:  memcpy(img->screen[op->bank], src, size); break;
            case DT_CHAR_DATA:   memcpy(img->charmem[op->bank], src, size); break;
            case DT_EXTRA_DATA:
                if (img->extraData[op->bank] != NULL)
                {
                    return dmError(DMERR_INTERNAL,
                        "Extra data block already allocated and used! "
                        "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                        i, op->offs, op->offs, op->bank, size, size, len, len);
                }
                if ((img->extraData[op->bank] = dmMalloc0(size)) == NULL)
                {
                    return dmError(DMERR_MALLOC,
                        "Could not allocate extradata block! "
                        "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                        i, op->offs, op->offs, op->bank, size, size, len, len);
                }
                img->extraDataSizes[op->bank] = size;
                memcpy(img->extraData[op->bank], src, size);
                break;

            case DT_COLOR_REG:
                switch (op->size)
                {
                    case DC_D020: img->d020 = *src; break;
                    case DC_BGCOL:
                    case DC_D021: img->bgcolor = *src; break;
                    case DC_D022: img->d022 = *src; break;
                    case DC_D023: img->d023 = *src; break;
                    case DC_D024: img->d024 = *src; break;
                    default:
                        return dmError(DMERR_INTERNAL,
                            "Unhandled DT_COLOR_REG mode %d in "
                            "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                            op->size, i, op->offs, op->offs, op->bank, size, size, len, len);
                }
                break;

            case DT_COLOR_SET:
                switch (op->size)
                {
                    case DC_D020: img->d020 = op->offs; break;
                    case DC_BGCOL:
                    case DC_D021: img->bgcolor = op->offs; break;
                    case DC_D022: img->d022 = op->offs; break;
                    case DC_D023: img->d023 = op->offs; break;
                    case DC_D024: img->d024 = op->offs; break;
                    default:
                        return dmError(DMERR_INTERNAL,
                            "Unhandled DT_COLOR_SET mode %d in "
                            "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                            op->size, i, op->offs, op->offs, op->bank, size, size, len, len);
                }
                break;

            case DT_CHAR_CONFIG:
                switch (op->offs)
                {
                    case D64_CHCFG_SCREEN:
                        break;

                    case D64_CHCFG_LINEAR:
                        {
                            for (int bank = 0; bank < img->nbanks; bank++)
                            for (int offs = 0; offs < fmt->chHeight * fmt->chWidth; offs++)
                                img->screen[bank][offs] = offs & 0xff;
                        }
                        break;

                    default:
                        return dmError(DMERR_INTERNAL,
                            "Unhandled DT_CHAR_CONFIG mode %d in ",
                            "op #%d, bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                            op->offs, i, op->bank, size, size, len, len);
                }
                break;

            case DT_DEC_FUNCTION:
                if (op->decFunction == NULL)
                {
                    return dmError(DMERR_INTERNAL,
                        "Decode op is a function, but function ptr is NULL: "
                        "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                        i, op->offs, op->offs, op->bank, size, size, len, len);
                }
                if (!op->decFunction(img, op, buf, len, fmt))
                {
                    return dmError(DMERR_INTERNAL,
                        "Decode op custom function failed: op #%d, "
                        "offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                        i, op->offs, op->offs, op->bank, size, size, len, len);
                }
                break;
        }
    }

    return DMERR_OK;
}


int dmC64EncodeGenericBMP(const BOOL allocate, DMGrowBuf *buf, const DMC64Image *img, const DMC64ImageFormat *fmt)
{
    int res = DMERR_OK;

    if (img == NULL || fmt == NULL)
        return DMERR_NULLPTR;

    // Allocate the output buffer if requested
    if (allocate && (res = dmGrowBufAlloc(buf, BUF_SIZE_INITIAL, BUF_SIZE_GROW)) != DMERR_OK)
    {
        dmError(res,
            "Could not allocate %d bytes of memory for C64 image encoding buffer.\n",
            fmt->size);
        goto err;
    }

    dmGrowBufPush(buf);

    // Perform encoding
    for (int i = 0; i < D64_MAX_ENCDEC_OPS; i++)
    {
        const DMC64EncDecOp *op = &fmt->encdecOps[i];
        size_t size, chksize;

        // Check for last operator
        if (op->type == DT_LAST)
            break;

        // Check operation validity
        if ((res = dmC64SanityCheckEncDecOp(i, op, img)) != DMERR_OK)
            goto err;

        // Check size
        if (!dmC64GetOpSize(op, fmt, &size))
        {
            res = dmError(DMERR_INVALID_DATA,
                "Encode op SIZE out of bounds, op #%d type=%d, offs=%d ($%04x), "
                "bank=%d, size=%d ($%04x) vs. allocated %d ($%04x)\n",
                i, op->type, op->offs, op->offs, op->bank, size, size, op->size, op->size);
            goto err;
        }

        // Do we need to reallocate some more space?
        chksize = op->offs + size;
        if (!dmGrowBufCheckGrow(buf, chksize))
        {
            res = dmError(DMERR_MALLOC,
                "Could not re-allocate %d bytes of memory for C64 image encoding buffer.\n",
                chksize);
            goto err;
        }

        if (chksize > buf->len)
            buf->len = chksize;

        // Perform operation
        Uint8 *dst = buf->data + op->offs;
        switch (op->type)
        {
            case DT_COLOR_RAM:   memcpy(dst, img->color[op->bank], size); break;
            case DT_BITMAP:      memcpy(dst, img->bitmap[op->bank], size); break;
            case DT_SCREEN_RAM:  memcpy(dst, img->screen[op->bank], size); break;
            case DT_CHAR_DATA:   memcpy(dst, img->charmem[op->bank], size); break;
            case DT_EXTRA_DATA:
                if (img->extraData[op->bank] == NULL)
                {
                    res = dmError(DMERR_NULLPTR,
                        "DT_EXTRA_DATA block is NULL in "
                        "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                        i, op->offs, op->offs, op->bank, size, size, buf->len, buf->len);
                    goto err;
                }
                if (size > img->extraDataSizes[op->bank])
                {
                    res = dmError(DMERR_INTERNAL,
                        "DT_EXTRA_DATA size mismatch %d <> %d in "
                        "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                        op->size, img->extraDataSizes[op->bank], i, op->offs, op->offs, op->bank, size, size, buf->len, buf->len);
                    goto err;
                }
                memcpy(dst, img->extraData[op->bank], size);
                break;

            case DT_COLOR_REG:
                switch (op->size)
                {
                    case DC_D020: *dst = img->d020; break;
                    case DC_BGCOL:
                    case DC_D021: *dst = img->bgcolor; break;
                    case DC_D022: *dst = img->d022; break;
                    case DC_D023: *dst = img->d023; break;
                    case DC_D024: *dst = img->d024; break;
                    default:
                        res = dmError(DMERR_INTERNAL,
                            "Unhandled DT_COLOR_REG mode %d in "
                            "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                            op->size, i, op->offs, op->offs, op->bank, size, size, buf->len, buf->len);
                        goto err;
                }
                break;

            case DT_ENC_FUNCTION:
                if (op->encFunction == NULL)
                {
                    res = dmError(DMERR_INTERNAL,
                        "Encode op is a function, but function ptr is NULL: "
                        "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                        i, op->offs, op->offs, op->bank, size, size, buf->len, buf->len);
                    goto err;
                }
                if (!op->encFunction(op, buf, img, fmt))
                {
                    res = dmError(DMERR_INTERNAL,
                        "Encode op custom function failed: op #%d, "
                        "offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                        i, op->offs, op->offs, op->bank, size, size, buf->len, buf->len);
                    goto err;
                }
                break;
        }
    }

    dmGrowBufPop(buf);

    res = DMERR_OK;

err:
    return res;
}


// Convert a generic "C64" format bitmap in DMC64Image struct to
// a indexed/paletted bitmap image.
int dmC64ConvertGenericBMP2Image(DMImage *dst, const DMC64Image *src, const DMC64ImageFormat *fmt)
{
    DMC64GetPixelFunc getPixel;

    // Sanity check arguments
    if (dst == NULL || src == NULL)
        return DMERR_NULLPTR;

    if (dst->width < src->width || dst->height < src->height)
    {
        return dmError(DMERR_INVALID_DATA,
            "Invalid src vs. dst width/height %d x %d <-> %d x %d\n",
            src->width, src->height, dst->width, dst->height);
    }

    dmMemset(dst->data, 0, dst->size);

    // Check pixel getter function
    if (fmt->getPixel != NULL)
        getPixel = fmt->getPixel;
    else
        getPixel = (fmt->type & D64_FMT_MC) ? fmtGetGenericMCPixel : fmtGetGenericSCPixel;

    // Resolution interlaced pics need to halve the source width
    int rwidth = src->width;
    if ((src->type & D64_FMT_ILACE) && src->laceType == D64_ILACE_RES)
        rwidth /= 2;

    // Perform conversion
    Uint8 *dp = dst->data;
    for (int yc = 0; yc < src->height; yc++)
    {
        Uint8 *d = dp;
        const int y = yc / 8, yb = yc & 7;
        const int scroffsy = y * src->chWidth;
        int xc;

        if (src->type & D64_FMT_CHAR)
        {
            // Charmode conversion
            if ((src->type & D64_FMT_MC) == D64_FMT_HIRES)
            // Hi-res charmap
            for (xc = 0; xc < rwidth; xc++)
            {
                const int x = xc / 8;
                const int scroffs = scroffsy + x;
                const int chr = src->screen[0][scroffs];
                const int v = 7 - (xc & 7);

                if ((src->charmem[0][chr * C64_CHR_SIZE + yb] >> v) & 1)
                    *d++ = src->color[0][scroffs];
                else
                    *d++ = src->bgcolor;
            }
            else
            // Multicolor variants
            for (xc = 0; xc < rwidth; xc++)
            {
                const int x = xc / 4;
                const int scroffs = scroffsy + x;
                const int chr = src->screen[0][scroffs];
                const int col = src->color[0][scroffs] & 15;

                if (col & 8)
                {
                    const int v = 6 - ((xc * 2) & 6);
                    switch ((src->charmem[0][chr * C64_CHR_SIZE + yb] >> v) & 3)
                    {
                        case 0: *d++ = src->bgcolor; break;
                        case 1: *d++ = src->d022; break;
                        case 2: *d++ = src->d023; break;
                        case 3: *d++ = col;
                    }
                }
                else
                {
                    const int v = 7 - (xc & 7);
                    if ((src->charmem[0][chr * C64_CHR_SIZE + yb] >> v) & 1)
                        *d++ = src->color[0][scroffs];
                    else
                        *d++ = src->bgcolor;
                }
            }
        }
        else
        {
            // Perform generic BITMAP conversion
            const int bmoffsy = y * src->chWidth * 8 + yb;

            if ((src->type & D64_FMT_MC) == D64_FMT_HIRES)
            // Hi-res bitmap
            for (xc = 0; xc < rwidth; xc++)
            {
                const int x = xc / 8;
                const int scroffs = scroffsy + x;
                const int bmoffs = bmoffsy + (x * 8);
                const int vshift = 7 - (xc & 7);

                *d++ = getPixel(src, bmoffs, scroffs, vshift, 0, yc);
            }
            else
            // Multicolor bitmap and variants
            for (xc = 0; xc < rwidth; xc++)
            {
                const int x = xc / 4;
                const int scroffs = scroffsy + x;
                const int bmoffs = bmoffsy + (x * 8);
                const int vshift = 6 - ((xc * 2) & 6);

                if (src->type & D64_FMT_ILACE)
                {
                    switch (src->laceType)
                    {
                        case D64_ILACE_RES:
                            *d++ = getPixel(src, bmoffs, scroffs, vshift, 0, yc);
                            *d++ = getPixel(src, bmoffs, scroffs, vshift, 1, yc);
                            break;

                        default:
                            return DMERR_NOT_SUPPORTED;
                    }
                }
                else
                {
                    *d++ = getPixel(src, bmoffs, scroffs, vshift, 0, yc);
                }
            }
        }
        dp += dst->pitch;
    }

    return DMERR_OK;
}


int dmC64ConvertBMP2Image(DMImage **pdst, const DMC64Image *src, const DMC64ImageFormat *fmt)
{
    int res;
    DMImage *dst;

    if (pdst == NULL || src == NULL)
        return DMERR_NULLPTR;

    // Allocate image structure
    if ((*pdst = dst = dmImageAlloc(src->width, src->height, DM_IFMT_PALETTE, -1)) == NULL)
        return DMERR_MALLOC;

    // Set partial palette information
    dst->ncolors  = C64_NCOLORS;
    dst->constpal = TRUE;

    // Convert
    if (fmt->convertFrom != NULL)
        res = fmt->convertFrom(dst, src, fmt);
    else
        res = dmC64ConvertGenericBMP2Image(dst, src, fmt);

    return res;
}


int dmC64DecodeBMP(DMC64Image **img, const Uint8 *buf, const size_t len,
    const size_t probeOffs, const size_t loadOffs,
    const DMC64ImageFormat **fmt, const DMC64ImageFormat *forced)
{
    if (img == NULL)
        return DMERR_NULLPTR;

    // Check for forced format
    if (forced != NULL)
        *fmt = forced;
    else
    {
        // Nope, perform a generic probe
        if (probeOffs >= len)
            return DMERR_OUT_OF_DATA;

        if (dmC64ProbeBMP(buf + probeOffs, len - probeOffs, fmt) == DM_PROBE_SCORE_FALSE)
            return DMERR_NOT_SUPPORTED;
    }

    if (loadOffs >= len)
        return DMERR_INVALID_ARGS;

    if (*fmt == NULL)
        return DMERR_INVALID_DATA;

    // Allocate memory
    if ((*img = dmC64ImageAlloc(*fmt)) == NULL)
        return DMERR_MALLOC;

    // Decode the bitmap to memory layout
    if ((*fmt)->decode != NULL)
        return (*fmt)->decode(*img, buf + loadOffs, len - loadOffs, *fmt);
    else
        return dmC64DecodeGenericBMP(*img, buf + loadOffs, len - loadOffs, *fmt);
}


// Convert a generic bitmap image to DMC64Image
int dmC64ConvertGenericImage2BMP(DMC64Image *dst, const DMImage *src, const DMC64ImageFormat *fmt)
{
    if (dst == NULL || src == NULL || fmt == NULL)
        return DMERR_NULLPTR;

    return DMERR_OK;
}


int dmC64ConvertImage2BMP(DMC64Image **pdst, const DMImage *src, const DMC64ImageFormat *fmt)
{
    int res;
    DMC64Image *dst;

    if (pdst == NULL || src == NULL)
        return DMERR_NULLPTR;

    // Allocate the basic C64 bitmap image structure
    if ((*pdst = dst = dmC64ImageAlloc(fmt)) == NULL)
        return DMERR_MALLOC;

    // Convert
    if (fmt->convertTo != NULL)
        res = fmt->convertTo(dst, src, fmt);
    else
        res = dmC64ConvertGenericImage2BMP(dst, src, fmt);

    return res;
}


int dmC64EncodeBMP(DMGrowBuf *buf, const DMC64Image *img, const DMC64ImageFormat *fmt)
{
    int res;

    if (img == NULL || fmt == NULL)
        return DMERR_NULLPTR;

    // Allocate a buffer
    if ((res = dmGrowBufAlloc(buf, BUF_SIZE_INITIAL, BUF_SIZE_GROW)) != DMERR_OK)
        goto err;

    // Add the loading address
    if (!dmGrowBufPutU8(buf, DM_GET_ADDR_LO(fmt->addr)) ||
        !dmGrowBufPutU8(buf, DM_GET_ADDR_HI(fmt->addr)))
        goto err;

    // Attempt to encode the image to a buffer
    if (fmt->encode != NULL)
        res = fmt->encode(buf, img, fmt);
    else
        res = dmC64EncodeGenericBMP(FALSE, buf, img, fmt);

    if (res != DMERR_OK)
        goto err;

    return DMERR_OK;

err:
    dmGrowBufFree(buf);
    return res;
}