view tools/gfxconv.c @ 1545:3b613fcbf3ff

Improve how format read/write capabilities are marked and shown.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 12 May 2018 21:01:46 +0300
parents 416d7b3ba3b2
children 228e71d66089
line wrap: on
line source

/*
 * gfxconv - Convert various graphics formats
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2012-2017 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "dmtool.h"
#include "dmlib.h"
#include "dmargs.h"
#include "dmfile.h"
#include "libgfx.h"
#include "lib64gfx.h"
#include "dmmutex.h"


#define DM_MAX_COLORS 256

#define DM_ASC_NBITS    8
#define DM_ASC_NCOLORS  4
static const char dmASCIIPalette[DM_ASC_NCOLORS] = ".:X#";


enum
{
    FFMT_AUTO = 0,

    FFMT_ASCII,
    FFMT_ANSI,
    FFMT_IMAGE,

    FFMT_CHAR,
    FFMT_SPRITE,
    FFMT_BITMAP,

    FFMT_LAST
};


enum
{
    CROP_NONE = 0,
    CROP_AUTO,
    CROP_SIZE,
};


typedef struct
{
    char *name;       // Descriptive name of the format
    char *fext;       // File extension
    int flags;        // DM_FMT_* flags, see libgfx.h
    int format;       // Format identifier
    int subformat;    // Subformat identifier
} DMConvFormat;


static DMConvFormat convFormatList[] =
{
    { "ASCII text"                           , "asc"   , DM_FMT_WR   , FFMT_ASCII  , 0 },
    { "ANSI colored text"                    , "ansi"  , DM_FMT_WR   , FFMT_ANSI   , 0 },
    { "PNG image"                            , "png"   , DM_FMT_RDWR , FFMT_IMAGE  , DM_IMGFMT_PNG },
    { "PPM image"                            , "ppm"   , DM_FMT_WR   , FFMT_IMAGE  , DM_IMGFMT_PPM },
    { "PCX image"                            , "pcx"   , DM_FMT_RDWR , FFMT_IMAGE  , DM_IMGFMT_PCX },
    { "IFF ILBM"                             , "lbm"   , DM_FMT_RD   , FFMT_IMAGE  , DM_IMGFMT_ILBM },
    { "Bitplaned RAW (intl/non-intl) image"  , "raw"   , DM_FMT_WR   , FFMT_IMAGE  , DM_IMGFMT_RAW },
    { "IFFMaster RAW image"                  , "araw"  , DM_FMT_WR   , FFMT_IMAGE  , DM_IMGFMT_ARAW },
    { "C64 bitmap image"                     , NULL    , DM_FMT_RDWR , FFMT_BITMAP , -1  },
    { "C64 character/font data"              , "chr"   , DM_FMT_RDWR , FFMT_CHAR   , 0 },
    { "C64 sprite data"                      , "spr"   , DM_FMT_RDWR , FFMT_SPRITE , 0 },
};

static const int nconvFormatList = sizeof(convFormatList) / sizeof(convFormatList[0]);


typedef struct
{
    BOOL triplet, alpha;
    DMColor color;
    unsigned int from, to;
} DMMapValue;



char    *optInFilename = NULL,
        *optOutFilename = NULL;

int     optInFormat = FFMT_AUTO,
        optOutFormat = FFMT_ASCII,
        optInSubFormat = DM_IMGFMT_PNG,
        optOutSubFormat = DM_IMGFMT_PNG,
        optItemCount = -1,
        optPlanedWidth = 1,
        optForcedFormat = -1;
unsigned int optInSkip = 0;

int     optCropMode = CROP_NONE,
        optCropX0, optCropY0,
        optCropW, optCropH;

BOOL    optInMulticolor = FALSE,
        optSequential = FALSE,
        optRemapColors = FALSE,
        optRemapRemove = FALSE;
int     optNRemapTable = 0;
DMMapValue optRemapTable[DM_MAX_COLORS];
int     optColors[C64_MAX_COLORS];

DMImageConvSpec optSpec =
{
    .scaleX = 1,
    .scaleY = 1,
    .nplanes = 4,
    .bpp = 8,
    .planar = FALSE,
    .paletted = FALSE,
    .format = 0,
};

static const DMOptArg optList[] =
{
    {  0, '?', "help",          "Show this help", OPT_NONE },
    { 15, 'v', "verbose",       "Increase verbosity", OPT_NONE },
    {  3, 'o', "output",        "Output filename", OPT_ARGREQ },
    {  1, 'i', "informat",      "Set input format (sprite[:mc:sc], char[:mc|sc], bitmap[:<bformat>], image)", OPT_ARGREQ },
    {  2, 'm', "multicolor",    "Input is multicolor / output in multicolor", OPT_NONE },
    {  4, 's', "skip",          "Skip bytes in input", OPT_ARGREQ },
    {  5, 'f', "format",        "Output format (see --formats)", OPT_ARGREQ },
    { 17,  0 , "formats",       "List available input/output formats", OPT_NONE },
    {  8, 'q', "sequential",    "Output sequential files (image output only)", OPT_NONE },
    {  6, 'c', "colormap",      "Color mappings (see below for information)", OPT_ARGREQ },
    {  7, 'n', "numitems",      "How many 'items' to output (default: all)", OPT_ARGREQ },
    { 11, 'w', "width",         "Item width (number of items per row, min 1)", OPT_ARGREQ },
    {  9, 'S', "scale",         "Scale output image by <n> or <x>:<y> integer factor(s). "
                                "-S <n> scales both height and width by <n>.", OPT_ARGREQ },
    { 12, 'P', "paletted",      "Use indexed/paletted output IF possible.", OPT_NONE },
    { 13, 'N', "nplanes",       "# of bitplanes (some output formats)", OPT_ARGREQ },
    { 18, 'B', "bpp",           "Bits per pixel (some output formats)", OPT_ARGREQ },
    { 14, 'I', "interleave",    "Interleaved/planar output (some output formats)", OPT_NONE },
    { 16, 'R', "remap",         "Remap output image colors (-R <(#RRGGBB|index):index>[,<..>][+remove] | -R @map.txt[+remove])", OPT_ARGREQ },
};

static const int optListN = sizeof(optList) / sizeof(optList[0]);


void argShowFormats()
{
    printf(
    "Available input/output formats (-f <frmt>):\n"
    " frmt | RW | Description\n"
    "------+----+-------------------------------------------------------\n"
    );

    for (int i = 0; i < nconvFormatList; i++)
    {
        const DMConvFormat *fmt = &convFormatList[i];
        printf("%-5s | %c%c | %s\n",
            fmt->fext ? fmt->fext : "",
            (fmt->flags & DM_FMT_RD) ? 'R' : ' ',
            (fmt->flags & DM_FMT_WR) ? 'W' : ' ',
            fmt->name);
    }

    printf(
    "\n"
    "(Not all input->output combinations are actually supported.)\n"
    "\n"
    "Available C64 bitmap formats (-f <bfrm>):\n"
    " frmt | RW | Type            | Description\n"
    "------+----+-----------------+-------------------------------------\n"
    );

    for (int i = 0; i < ndmC64ImageFormats; i++)
    {
        const DMC64ImageFormat *fmt = &dmC64ImageFormats[i];
        char buf[64];
        printf("%-5s | %c%c | %-15s | %s\n",
            fmt->fext,
            (fmt->flags & DM_FMT_RD) ? 'R' : ' ',
            (fmt->flags & DM_FMT_WR) ? 'W' : ' ',
            dmC64GetImageTypeString(buf, sizeof(buf), fmt->type, FALSE),
            fmt->name);
    }
}


void argShowHelp()
{
    dmPrintBanner(stdout, dmProgName, "[options] <input file>");
    dmArgsPrintHelp(stdout, optList, optListN, 0);

    printf(
    "\n"
    "Palette / color remapping (-R)\n"
    "------------------------------\n"
    "Indexed palette/color remapping can be performed via the -R option, either\n"
    "specifying single colors or filename of file containing remap definitions.\n"
    "Colors to be remapped can be specified either by their palette index or by\n"
    "their RGB values as a hex triplet (#rrggbb). Example of a remap definition:\n"
    "-R #000000:0,#ffffff:1 would remap black and white to indices 0 and 1.\n"
    "\n"
    "Remap file can be specified as \"-R @filename\", and it is a text file with\n"
    "one remap definition per line in same format as above. All empty lines and\n"
    "lines starting with a semicolor (;) will be ignored as comments. Any extra\n"
    "whitespace separating items will be ignored as well.\n"
    "\n"
    "Optional +remove can be specified (-R <...>+remove), which will remove all\n"
    "unused colors from the palette. This is not usually desirable, for example\n"
    "when converting multiple images to same palette.\n"
    "\n"
    "Color map defs\n"
    "--------------\n"
    "Color map definitions are used for ANSI and image output, to declare what\n"
    "output colors of the C64 palette are used for each single color/multi color\n"
    "bit-combination. For example, if the input is multi color sprite or char,\n"
    "you can define colors like: -c 0,8,3,15 .. for single color: -c 0,1\n"
    "The numbers are palette indexes, and the order is for bit(pair)-values\n"
    "00, 01, 10, 11 (multi color) and 0, 1 (single color). NOTICE! 255 is the\n"
    "special color that can be used for transparency.\n"
    "\n"
    );
}


int dmGetConvFormat(int format, int subformat)
{
    int i;
    for (i = 0; i < nconvFormatList; i++)
    {
        DMConvFormat *fmt = &convFormatList[i];
        if (fmt->format == format &&
            fmt->subformat == subformat)
            return i;
    }
    return -1;
}


BOOL dmGetC64FormatByExt(const char *fext, int *format, int *subformat)
{
    int i;
    if (fext == NULL)
        return FALSE;

    for (i = 0; i < ndmC64ImageFormats; i++)
    {
        const DMC64ImageFormat *fmt = &dmC64ImageFormats[i];
        if (fmt->fext != NULL &&
            strcasecmp(fext, fmt->fext) == 0)
        {
            *format = FFMT_BITMAP;
            *subformat = i;
            return TRUE;
        }
    }

    return FALSE;
}



BOOL dmGetFormatByExt(const char *fext, int *format, int *subformat)
{
    int i;
    if (fext == NULL)
        return FALSE;

    for (i = 0; i < nconvFormatList; i++)
    {
        DMConvFormat *fmt = &convFormatList[i];
        if (fmt->fext != NULL &&
            strcasecmp(fext, fmt->fext) == 0)
        {
            *format = fmt->format;
            *subformat = fmt->subformat;
            return TRUE;
        }
    }

    return FALSE;
}


static BOOL dmParseMapOptionMapItem(const char *popt, DMMapValue *value, const unsigned int nmax, const char *msg)
{
    char *end, *split, *opt = dm_strdup(popt);

    if (opt == NULL)
        goto error;

    if ((end = split = strchr(opt, ':')) == NULL)
    {
        dmErrorMsg("Invalid %s value '%s', expected <(#|%)RRGGBB|[$|0x]index>:<[$|0x]index>.\n", msg, opt);
        goto error;
    }

    // Trim whitespace
    *end = 0;
    for (end--; end > opt && *end && isspace(*end); end--)
        *end = 0;

    // Parse either a hex triplet color definition or a normal value
    if (*opt == '#' || *opt == '%')
    {
        unsigned int colR, colG, colB, colA;

        if (sscanf(opt + 1, "%2x%2x%2x%2x", &colR, &colG, &colB, &colA) == 4 ||
            sscanf(opt + 1, "%2X%2X%2X%2X", &colR, &colG, &colB, &colA) == 4)
        {
            value->alpha = TRUE;
            value->color.a = colA;
        }
        else
        if (sscanf(opt + 1, "%2x%2x%2x", &colR, &colG, &colB) != 3 &&
            sscanf(opt + 1, "%2X%2X%2X", &colR, &colG, &colB) != 3)
        {
            dmErrorMsg("Invalid %s value '%s', expected a hex triplet, got '%s'.\n", msg, popt, opt + 1);
            goto error;
        }

        value->color.r = colR;
        value->color.g = colG;
        value->color.b = colB;
        value->triplet = TRUE;
    }
    else
    {
        if (!dmGetIntVal(opt, &value->from))
        {
            dmErrorMsg("Invalid %s value '%s', could not parse source value '%s'.\n", msg, popt, opt);
            goto error;
        }
        value->triplet = FALSE;
    }

    // Trim whitespace
    split++;
    while (*split && isspace(*split)) split++;

    // Parse destination value
    if (!dmGetIntVal(split, &value->to))
    {
        dmErrorMsg("Invalid %s value '%s', could not parse destination value '%s'.\n", msg, popt, split);
        goto error;
    }

    if (!value->triplet && value->from > 255)
    {
        dmErrorMsg("Invalid %s map source color index value %d, must be [0..255].\n", msg, value->from);
        goto error;
    }

    if (value->to > nmax)
    {
        dmErrorMsg("Invalid %s map destination color index value %d, must be [0..%d].\n", msg, value->to, nmax);
        goto error;
    }

    dmFree(opt);
    return TRUE;

error:
    dmFree(opt);
    return FALSE;
}


static BOOL dmParseMapOptionItem(char *opt, char *end, void *pvalue, const int index, const int nmax, const BOOL requireIndex, const char *msg)
{
    // Trim whitespace
    if (end != NULL)
    {
        *end = 0;
        for (end--; end > opt && *end && isspace(*end); end--)
            *end = 0;
    }
    while (*opt && isspace(*opt)) opt++;

    // Parse item based on mode
    if (requireIndex)
    {
        DMMapValue *value = (DMMapValue *) pvalue;
        if (!dmParseMapOptionMapItem(opt, &value[index], nmax, msg))
            return FALSE;
    }
    else
    {
        unsigned int *value = (unsigned int *) pvalue;
        char *split = strchr(opt, ':');
        if (split != NULL)
        {
            dmErrorMsg("Unexpected ':' in indexed %s '%s'.\n", msg, opt);
            return FALSE;
        }

        if (!dmGetIntVal(opt, &value[index]))
        {
            dmErrorMsg("Invalid %s value '%s', could not parse.\n", msg, opt);
            return FALSE;
        }
    }

    return TRUE;
}


BOOL dmParseMapOptionString(char *opt, void *values, int *nvalues, const int nmax, const BOOL requireIndex, const char *msg)
{
    char *start = opt;

    *nvalues = 0;
    while (*start && *nvalues < nmax)
    {
        char *end = strchr(start, ',');

        if (!dmParseMapOptionItem(start, end, values, *nvalues, nmax, requireIndex, msg))
            return FALSE;

        (*nvalues)++;

        if (!end)
            break;

        start = end + 1;
    }

    return TRUE;
}


int dmParseColorRemapFile(const char *filename, DMMapValue *values, int *nvalue, const int nmax)
{
    FILE *fp;
    char line[512];
    int res = DMERR_OK;

    if ((fp = fopen(filename, "r")) == NULL)
    {
        res = dmGetErrno();
        dmError(res, "Could not open color remap file '%s' for reading, %d: %s\n",
            res, dmErrorStr(res));
        return res;
    }

    while (fgets(line, sizeof(line), fp))
    {
        char *start = line;
        while (*start && isspace(*start)) start++;

        if (*start != 0 && *start != ';')
        {
            if (!dmParseMapOptionMapItem(line, &values[*nvalue], nmax, "mapping file"))
                goto error;
            else
            {
                (*nvalue)++;
                if (*nvalue >= nmax)
                {
                    dmErrorMsg("Too many mapping pairs in '%s', maximum is %d.\n",
                        filename, nmax);
                    goto error;
                }
            }
        }
    }

error:
    fclose(fp);
    return res;
}


BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    switch (optN)
    {
        case 0:
            argShowHelp();
            exit(0);
            break;

        case 17:
            argShowFormats();
            exit(0);
            break;

        case 15:
            dmVerbosity++;
            break;

        case 1:
            {
                switch (tolower(optArg[0]))
                {
                    case 's': optInFormat = FFMT_SPRITE; break;
                    case 'c': optInFormat = FFMT_CHAR; break;
                    case 'b': optInFormat = FFMT_BITMAP; break;
                    case 'i': optInFormat = FFMT_IMAGE; break;
                    default:
                        dmErrorMsg("Invalid input format '%s'.\n", optArg);
                        return FALSE;
                }

                char *tmp = strchr(optArg, ':');
                if (tmp != NULL)
                {
                    tmp++;
                    switch (optInFormat)
                    {
                        case FFMT_SPRITE:
                        case FFMT_CHAR:
                            if (strcasecmp(tmp, "mc") == 0)
                                optInMulticolor = TRUE;
                            else
                            if (strcasecmp(tmp, "sc") == 0)
                                optInMulticolor = FALSE;
                            else
                            {
                                dmErrorMsg("Invalid input subformat for sprite/char: '%s', should be 'mc' or 'sc'.\n",
                                    tmp);
                                return FALSE;
                            }
                            break;

                        case FFMT_BITMAP:
                            if (!dmGetC64FormatByExt(tmp, &optInFormat, &optInSubFormat))
                            {
                                dmErrorMsg("Invalid bitmap subformat '%s', see format list for valid bformats.\n",
                                    tmp);
                                return FALSE;
                            }
                            break;
                    }
                }
            }
            break;

        case 2:
            optInMulticolor = TRUE;
            break;

        case 3:
            optOutFilename = optArg;
            break;

        case 4:
            if (!dmGetIntVal(optArg, &optInSkip))
            {
                dmErrorMsg("Invalid skip value argument '%s'.\n", optArg);
                return FALSE;
            }
            break;

        case 5:
            if (!dmGetFormatByExt(optArg, &optOutFormat, &optOutSubFormat) &&
                !dmGetC64FormatByExt(optArg, &optOutFormat, &optOutSubFormat))
            {
                dmErrorMsg("Invalid output format '%s'.\n", optArg);
                return FALSE;
            }
            break;

        case 6:
            {
                int index, ncolors;
                if (!dmParseMapOptionString(optArg, optColors,
                    &ncolors, C64_MAX_COLORS, FALSE, "color table option"))
                    return FALSE;

                dmMsg(1, "Set color table: ");
                for (index = 0; index < ncolors; index++)
                {
                    dmPrint(1, "[%d:%d]%s",
                        index, optColors[index],
                        (index < ncolors) ? ", " : "");
                }
                dmPrint(1, "\n");
            }
            break;

        case 7:
            if (sscanf(optArg, "%d", &optItemCount) != 1)
            {
                dmErrorMsg("Invalid count value argument '%s'.\n", optArg);
                return FALSE;
            }
            break;

        case 8:
            optSequential = TRUE;
            break;

        case 9:
            if (sscanf(optArg, "%d:%d", &optSpec.scaleX, &optSpec.scaleY) != 2)
            {
                if (sscanf(optArg, "%d", &optSpec.scaleX) == 1)
                    optSpec.scaleY = optSpec.scaleX;
                else
                {
                    dmErrorMsg("Invalid scale option value '%s', should be <n> or <w>:<h>.\n", optArg);
                    return FALSE;
                }
            }
            if (optSpec.scaleX < 1 || optSpec.scaleX > 50)
            {
                dmErrorMsg("Invalid X scale value '%d'.\n", optSpec.scaleX);
                return FALSE;
            }
            if (optSpec.scaleY < 1 || optSpec.scaleY > 50)
            {
                dmErrorMsg("Invalid Y scale value '%d'.\n", optSpec.scaleY);
                return FALSE;
            }
            break;

        case 11:
            if (sscanf(optArg, "%d", &optPlanedWidth) != 1)
            {
                dmErrorMsg("Invalid planed width value argument '%s'.\n", optArg);
                return FALSE;
            }
            if (optPlanedWidth < 1 || optPlanedWidth > 512)
            {
                dmErrorMsg("Invalid planed width value '%d' [1..512].\n", optPlanedWidth);
                return FALSE;
            }
            break;

        case 12:
            optSpec.paletted = TRUE;
            break;

        case 13:
            {
                int tmp = atoi(optArg);
                if (tmp < 1 || tmp > 8)
                {
                    dmErrorMsg("Invalid number of bitplanes value '%s'.\n", optArg);
                    return FALSE;
                }
                optSpec.nplanes = tmp;
            }
            break;

        case 18:
            {
                int tmp = atoi(optArg);
                if (tmp < 1 || tmp > 32)
                {
                    dmErrorMsg("Invalid number of bits per pixel value '%s'.\n", optArg);
                    return FALSE;
                }
                optSpec.nplanes = tmp;
            }
            break;

        case 14:
            optSpec.planar = TRUE;
            break;

        case 16:
            {
                char *tmp;
                if ((tmp = dm_strrcasecmp(optArg, "+remove")) != NULL)
                {
                    optRemapRemove = TRUE;
                    *tmp = 0;
                }

                if (optArg[0] == '@')
                {
                    if (optArg[1] != 0)
                    {
                        int res;
                        if ((res = dmParseColorRemapFile(optArg + 1,
                            optRemapTable, &optNRemapTable, DM_MAX_COLORS)) != DMERR_OK)
                            return FALSE;
                    }
                    else
                    {
                        dmErrorMsg("No remap filename given.\n");
                        return FALSE;
                    }
                }
                else
                {
                    if (!dmParseMapOptionString(optArg, optRemapTable,
                        &optNRemapTable, DM_MAX_COLORS, TRUE, "color remap option"))
                        return FALSE;
                }
            }

            optRemapColors = TRUE;
            break;

        case 19:
            {
                int tx0, ty0, tx1, ty1;
                if (strcasecmp(optArg, "auto") == 0)
                {
                    optCropMode = CROP_AUTO;
                }
                else
                if (sscanf(optArg, "%d:%d-%d:%d", &tx0, &ty0, &tx1, &ty1) == 4)
                {
                    optCropMode = CROP_SIZE;
                    optCropX0   = tx0;
                    optCropY0   = ty0;
                    optCropW    = tx1 - tx0 + 1;
                    optCropH    = ty1 - ty0 + 1;
                }
                else
                if (sscanf(optArg, "%d:%d:%d:%d", &tx0, &ty0, &tx1, &ty1) == 4)
                {
                    optCropMode = CROP_SIZE;
                    optCropX0   = tx0;
                    optCropY0   = ty0;
                    optCropW    = tx1;
                    optCropH    = ty1;
                }
                else
                {
                    dmErrorMsg("Invalid crop mode / argument '%s'.\n", optArg);
                    return FALSE;
                }
            }
            break;

        default:
            dmErrorMsg("Unknown option '%s'.\n", currArg);
            return FALSE;
    }

    return TRUE;
}


BOOL argHandleFile(char *currArg)
{
    if (!optInFilename)
        optInFilename = currArg;
    else
    {
        dmErrorMsg("Source filename already specified, extraneous argument '%s'.\n",
             currArg);
        return FALSE;
    }

    return TRUE;
}


void dmPrintByte(FILE *out, int byte, int format, BOOL multicolor)
{
    int i;

    if (multicolor)
    {
        for (i = DM_ASC_NBITS; i; i -= 2)
        {
            int val = (byte & (3ULL << (i - 2))) >> (i - 2);
            char ch;
            switch (format)
            {
                case FFMT_ASCII:
                    ch = dmASCIIPalette[val];
                    fprintf(out, "%c%c", ch, ch);
                    break;
                case FFMT_ANSI:
                    fprintf(out, "%c[0;%d;%dm##%c[0m",
                        0x1b,
                        1,
                        31 + optColors[val],
                        0x1b);
                    break;
            }
        }
    }
    else
    {
        for (i = DM_ASC_NBITS; i; i--)
        {
            int val = (byte & (1ULL << (i - 1))) >> (i - 1);
            char ch;
            switch (format)
            {
                case FFMT_ASCII:
                    ch = val ? '#' : '.';
                    fputc(ch, out);
                    break;
                case FFMT_ANSI:
                    fprintf(out, "%c[0;%d;%dm %c[0m",
                        0x1b,
                        1,
                        31 + optColors[val],
                        0x1b);
                    break;
            }
        }
    }
}


void dmDumpCharASCII(FILE *outFile, const Uint8 *buf, const size_t offs, const int fmt, const BOOL multicolor)
{
    for (size_t yc = 0; yc < C64_CHR_HEIGHT; yc++)
    {
        fprintf(outFile, "%04" DM_PRIx_SIZE_T " : ", offs + yc);
        dmPrintByte(outFile, buf[yc], fmt, multicolor);
        fprintf(outFile, "\n");
    }
}


void dmDumpSpriteASCII(FILE *outFile, const Uint8 *buf, const size_t offs, const int fmt, BOOL multicolor)
{
    size_t bufOffs, xc, yc;

    for (bufOffs = yc = 0; yc < C64_SPR_HEIGHT; yc++)
    {
        fprintf(outFile, "%04" DM_PRIx_SIZE_T " ", offs + bufOffs);
        for (xc = 0; xc < C64_SPR_WIDTH; xc++)
        {
            dmPrintByte(outFile, buf[bufOffs], fmt, multicolor);
            fprintf(outFile, " ");
            bufOffs++;
        }
        fprintf(outFile, "\n");
    }
}


int dmRemapImageColors(DMImage **pdst, const DMImage *src)
{
    DMColor *npal = dmCalloc(src->ncolors, sizeof(DMColor));
    int  *mapping = dmMalloc(src->ncolors * sizeof(int));
    BOOL *mapped  = dmMalloc(src->ncolors * sizeof(BOOL));
    BOOL *used    = dmMalloc(src->ncolors * sizeof(BOOL));
    int n, index, xc, yc, ncolors;
    DMImage *dst;

    if ((dst = *pdst = dmImageAlloc(src->width, src->height, src->format, src->bpp)) == NULL)
    {
        return dmError(DMERR_MALLOC,
            "Could not allocate memory for remapped image.\n");
    }

    dmMsg(1, "Remapping %d output image colors of %d colors.\n", optNRemapTable, src->ncolors);

    if (npal == NULL || mapping == NULL || mapped == NULL || used == NULL)
    {
        return dmError(DMERR_MALLOC,
            "Could not allocate memory for reused palette.\n");
    }

    for (index = 0; index < src->ncolors; index++)
    {
        mapping[index] = -1;
        mapped[index] = used[index] = FALSE;
    }

    // Find used colors
    dmMsg(2, "Scanning image for used colors...\n");
    for (ncolors = yc = 0; yc < src->height; yc++)
    {
        const Uint8 *dp = src->data + src->pitch * yc;
        for (xc = 0; xc < src->width; xc++)
        {
            Uint8 col = dp[xc];
            if (col < src->ncolors && !used[col])
            {
                used[col] = TRUE;
                ncolors++;
            }
        }
    }
    dmMsg(2, "Found %d used colors, creating remap-table.\n", ncolors);

    // Match and mark mapped colors
    for (index = 0; index < optNRemapTable; index++)
    {
        DMMapValue *map = &optRemapTable[index];
        if (map->triplet)
        {
            BOOL found = FALSE;
            for (n = 0; n < src->ncolors; n++)
            {
                if (dmCompareColor(&(src->pal[n]), &(map->color), map->alpha))
                {
                    dmMsg(3, "RGBA match #%02x%02x%02x%02x: %d -> %d\n",
                        map->color.r, map->color.g, map->color.b, map->color.a,
                        n,
                        map->to);

                    mapping[n] = map->to;
                    mapped[map->to] = TRUE;
                    found = TRUE;
                }
            }

            if (!found)
            {
                dmMsg(3, "No RGBA match found for map index %d, #%02x%02x%02x%02x\n",
                    index,
                    map->color.r, map->color.g, map->color.b, map->color.a);
            }
        }
        else
        {
            dmMsg(3, "Map index: %d -> %d\n",
                map->from, map->to);

            mapping[map->from] = map->to;
            mapped[map->to] = TRUE;
        }
    }


    // Fill in the rest
    if (optRemapRemove)
    {
        dmMsg(2, "Removing unused colors.\n");
        for (index = 0; index < src->ncolors; index++)
        if (mapping[index] < 0 && used[index])
        {
            for (n = 0; n < src->ncolors; n++)
            if (!mapped[n])
            {
                mapping[index] = n;
                mapped[n] = TRUE;
                break;
            }
        }
    }
    else
    {
        for (index = 0; index < src->ncolors; index++)
        if (mapping[index] < 0)
        {
            for (n = 0; n < src->ncolors; n++)
            if (!mapped[n])
            {
                mapping[index] = n;
                mapped[n] = TRUE;
                break;
            }
        }
    }

    // Calculate final number of palette colors
    ncolors = 0;
    for (index = 0; index < src->ncolors; index++)
    {
        if (mapping[index] + 1 > ncolors)
            ncolors = mapping[index] + 1;
    }

    // Copy palette entries
    for (index = 0; index < src->ncolors; index++)
    {
        if (mapping[index] >= 0)
        {
            memcpy(&npal[mapping[index]], &(src->pal[index]), sizeof(DMColor));
        }
    }

    // Remap image
    dmMsg(1, "Remapping image to %d colors...\n", ncolors);
    for (yc = 0; yc < src->height; yc++)
    {
        Uint8 *sp = src->data + src->pitch * yc;
        Uint8 *dp = dst->data + dst->pitch * yc;
        for (xc = 0; xc < src->width; xc++)
        {
            Uint8 col = sp[xc];
            if (col < src->ncolors && mapping[col] >= 0 && mapping[col] < src->ncolors)
                dp[xc] = mapping[col];
            else
                dp[xc] = 0;
        }
    }

    // Set new palette, free memory
    dst->pal = npal;
    dst->ncolors = ncolors;

    dmFree(mapping);
    dmFree(mapped);
    dmFree(used);
    return DMERR_OK;
}


int dmConvertC64Bitmap(DMC64Image **pdst, const DMC64Image *src, const DMC64ImageFormat *fmt)
{
    DMC64Image *dst;

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

    if ((dst = *pdst = dmC64ImageAlloc(fmt)) == NULL)
        return DMERR_MALLOC;

    if (src->type == dst->type)
    {
        for (int i = 0; i < dst->nbanks; i++)
        {
            memcpy(dst->color[i], src->color[i], dst->screenSize);
            memcpy(dst->bitmap[i], src->bitmap[i], dst->bitmapSize);
            memcpy(dst->screen[i], src->screen[i], dst->screenSize);
            memcpy(dst->charmem[i], src->charmem[i], dst->charmemSize);
        }
    }
    else
    {
        // Try to do some simple fixups
        if ((dst->type & D64_FMT_FLI) && (src->type & D64_FMT_FLI) == 0)
        {
            dmMsg(1, "Upconverting multicolor to FLI.\n");
            for (int i = 0; i < dst->nbanks; i++)
            {
                memcpy(dst->color[i], src->color[0], dst->screenSize);
                memcpy(dst->screen[i], src->screen[0], dst->screenSize);
                memcpy(dst->bitmap[i], src->bitmap[0], dst->bitmapSize);
                memcpy(dst->charmem[i], src->charmem[0], dst->charmemSize);
            }

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

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

                // Check size
                if (!dmC64GetOpSize(op, fmt, &size))
                    return DMERR_INVALID_DATA;

                // Perform operation
                switch (op->type)
                {
                    case DT_EXTRA_DATA:
                        dst->extraData[op->bank] = dmMalloc0(size);
                        dst->extraDataSizes[op->bank] = size;
                        break;
                }
            }
        }
    }

    return DMERR_OK;
}


int dmWriteBitmap(const char *filename, const DMC64Image *image, const DMC64ImageFormat *fmt)
{
    int res = DMERR_OK;
    DMGrowBuf buf;

    // Encode to target format
    dmMsg(1, "Encoding C64 bitmap data to format '%s'\n", fmt->name);
    if ((res = dmC64EncodeBMP(&buf, image, fmt)) != DMERR_OK)
        goto error;

    // And output the file
    dmMsg(1, "Writing output file '%s'\n", filename);
    if ((res = dmWriteDataFile(NULL, filename, buf.data, buf.len)) != DMERR_OK)
        goto error;

error:
    dmGrowBufFree(&buf);
    return res;
}


void dmOutputImageBitFormat(const int format, const BOOL info)
{
    if (info)
    {
        char *str;
        switch (format)
        {
            case DM_IFMT_PALETTE : str = "Indexed 8bpp"; break;
            case DM_IFMT_RGB     : str = "24bit RGB"; break;
            case DM_IFMT_RGBA    : str = "32bit RGBA"; break;
            default              : str = "???"; break;
        }
        dmMsg(2, "%s output.\n", str);
    }
}


int dmWriteImage(const char *filename, DMImage *pimage, DMImageConvSpec *spec, int iformat, BOOL info)
{
    int res = DMERR_OK;

    if (info)
    {
        dmMsg(1, "Outputting %s image %d x %d -> %d x %d [%d x %d]\n",
            dmImageFormatList[iformat].fext,
            pimage->width, pimage->height,
            pimage->width * spec->scaleX, pimage->height * spec->scaleY,
            spec->scaleX, spec->scaleY);
    }

    // Perform color remapping
    DMImage *image = pimage;
    BOOL allocated = FALSE;
    if (optRemapColors)
    {
        int res;
        if ((res = dmRemapImageColors(&image, pimage)) != DMERR_OK)
            return res;

        allocated = TRUE;
    }

    switch (iformat)
    {
#ifdef DM_USE_LIBPNG
        case DM_IMGFMT_PNG:
            spec->format = spec->paletted ? DM_IFMT_PALETTE : DM_IFMT_RGBA;
            dmOutputImageBitFormat(spec->format, info);
            res = dmWritePNGImage(filename, image, spec);
            break;
#endif

        case DM_IMGFMT_PPM:
            spec->format = DM_IFMT_RGB;
            dmOutputImageBitFormat(spec->format, info);
            res = dmWritePPMImage(filename, image, spec);
            break;

        case DM_IMGFMT_PCX:
            spec->format = spec->paletted ? DM_IFMT_PALETTE : DM_IFMT_RGB;
            dmOutputImageBitFormat(spec->format, info);
            res = dmWritePCXImage(filename, image, spec);
            break;

        case DM_IMGFMT_RAW:
        case DM_IMGFMT_ARAW:
            {
                // Open data file for writing
                FILE *fp;
                char * dataFilename = dm_strdup_fext(filename, "%s.inc");
                if ((fp = fopen(dataFilename, "w")) == NULL)
                {
                    res = dmError(DMERR_FOPEN,
                        "Could not create '%s'.\n", dataFilename);
                    goto err;
                }

                dmFree(dataFilename);

                if (fp != NULL)
                {
                    // Strip extension
                    char *palID = dm_strdup_fext(filename, "img_%s");

                    // Replace any non-alphanumerics
                    for (int i = 0; palID[i]; i++)
                    {
                        if (isalnum(palID[i]))
                            palID[i] = tolower(palID[i]);
                        else
                            palID[i] = '_';
                    }

                    if (iformat == DM_IMGFMT_ARAW)
                    {
                        fprintf(fp,
                        "%s_width: dw.w %d\n"
                        "%s_height: dw.w %d\n"
                        "%s_nplanes: dw.w %d\n"
                        "%s_ncolors: dw.w %d\n"
                        "%s_palette:\n",
                        palID, image->width,
                        palID, image->height,
                        palID, spec->nplanes,
                        palID, image->ncolors,
                        palID);

                        dmWriteIFFMasterRAWPalette(fp, image, 1 << optSpec.nplanes, NULL, NULL);

                        fprintf(fp,
                        "%s: incbin \"%s\"\n",
                        palID, filename);
                    }
                    else
                    {
                        fprintf(fp,
                        "%s_width: dw.w %d\n"
                        "%s_height: dw.w %d\n"
                        "%s_nplanes: dw.w %d\n",
                        palID, image->width,
                        palID, image->height,
                        palID, spec->nplanes);
                    }

                    fclose(fp);
                    dmFree(palID);
                }

                if (info)
                {
                    dmMsg(2, "%d bitplanes, %s planes.\n",
                        spec->nplanes,
                        spec->planar ? "planar/interleaved" : "non-interleaved");
                }
                res = dmWriteRAWImage(filename, image, spec);
            }
            break;

        default:
            res = DMERR_INVALID_DATA;
    }

err:
    if (allocated)
        dmImageFree(image);

    return res;
}


static Uint8 dmConvertByte(const Uint8 *sp, const BOOL multicolor)
{
    Uint8 byte = 0;
    int xc;

    if (multicolor)
    {
        for (xc = 0; xc < 8 / 2; xc++)
        {
            Uint8 pixel = sp[xc * 2] & 3;
            byte |= pixel << (6 - (xc * 2));
        }
    }
    else
    {
        for (xc = 0; xc < 8; xc++)
        {
            Uint8 pixel = sp[xc] == 0 ? 0 : 1;
            byte |= pixel << (7 - xc);
        }
    }

    return byte;
}


BOOL dmConvertImage2Char(Uint8 *buf, const DMImage *image,
    const int xoffs, const int yoffs, const BOOL multicolor)
{
    int yc;

    if (xoffs < 0 || yoffs < 0 ||
        xoffs + C64_CHR_WIDTH_PX > image->width ||
        yoffs + C64_CHR_HEIGHT > image->height)
        return FALSE;

    for (yc = 0; yc < C64_CHR_HEIGHT; yc++)
    {
        const Uint8 *sp = image->data + ((yc + yoffs) * image->pitch) + xoffs;
        buf[yc] = dmConvertByte(sp, multicolor);
    }

    return TRUE;
}


BOOL dmConvertImage2Sprite(Uint8 *buf, const DMImage *image,
    const int xoffs, const int yoffs, const BOOL multicolor)
{
    int yc, xc;

    if (xoffs < 0 || yoffs < 0 ||
        xoffs + C64_SPR_WIDTH_PX > image->width ||
        yoffs + C64_SPR_HEIGHT > image->height)
        return FALSE;

    for (yc = 0; yc < C64_SPR_HEIGHT; yc++)
    {
        for (xc = 0; xc < C64_SPR_WIDTH_PX / C64_SPR_WIDTH; xc++)
        {
            const Uint8 *sp = image->data + ((yc + yoffs) * image->pitch) + (xc * 8) + xoffs;
            buf[(yc * C64_SPR_WIDTH) + xc] = dmConvertByte(sp, multicolor);
        }
    }

    return TRUE;
}


int dmWriteSpritesAndChars(const char *filename, DMImage *image, int outFormat, BOOL multicolor)
{
    int ret = DMERR_OK;
    int outBlockW, outBlockH, bx, by;
    FILE *outFile = NULL;
    Uint8 *buf = NULL;
    size_t outBufSize;
    char *outType;

    switch (outFormat)
    {
        case FFMT_CHAR:
            outBufSize = C64_CHR_SIZE;
            outBlockW = image->width / C64_CHR_WIDTH_PX;
            outBlockH = image->height / C64_CHR_HEIGHT;
            outType = "char";
            break;

        case FFMT_SPRITE:
            outBufSize = C64_SPR_SIZE;
            outBlockW = image->width / C64_SPR_WIDTH_PX;
            outBlockH = image->height / C64_SPR_HEIGHT;
            outType = "sprite";
            break;

        default:
            ret = dmError(DMERR_INVALID_ARGS,
                "Invalid output format %d, internal error.\n", outFormat);
            goto error;
    }

    if (outBlockW < 1 || outBlockH < 1)
    {
        ret = dmError(DMERR_INVALID_ARGS,
            "Source image dimensions too small for conversion, block dimensions %d x %d.\n",
            outBlockW, outBlockH);
        goto error;
    }

    if ((outFile = fopen(filename, "wb")) == NULL)
    {
        ret = dmGetErrno();
        dmErrorMsg("Could not open '%s' for writing, %d: %s.\n",
            filename, ret, dmErrorStr(ret));
        goto error;
    }

    if ((buf = dmMalloc(outBufSize)) == NULL)
    {
        dmErrorMsg("Could not allocate %d bytes for conversion buffer.\n",
            outBufSize);
        goto error;
    }

    dmMsg(1, "Writing %d x %d = %d blocks of %s data...\n",
        outBlockW, outBlockH, outBlockW * outBlockH, outType);

    for (by = 0; by < outBlockH; by++)
    for (bx = 0; bx < outBlockW; bx++)
    {
        switch (outFormat)
        {
            case FFMT_CHAR:
                if (!dmConvertImage2Char(buf, image,
                    bx * C64_CHR_WIDTH_PX, by * C64_CHR_HEIGHT,
                    multicolor))
                {
                    ret = DMERR_DATA_ERROR;
                    goto error;
                }
                break;

            case FFMT_SPRITE:
                if (!dmConvertImage2Sprite(buf, image,
                    bx * C64_SPR_WIDTH_PX, by * C64_SPR_HEIGHT,
                    multicolor))
                {
                    ret = DMERR_DATA_ERROR;
                    goto error;
                }
                break;
        }

        if (!dm_fwrite_str(outFile, buf, outBufSize))
        {
            ret = dmGetErrno();
            dmError(ret, "Error writing data block %d,%d to '%s', %d: %s\n",
                bx, by, filename, ret, dmErrorStr(ret));
            goto error;
        }
    }

    fclose(outFile);
    dmFree(buf);
    return 0;

error:
    if (outFile != NULL)
        fclose(outFile);
    dmFree(buf);
    return ret;
}


int dmDumpSpritesAndChars(const Uint8 *dataBuf, const size_t dataSize, const size_t realOffs)
{
    int ret = DMERR_OK, itemCount, outWidth, outWidthPX, outHeight;
    size_t offs, outSize;

    switch (optInFormat)
    {
        case FFMT_CHAR:
            outSize    = C64_CHR_SIZE;
            outWidth   = C64_CHR_WIDTH;
            outWidthPX = C64_CHR_WIDTH_PX;
            outHeight  = C64_CHR_HEIGHT;
            break;

        case FFMT_SPRITE:
            outSize    = C64_SPR_SIZE;
            outWidth   = C64_SPR_WIDTH;
            outWidthPX = C64_SPR_WIDTH_PX;
            outHeight  = C64_SPR_HEIGHT;
            break;

        default:
            return dmError(DMERR_INTERNAL,
                "Invalid input format %d, internal error.\n", optInFormat);
    }

    offs = 0;
    itemCount = 0;

    if (optOutFormat == FFMT_ANSI || optOutFormat == FFMT_ASCII)
    {
        BOOL error = FALSE;
        FILE *outFile;

        if (optOutFilename == NULL)
            outFile = stdout;
        else
        if ((outFile = fopen(optOutFilename, "w")) == NULL)
        {
            ret = dmGetErrno();
            dmError(ret, "Error opening output file '%s': %s\n",
                  optOutFilename, dmErrorStr(ret));
            goto error;
        }

        while (offs + outSize < dataSize && !error && (optItemCount < 0 || itemCount < optItemCount))
        {
            fprintf(outFile, "---- : -------------- #%d\n", itemCount);

            switch (optInFormat)
            {
                case FFMT_CHAR:
                    dmDumpCharASCII(outFile, dataBuf + offs, realOffs + offs, optOutFormat, optInMulticolor);
                    break;
                case FFMT_SPRITE:
                    dmDumpSpriteASCII(outFile, dataBuf + offs, realOffs + offs, optOutFormat, optInMulticolor);
                    break;
            }
            offs += outSize;
            itemCount++;
        }

        fclose(outFile);
    }
    else
    if (optOutFormat == FFMT_IMAGE)
    {
        DMImage *outImage = NULL;
        char *outFilename = NULL;
        int outX = 0, outY = 0, err;

        if (optSequential)
        {
            if (optOutFilename == NULL)
            {
                dmErrorMsg("Sequential image output requires filename template.\n");
                goto error;
            }

            outImage = dmImageAlloc(outWidthPX, outHeight, DM_IFMT_PALETTE, -1);
            dmMsg(1, "Outputting sequence of %d images @ %d x %d -> %d x %d.\n",
                optItemCount,
                outImage->width, outImage->height,
                outImage->width * optSpec.scaleX, outImage->height * optSpec.scaleY);
        }
        else
        {
            int outIWidth, outIHeight;
            if (optItemCount <= 0)
            {
                dmErrorMsg("Single-image output requires count to be set (-n).\n");
                goto error;
            }

            outIWidth = optPlanedWidth;
            outIHeight = (optItemCount / optPlanedWidth);
            if (optItemCount % optPlanedWidth)
                outIHeight++;

            outImage = dmImageAlloc(outWidthPX * outIWidth, outIHeight * outHeight, DM_IFMT_PALETTE, -1);
        }

        dmSetDefaultC64Palette(outImage);

        while (offs + outSize < dataSize && (optItemCount < 0 || itemCount < optItemCount))
        {
            if ((err = dmC64ConvertCSDataToImage(outImage, outX * outWidthPX, outY * outHeight,
                dataBuf + offs, outWidth, outHeight, optInMulticolor, optColors)) != DMERR_OK)
            {
                dmErrorMsg("Internal error in conversion of raw data to bitmap: %d.\n", err);
                break;
            }

            if (optSequential)
            {
                outFilename = dm_strdup_printf("%s%04d.%s",
                    optOutFilename,
                    itemCount,
                    convFormatList[optOutFormat].fext);

                if (outFilename == NULL)
                {
                    dmErrorMsg("Could not allocate memory for filename template?\n");
                    goto error;
                }

                ret = dmWriteImage(outFilename, outImage, &optSpec, optOutSubFormat, TRUE);
                if (ret != DMERR_OK)
                {
                    dmErrorMsg("Error writing output image '%s': %s.\n",
                        outFilename, dmErrorStr(ret));
                }

                dmFree(outFilename);
            }
            else
            {
                if (++outX >= optPlanedWidth)
                {
                    outX = 0;
                    outY++;
                }
            }

            offs += outSize;
            itemCount++;
        }

        if (!optSequential)
        {
            ret = dmWriteImage(optOutFilename, outImage, &optSpec, optOutSubFormat, TRUE);
            if (ret != DMERR_OK)
            {
                dmError(ret, "Error writing output image '%s': %s.\n",
                    optOutFilename, dmErrorStr(ret));
            }
        }

        dmImageFree(outImage);
    }
    else
    if (optOutFormat == FFMT_BITMAP)
    {
        if (optSequential)
        {
            ret = dmError(DMERR_INVALID_ARGS,
                "Sequential output not supported for spr/char -> bitmap conversion.\n");
            goto error;
        }
    }

error:
    return ret;
}


int main(int argc, char *argv[])
{
    FILE *inFile = NULL;
    const DMC64ImageFormat *inC64Fmt = NULL;
    DMC64Image *inC64Image = NULL, *outC64Image = NULL;
    Uint8 *dataBuf = NULL, *dataBufOrig = NULL;
    size_t dataSize, dataSizeOrig;
    int i;

    // Default colors
    for (i = 0; i < C64_MAX_COLORS; i++)
        optColors[i] = i;

    // Initialize and parse commandline
    dmInitProg("gfxconv", "Simple graphics converter", "0.91", NULL, NULL);

    if (!dmArgsProcess(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, OPTH_BAILOUT))
        exit(1);

#ifndef DM_USE_LIBPNG
    if (optOutFormat == DM_IMGFMT_PNG)
    {
        dmErrorMsg("PNG output format support not compiled in, sorry.\n");
        goto error;
    }
#endif

    // Determine input format, if not specified'
    if (optInFormat == FFMT_AUTO && optInFilename != NULL)
    {
        char *dext = strrchr(optInFilename, '.');
        dmMsg(4, "Trying to determine file format by extension.\n");
        if (dext)
        {
            if (!dmGetFormatByExt(dext + 1, &optInFormat, &optInSubFormat))
                dmGetC64FormatByExt(dext + 1, &optInFormat, &optInSubFormat);
        }
    }

    if (optInFilename == NULL)
    {
        if (optInFormat == FFMT_AUTO)
        {
            dmErrorMsg("Standard input cannot be used without specifying input format.\n");
            dmErrorMsg("Perhaps you should try using --help\n");
            goto error;
        }
        inFile = stdin;
        optInFilename = "stdin";
    }
    else
    if ((inFile = fopen(optInFilename, "rb")) == NULL)
    {
        int res = dmGetErrno();
        dmErrorMsg("Error opening input file '%s', %d: %s\n",
              optInFilename, res, dmErrorStr(res));
        goto error;
    }

    // Read the input ..
    dmMsg(1, "Reading input from '%s'.\n", optInFilename);

    if (dmReadDataFile(inFile, NULL, &dataBufOrig, &dataSizeOrig) != 0)
        goto error;

    // Check and compute the input skip
    if (optInSkip > dataSizeOrig)
    {
        dmErrorMsg("Input skip value %d is larger than input size %d.\n",
            optInSkip, dataSizeOrig);
        goto error;
    }

    dataBuf = dataBufOrig + optInSkip;
    dataSize = dataSizeOrig - optInSkip;


    // Perform probing, if required
    if (optInFormat == FFMT_AUTO || optInFormat == FFMT_BITMAP)
    {
        // Probe for format
        const DMC64ImageFormat *forced = NULL;
        int res;

        if (optForcedFormat >= 0)
        {
            forced = &dmC64ImageFormats[optForcedFormat];
            dmMsg(0, "Forced %s format image, type %d, %s\n",
                forced->name, forced->type, forced->fext);
        }

        res = dmC64DecodeBMP(&inC64Image, dataBuf, dataSize, optInSkip, optInSkip + 2, &inC64Fmt, forced);
        if (forced == NULL && inC64Fmt != NULL)
        {
            dmMsg(1, "Probed '%s' format image, type %d, %s\n",
                inC64Fmt->name, inC64Fmt->type, inC64Fmt->fext);
        }

        if (res == DMERR_OK)
            optInFormat = FFMT_BITMAP;
        else
        {
            dmErrorMsg("Could not decode input image: %s.\n", dmErrorStr(res));
            goto error;
        }
    }

    if (optInFormat == FFMT_AUTO || optInFormat == FFMT_IMAGE)
    {
        DMImageFormat *ifmt = NULL;
        int index;
        dmMsg(4, "Trying to probe image formats.\n");
        if (dmImageProbeGeneric(dataBuf + optInSkip, dataSize - optInSkip, &ifmt, &index) > 0)
        {
            optInFormat = FFMT_IMAGE;
            optInSubFormat = index;
            dmMsg(1, "Probed '%s' format image.\n", ifmt->fext);
        }
    }

    if (optInFormat == FFMT_AUTO)
    {
        dmErrorMsg("No input format specified, and could not be determined automatically.\n");
        goto error;
    }

    int inFormat = dmGetConvFormat(optInFormat, optInSubFormat),
        outFormat = dmGetConvFormat(optOutFormat, optOutSubFormat);

    if (inFormat != -1 && outFormat != -1)
    {
        char *inFmtName = convFormatList[inFormat].name,
             *inFmtExt = convFormatList[inFormat].fext,
             *outFmtName = convFormatList[outFormat].name,
             *outFmtExt = convFormatList[outFormat].fext;

        if (optInFormat == FFMT_BITMAP)
            inFmtExt = inC64Fmt->name;

        dmMsg(1, "Attempting conversion %s (%s) -> %s (%s)\n",
            inFmtName, inFmtExt, outFmtName, outFmtExt);
    }

    switch (optInFormat)
    {
        case FFMT_SPRITE:
        case FFMT_CHAR:
            dmDumpSpritesAndChars(dataBuf, dataSize, optInSkip);
            break;

        case FFMT_BITMAP:
            {
                DMImage *outImage = NULL;
                int res = DMERR_OK;

                if (optOutFilename == NULL)
                {
                    dmErrorMsg("Output filename not set, required for bitmap formats.\n");
                    goto error;
                }

                switch (optOutFormat)
                {
                    case FFMT_IMAGE:
                        res = dmC64ConvertBMP2Image(&outImage, inC64Image, inC64Fmt);

                        if (res != DMERR_OK || outImage == NULL)
                        {
                            dmErrorMsg("Error in bitmap to image conversion.\n");
                            goto error;
                        }

                        dmSetDefaultC64Palette(outImage);
                        res = dmWriteImage(optOutFilename, outImage, &optSpec, optOutSubFormat, TRUE);
                        break;

                    case FFMT_BITMAP:
                        if ((res = dmConvertC64Bitmap(&outC64Image, inC64Image, &dmC64ImageFormats[optOutSubFormat])) != DMERR_OK)
                        {
                            dmErrorMsg("Error in bitmap format conversion.\n");
                            goto error;
                        }
                        res = dmWriteBitmap(optOutFilename, outC64Image, &dmC64ImageFormats[optOutSubFormat]);
                        break;

                    case FFMT_CHAR:
                    case FFMT_SPRITE:
                        res = dmC64ConvertBMP2Image(&outImage, inC64Image, inC64Fmt);

                        if (res != DMERR_OK || outImage == NULL)
                        {
                            dmErrorMsg("Error in bitmap to template image conversion.\n");
                            goto error;
                        }

                        dmSetDefaultC64Palette(outImage);
                        res = dmWriteSpritesAndChars(optOutFilename, outImage, optOutFormat, optInMulticolor);
                        break;

                    default:
                        dmErrorMsg("Unsupported output format for bitmap/image conversion.\n");
                        break;
                }

                dmImageFree(outImage);
            }
            break;

        case FFMT_IMAGE:
            {
                DMImage *inImage = NULL;
                int res = DMERR_OK;

                if (optOutFilename == NULL)
                {
                    dmErrorMsg("Output filename not set, required for image formats.\n");
                    goto error;
                }

                // Read input
                DMImageFormat *ifmt = &dmImageFormatList[optInSubFormat];
                if (ifmt->readFILE != NULL)
                    res = ifmt->readFILE(inFile, &inImage);
                else
                    dmErrorMsg("Unsupported input image format for bitmap/image conversion.\n");

                if (res != DMERR_OK || inImage == NULL)
                    break;

                switch (optOutFormat)
                {
                    case FFMT_IMAGE:
                        res = dmWriteImage(optOutFilename, inImage, &optSpec, optOutSubFormat, TRUE);
                        break;

                    case FFMT_CHAR:
                    case FFMT_SPRITE:
                        res = dmWriteSpritesAndChars(optOutFilename, inImage, optOutFormat, optInMulticolor);
                        break;

                    default:
                        dmErrorMsg("Unsupported output format for bitmap/image conversion.\n");
                        break;
                }

                if (res != DMERR_OK)
                {
                    dmErrorMsg("Error writing output (%s), probably unsupported output format for bitmap/image conversion.\n",
                        dmErrorStr(res));
                }

                dmImageFree(inImage);
            }
            break;
    }

error:
    if (inFile != NULL)
        fclose(inFile);

    dmFree(dataBufOrig);
    dmC64ImageFree(inC64Image);
    dmC64ImageFree(outC64Image);

    return 0;
}