view tools/gfxconv.c @ 2576:812b16ee49db

I had been living under apparent false impression that "realfft.c" on which the FFT implementation in DMLIB was basically copied from was released in public domain at some point, but it could very well be that it never was. Correct license is (or seems to be) GNU GPL. Thus I removing the code from DMLIB, and profusely apologize to the author, Philip Van Baren. It was never my intention to distribute code based on his original work under a more liberal license than originally intended.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 11 Mar 2022 16:32:50 +0200
parents bb44c48cffac
children 9807ae37ad69
line wrap: on
line source

/*
 * gfxconv - Convert various graphics formats
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2012-2022 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 "lib64util.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_BITMAP,
    FFMT_CHAR,
    FFMT_SPRITE,

    FFMT_IMAGE,
    FFMT_DUMP,
    FFMT_PALETTE,

    FFMT_LAST
};


enum
{
    FCMP_NONE = 0,
    FCMP_BEST = 9
};


static const char *formatTypeList[FFMT_LAST] =
{
    "AUTO",
    "ASCII text",
    "ANSI text",
    "C64 bitmap image",
    "C64 character/font data",
    "C64 sprite data",
    "Image",
    "CDump",
    "Palette",
};


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


enum
{
    SCALE_SET,
    SCALE_RELATIVE,
    SCALE_AUTO,
};


enum
{
    REMAP_NONE = 0,
    REMAP_AUTO,
    REMAP_MAPPED,
};


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


static const DMConvFormat baseFormatList[] =
{
    { "ASCII text"                           , "asc"   , DM_FMT_WR   , FFMT_ASCII   , 0  , NULL },
    { "ANSI colored text"                    , "ansi"  , DM_FMT_WR   , FFMT_ANSI    , 0  , NULL },
    { "C64 bitmap image"                     , "bitmap", DM_FMT_RDWR , FFMT_BITMAP  , -1 , NULL },
    { "C64 character/font data"              , "chr"   , DM_FMT_RDWR , FFMT_CHAR    , 0  , " (chr:mc/chr:sc for multi/singlecolor)" },
    { "C64 sprite data"                      , "spr"   , DM_FMT_RDWR , FFMT_SPRITE  , 0  , " (spr:mc/spr:sc for multi/singlecolor)" },
    { "C64 bitmap image dump"                , "dump"  , DM_FMT_WR   , FFMT_DUMP    , 0  , NULL },
    { "Palette data"                         , "pal"   , DM_FMT_RDWR , FFMT_PALETTE , -1 , NULL },
};

static const int nbaseFormatList = sizeof(baseFormatList) / sizeof(baseFormatList[0]);


static DMConvFormat *convFormatList = NULL;
static int nconvFormatList = 0;


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



char    *optInFilename = NULL,
        *optOutFilename = NULL;

int     optInType = FFMT_AUTO,
        optOutType = FFMT_AUTO,
        optInFormat = -1,
        optOutFormat = -1,
        optItemCount = -1,
        optPlanedWidth = 1,
        optForcedInSubFormat = -1,
        optShowHelp = 0;

unsigned int optInSkip = 0;
BOOL    optInSkipNeg = FALSE;

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

BOOL    optInMulticolor = FALSE,
        optSequential = FALSE,
        optRemapRemove = FALSE,
        optRemapMatchAlpha = FALSE,
        optUsePalette = FALSE;
int     optRemapMode = REMAP_NONE;
int     optNRemapTable = 0,
        optScaleMode = SCALE_AUTO;
float   optRemapMaxDist = -1;
int     optRemapNoMatchColor = -1;
DMMapValue optRemapTable[DM_MAX_COLORS];
int     optColorMap[D64_NCOLORS];
const char *optCharROMFilename = NULL;
DMC64Palette *optC64Palette = NULL;
char    *optPaletteFile = NULL;
DMPalette *optPaletteData = NULL;


DMImageWriteSpec optSpec =
{
    .scaleX = 1,
    .scaleY = 1,
    .nplanes = 0,
    .bpp = 8,
    .planar = FALSE,
    .pixfmt = 0,
    .compression = FCMP_BEST,
};

DMC64ImageConvSpec optC64Spec;


static const DMOptArg optList[] =
{
    {  0, '?', "help"            , "Show this help", OPT_NONE },
    {  3,   0, "longhelp"        , "Show a longer help", OPT_NONE },
    {  1,   0, "license"         , "Print out this program's license agreement", OPT_NONE },
    {  2, 'v', "verbose"         , "Be more verbose", OPT_NONE },

    { 10, 'o', "output"          , "Output filename", OPT_ARGREQ },
    { 12, 's', "skip"            , "Skip N bytes in input from start "
                                   "(a negative value will be offset "
                                   "from end of input data)", OPT_ARGREQ },
    { 14, 'i', "informat"        , "Set input format (see --formats)", OPT_ARGREQ },
    { 16, 'f', "format"          , "Set output format (see --formats)", OPT_ARGREQ },
    { 18, 'F', "formats"         , "List supported input/output formats", OPT_NONE },
    { 20, 'q', "sequential"      , "Output sequential files (image output only)", OPT_NONE },
    { 22, 'm', "colormap"        , "Set color map definitions (see '-m help')", OPT_ARGREQ },
    { 24, 'n', "numitems"        , "How many 'items' to output (default: all)", OPT_ARGREQ },
    { 26, 'w', "width"           , "Item width (number of items per row, min 1)", OPT_ARGREQ },
    { 28, 'S', "scale"           , "Scale output image (see '-S help')", OPT_ARGREQ },
    { 30, 'P', "paletted"        , "Use indexed/paletted output IF possible.", OPT_NONE },
    { 32, 'N', "nplanes"         , "# of bitplanes (some output formats)", OPT_ARGREQ },
    { 34, 'B', "bpp"             , "Bits per plane (some output formats)", OPT_ARGREQ },
    { 36, 'I', "interleave"      , "Interleaved/planar output (some output formats)", OPT_NONE },
    { 38, 'C', "compress"        , "Use compression -C <0-9>, 0 = disable, default is 9. "
                                   "(Not all formats support compression and the meaning "
                                   "apart from 0 or >= 1 depends on the format.)", OPT_ARGREQ },
    { 42, 'R', "remap"           , "Remap output image colors (see '-R help')", OPT_ARGREQ },
    { 44,   0, "char-rom"        , "Set character ROM file to be used.", OPT_ARGREQ },
    { 46, 'p', "palette"         , "Set palette to be used (see '-p help'). "
                                   "For paletted image file input, this will replace the "
                                   "image's original palette. Color remapping will not be "
                                   "done unless -R option is also specified.", 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("%-6s| %c%c | %s%s\n",
            fmt->fext ? fmt->fext : "",
            (fmt->flags & DM_FMT_RD) ? 'R' : ' ',
            (fmt->flags & DM_FMT_WR) ? 'W' : ' ',
            fmt->name,
            fmt->help != NULL ? fmt->help : "");
    }

    printf(
    "\n"
    "(Not all input->output combinations are actually supported.)\n"
    "\n"
    );
}


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

    fprintf(stdout,
    "\n"
    "Default C64 character ROM file for this build is:\n"
    "%s\n"
    "\n",
    dmGetChargenROMPath()
    );
}


const char * argGetHelpTopic(const int opt)
{
    switch (opt)
    {
        case 22:
            return
            "Color map definitions (-m)\n"
            "--------------------------\n"
            "Color map definitions are used for sprite/char data input (and ANSI text\n"
            "output), to set what colors of the C64 palette are used for each single\n"
            "color/multi color bit-combination.\n"
            "\n"
            "For example, if the input is multi color sprite or char, you can define\n"
            "colors like: -m 0,8,3,15 .. for hires/single color: -m 0,1\n"
            "\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).\n"
            "\n"
            "NOTICE! 255 is a special transparency color index; -m 255,2 would use\n"
            "transparency for '0' bits and and C64 color 2 for '1' bits.\n";

        case 28:
            return
            "Output image scaling (-S)\n"
            "-------------------------\n"
            "Scaling can be done in direct or relative mode with integer scale factors.\n"
            "\n"
            "'-S [*]<n>' sets both height and width scale factor to <n> directly or\n"
            "    relative to the certain input format's default aspect/scale factors.\n"
            "    * = scale relatively, e.g. '-S *2'.\n"
            "\n"
            "'-S <W>:<H>[*<n>]' scales width by W*n and height H*n.\n"
            "    If <n> is not specified, it defaults to 1.\n";

        case 42:
            return
            "Palette remapping (-R)\n"
            "----------------------\n"
            "Indexed palette color remapping can be performed via the '-R' option in\n"
            "several different ways:\n"
            "\n"
            " 1) '-R auto'\n"
            "    will remap input image to a destination palette specified with the\n"
            "    '-p' option (which may be a supported palette file, another paletted\n"
            "    image file, or one of the internal gfxconv palettes, see '-p help')\n"
            "\n"
            "    Example: '-R auto+remove -p pepto' would remap the input image to the\n"
            "    internal Pepto's C64 palette, removing any unused palette entries.\n"
            "\n"
            " 2) '-R <#RRGGBBaa|index>:<index>[,<(#RRGGBBaa|index):<index>[,...]]'\n"
            "    can be used to specify single RGB(A) color quadruplets/triplets or\n"
            "    palette indices to be remapped to destination palette indices. Any\n"
            "    unspecified indices will be automatically mapped. Specifying alpha\n"
            "    channel is optional, and will require +alpha flag/modifier to be\n"
            "    enabled for comparisions to take alpha channel into account.\n"
            "\n"
            "    Example: '-R #000000:0,#ffffff:1' would map black and white to indices 0 and 1.\n"
            "\n"
            " 3) '-R @<filename>'\n"
            "    can be used to specify a mapping file, which is a text file with one\n"
            "    remap definition per line in same format as above. All empty lines and\n"
            "    lines starting with a semicolor (;) will be ignored as comments. Also\n"
            "    any extra whitespace separating items will be ignored as well.\n"
            "\n"
            "Optional modifier flags can be specified as well:\n"
            "\n"
            "  +remove     Remove all unused colors from the resulting palette.\n"
            "\n"
            "  +alpha      Enable alpha value matching in color comparisions.\n"
            "              NOTE! This may result in unexpected behaviour.\n"
            "\n"
            "  +max=<f>    Set the maximum color distance/delta acceptable for\n"
            "              matching colors. Default is -1, meaning closest possible\n"
            "              that can be found even if the match is poor. Any value \n"
            "              above or equal to 0 will be considered strict limit, see\n"
            "              the 'nomatch' modifier below.\n"
            "              Range: -1, 0 .. 1.0\n"
            "\n"
            "  +nomatch=<n>\n"
            "              If no acceptable match is found (see +max modifier)\n"
            "              then use this (<n>) color index. This may have unexpected\n"
            "              results with +remove modifier. The default value of 'n'\n"
            "              is -1, which will result in error if no match is found.\n"
            "";

        default:
            return NULL;
    }
}


//
// Replace filename extension based on format pattern.
// Usage: res = dm_strdup_fext(orig_filename, "foo_%s.cmp");
//
char *dm_strdup_fext(const char *filename, const char *fmt)
{
    char *result, *tmp, *fext;

    if (filename == NULL ||
        (tmp = dm_strdup(filename)) == NULL)
        return NULL;

    if ((fext = strrchr(tmp, '.')) != NULL)
    {
        char *fpath = strrchr(tmp, DM_DIR_SEPARATOR);
        if (fpath == NULL || (fpath != NULL && fext > fpath))
            *fext = 0;
    }

    result = dm_strdup_printf(fmt, tmp);

    dmFree(tmp);

    return result;
}


//
// Return a "matching" ANSI colour code for given C64 palette index.
// As the standard 16 ANSI colours are not exact match and some C64
// colours cant be represented, this is an imperfect conversion.
//
const char *dmC64GetANSIFromC64Color(const int col)
{
    switch (col)
    {
        case  0: return "0;30";    // Black
        case  1: return "0;1;37";  // White
        case  2: return "0;31";    // Red
        case  3: return "0;36";
        case  4: return "0;35";
        case  5: return "0;32";
        case  6: return "0;34";
        case  7: return "0;1;33";
        case  8: return "0;33";
        case  9: return "0;31";
        case 10: return "0;1;31";
        case 11: return "0;1;30";
        case 12: return "0;1;30";
        case 13: return "0;1;32";
        case 14: return "0;1;34";
        case 15: return "0;37";

        default: return "0";
    }
}


BOOL dmGetConvFormat(const int type, const int format, DMConvFormat *pfmt)
{
    for (int i = 0; i < nconvFormatList; i++)
    {
        const DMConvFormat *fmt = &convFormatList[i];
        if (fmt->type == type &&
            fmt->format == format)
        {
            memcpy(pfmt, fmt, sizeof(DMConvFormat));
            return TRUE;
        }
    }

    for (int i = 0; i < nconvFormatList; i++)
    {
        const DMConvFormat *fmt = &convFormatList[i];
        if (fmt->type == type && type == FFMT_BITMAP)
        {
            const DMConvFormat *fmt = &convFormatList[i];
            const DMC64ImageFormat *cfmt = &dmC64ImageFormats[format];
            memcpy(pfmt, fmt, sizeof(DMConvFormat));
            pfmt->fext = cfmt->name;
            return TRUE;
        }
    }

    return FALSE;
}


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

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

    return FALSE;
}


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

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

    return FALSE;
}


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

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

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

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

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

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

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

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

    dmFree(opt);
    return TRUE;

out:
    dmFree(opt);
    return FALSE;
}


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], NULL))
        {
            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();
        return dmError(res, "Could not open color remap file '%s' for reading: %s.\n",
            filename, dmErrorStr(res));
    }

    while (fgets(line, sizeof(line), fp))
    {
        char *start = line;
        line[sizeof(line) - 1] = 0;

        while (*start && isspace(*start)) start++;

        if (*start != 0 && *start != ';')
        {
            if (!dmParseMapOptionMapItem(line, &values[*nvalue], nmax, "mapping file"))
                goto out;

            (*nvalue)++;
            if (*nvalue >= nmax)
            {
                dmErrorMsg("Too many mapping pairs in '%s', maximum is %d.\n",
                    filename, nmax);
                goto out;
            }
        }
    }

out:
    fclose(fp);
    return res;
}


BOOL dmParseFormatOption(const char *msg1, const char *msg2, char *optArg, int *format, int *subFormat)
{
    char *flags = strchr(optArg, ':');
    if (flags != NULL)
        *flags++ = 0;

    if (!dmGetFormatByExt(optArg, format, subFormat) &&
        !dmGetC64FormatByExt(optArg, format, subFormat))
    {
        dmErrorMsg("Invalid %s format '%s', see -F / --formats for format list.\n",
            msg1, optArg);
        return FALSE;
    }

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

            default:
                dmErrorMsg("%s format '%s' does not support any flags ('%s').\n",
                    msg2, optArg, flags);
                return FALSE;
        }
    }

    return TRUE;
}


char *dmParseValWithSep(char **arg, char *last, BOOL (*isok)(const int ch), const char *sep)
{
    char *ptr = *arg, *end, *start;

    // Skip any whitespace at start
    while (*ptr != 0 && isspace(*ptr))
        ptr++;

    start = ptr;

    // Find next not-xdigit/separator
    while (*ptr != 0 && (isok == NULL || isok(*ptr)) &&
        strchr(sep, *ptr) == NULL)
        ptr++;

    end = ptr;

    // Skip whitespace again
    while (*ptr != 0 && isspace(*ptr))
        ptr++;

    // Return last character in "last"
    *last = *ptr;

    // Set end to NUL
    *end = 0;

    // And if last character is not NUL, move ptr
    if (*last != 0)
        ptr++;

    *arg = ptr;

    return start;
}


BOOL dmParseIntValTok(const int ch)
{
    return isxdigit(ch) || ch == 'x' || ch == '$';
}


BOOL dmParseIntValWithSep(char **arg, unsigned int *value, char *last, const char *sep)
{
    return dmGetIntVal(dmParseValWithSep(arg, last, dmParseIntValTok, sep), value, NULL);
}


BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    unsigned int tmpUInt;
    char *tmpStr;

    switch (optN)
    {
        case 0:
            optShowHelp = 1;
            break;

        case 3:
            optShowHelp = 2;
            break;

        case 1:
            dmPrintLicense(stdout);
            exit(0);
            break;

        case 2:
            dmVerbosity++;
            break;

        case 10:
            optOutFilename = optArg;
            break;

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

        case 14:
            {
                DMConvFormat fmt;

                if (!dmParseFormatOption("input", "Input", optArg, &optInType, &optForcedInSubFormat))
                    return FALSE;

                dmGetConvFormat(optInType, optForcedInSubFormat, &fmt);
                if ((fmt.flags & DM_FMT_RD) == 0)
                {
                    dmErrorMsg("Invalid input format '%s', does not support reading.\n",
                        fmt.name);
                    return FALSE;
                }
            }
            break;

        case 16:
            {
                DMConvFormat fmt;

                if (!dmParseFormatOption("output", "Output", optArg, &optOutType, &optOutFormat))
                    return FALSE;

                dmGetConvFormat(optOutType, optOutFormat, &fmt);
                if ((fmt.flags & DM_FMT_WR) == 0)
                {
                    dmErrorMsg("Invalid output format '%s', does not support writing.\n",
                        fmt.name);
                    return FALSE;
                }
            }
            break;

        case 18:
            optShowHelp = 3;
            break;

        case 20:
            optSequential = TRUE;
            break;

        case 22:
            if (strcasecmp(optArg, "help") == 0)
            {
                fprintf(stdout, "\n%s\n", argGetHelpTopic(optN));
                exit(0);
            }
            else
            {
                int ncolors;
                if (!dmParseMapOptionString(optArg, optColorMap,
                    &ncolors, D64_NCOLORS, FALSE, "color index option"))
                    return FALSE;

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

        case 24:
            if (!dmGetIntVal(optArg, &tmpUInt, NULL) ||
                tmpUInt < 1)
            {
                dmErrorMsg("Invalid count value argument '%s' [1 .. MAXINT]\n",
                    optArg);
                return FALSE;
            }
            optItemCount = tmpUInt;
            break;

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

        case 28:
            if (strcasecmp(optArg, "help") == 0)
            {
                fprintf(stdout, "\n%s\n", argGetHelpTopic(optN));
                exit(0);
            }
            else
            {
                BOOL error = FALSE;
                unsigned int tmpUInt2;
                char *tmpStr = dm_strdup(optArg),
                     *tmpOpt = tmpStr, sep;

                // Check for "relative scale mode specifier
                if (*tmpOpt == '*')
                {
                    tmpOpt++;
                    optScaleMode = SCALE_RELATIVE;
                }
                else
                    optScaleMode = SCALE_SET;

                // Parse the values
                if (dmParseIntValWithSep(&tmpOpt, &tmpUInt, &sep, ":"))
                {
                    if (sep == ':' &&
                        dmParseIntValWithSep(&tmpOpt, &tmpUInt2, &sep, "*"))
                    {
                        optSpec.scaleX = tmpUInt;
                        optSpec.scaleY = tmpUInt2;

                        if (sep == '*' &&
                            dmParseIntValWithSep(&tmpOpt, &tmpUInt, &sep, ""))
                        {
                            optSpec.scaleX *= tmpUInt;
                            optSpec.scaleY *= tmpUInt;
                        }
                        error = (sep != 0);
                    }
                    else
                    if (sep == 0)
                    {
                        optSpec.scaleX = optSpec.scaleY = tmpUInt;
                    }
                    else
                        error = TRUE;
                }
                else
                    error = TRUE;

                dmFree(tmpStr);

                if (error)
                {
                    dmErrorMsg(
                        "Invalid scale option value '%s', should be [*]<n> or or <w>:<h>[*<n>].\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 30:
            optUsePalette = TRUE;
            break;

        case 32:
            if (!dmGetIntVal(optArg, &tmpUInt, NULL) ||
                tmpUInt < 1 || tmpUInt > 8)
            {
                dmErrorMsg("Invalid number of bitplanes value '%s' [1 .. 8]\n",
                    optArg);
                return FALSE;
            }
            optSpec.nplanes = tmpUInt;
            break;

        case 34:
            if (!dmGetIntVal(optArg, &tmpUInt, NULL) ||
                tmpUInt < 1 || tmpUInt > 32)
            {
                dmErrorMsg("Invalid number of bits per plane value '%s' [1 .. 32]\n",
                    optArg);
                return FALSE;
            }
            optSpec.bpp = tmpUInt;
            break;

        case 36:
            optSpec.planar = TRUE;
            break;

        case 38:
            if (!dmGetIntVal(optArg, &tmpUInt, NULL) ||
                tmpUInt > FCMP_BEST)
            {
                dmErrorMsg("Invalid compression setting '%s' [%d .. %d]\n",
                    optArg, FCMP_NONE, FCMP_BEST);
                return FALSE;
            }
            optSpec.compression = tmpUInt;
            break;

        case 40:
            if (strcasecmp(optArg, "auto") == 0)
            {
                optCropMode = CROP_AUTO;
            }
            else
            {
                unsigned int tx0, ty0, tx1, ty1;
                char sep, modeSep = 0, *tmpTok, *tmpStr;
                BOOL ok;
                if ((tmpTok = tmpStr = dm_strdup(optArg)) == NULL)
                {
                    dmErrorMsg("Could not allocate memory for temporary string.\n");
                    return FALSE;
                }

                // Check for 'x0:y0-x1:y1' pattern
                ok = dmParseIntValWithSep(&tmpTok, &tx0, &sep, ":") &&
                    sep == ':' &&
                        dmParseIntValWithSep(&tmpTok, &ty0, &modeSep, ":-") &&
                    (sep == ':' || sep == '-') &&
                    dmParseIntValWithSep(&tmpTok, &tx1, &sep, ":") &&
                    sep == ':' &&
                    dmParseIntValWithSep(&tmpTok, &ty1, &sep, "");

                dmFree(tmpStr);

                if (ok)
                {
                    optCropMode = CROP_SIZE;
                    optCropX0   = tx0;
                    optCropY0   = ty0;

                    if (modeSep == '-')
                    {
                        DM_SWAP_IF(unsigned int, tx0, tx1);
                        DM_SWAP_IF(unsigned int, ty0, ty1);

                        optCropW    = tx1 - tx0;
                        optCropH    = ty1 - ty0;

                        dmMsg(1, "Crop coordinates %d, %d - %d, %d [dim %d, %d]\n",
                            tx0, ty0, tx1, ty1, optCropW, optCropH);
                    }
                    else
                    {
                        optCropW    = tx1;
                        optCropH    = ty1;

                        dmMsg(1, "Crop coordinates %d, %d - %d, %d [dim %d, %d]\n",
                            tx0, ty0, tx0 + tx1, ty0 + ty1, optCropW, optCropH);
                    }
                }
                else
                {
                    dmErrorMsg("Invalid crop mode / argument '%s'.\n", optArg);
                    return FALSE;
                }
            }
            break;

        case 42:
            if (strcasecmp(optArg, "help") == 0)
            {
                fprintf(stdout, "\n%s\n", argGetHelpTopic(optN));
                exit(0);
            }

            // Check if any flags/sub-options are specified
            if ((tmpStr = strchr(optArg, '+')) != NULL)
            {
                *tmpStr++ = 0;
                do
                {
                    // Parse one sub-option
                    char sep, *topt = dmParseValWithSep(&tmpStr, &sep, NULL, "+");

                    // Check what option we have
                    if (strcasecmp(topt, "remove") == 0)
                        optRemapRemove = TRUE;
                    else
                    if (strcasecmp(topt, "alpha") == 0)
                        optRemapMatchAlpha = TRUE;
                    else
                    if (strncasecmp(topt, "max=", 4) == 0)
                    {
                        char *start = topt + 4, *end;
                        optRemapMaxDist = strtof(start, &end);

                        if (end == start)
                        {
                            dmErrorMsg("Invalid or missing value parameter for -R option flag: '%s'.\n",
                                topt);
                            return FALSE;
                        }
                    }
                    else
                    if (strncasecmp(topt, "nomatch=", 8) == 0)
                    {
                        char *start = topt + 8, *end;
                        optRemapNoMatchColor = strtol(start, &end, 10);

                        if (end == start)
                        {
                            dmErrorMsg("Invalid or missing value parameter for -R option flag: '%s'.\n",
                                topt);
                            return FALSE;
                        }

                        if (optRemapNoMatchColor < -1 || optRemapNoMatchColor > 255)
                        {
                            dmErrorMsg("Invalid remap no-match color value %d. Should be [-1 .. 255].\n",
                                optRemapNoMatchColor);
                            return FALSE;
                        }
                    }
                    else
                    {
                        dmErrorMsg("Unknown -R option flag '%s'.\n", topt);
                        return FALSE;
                    }
                } while (*tmpStr != 0);
            }

            // Check which remap mode is being requested
            if (strcasecmp(optArg, "auto") == 0)
            {
                if (optRemapMode != REMAP_NONE && optRemapMode != REMAP_AUTO)
                {
                    dmErrorMsg("Remap mode already set to something else than 'auto'. You can only have one remapping mode.\n");
                    return FALSE;
                }

                optRemapMode = REMAP_AUTO;
            }
            else
            {
                if (optRemapMode != REMAP_NONE && optRemapMode != REMAP_MAPPED)
                {
                    dmErrorMsg("Remap mode already set to something else than 'mapped'. You can only have one remapping mode.\n");
                    return FALSE;
                }

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

                optRemapMode = REMAP_MAPPED;
            }

            if (optRemapMatchAlpha)
            {
                dmErrorMsg("WARNING: Palette alpha matching may have unexpected results.\n");
            }
            break;

        case 44:
            optCharROMFilename = optArg;
            break;

        case 46:
            return argHandleC64PaletteOption(optArg, &optC64Palette, &optPaletteFile);

        default:
            dmErrorMsg("Unimplemented option argument '%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, const Uint8 byte, const int format, const BOOL multicolor, const BOOL dir)
{
    if (multicolor)
    {
        for (int i = 0; i < DM_ASC_NBITS; i += 2)
        {
            int k = dir ? i : (DM_ASC_NBITS - i - 1);
            Uint8 val = (byte & (3ULL << k)) >> k;
            char ch;
            switch (format)
            {
                case FFMT_ASCII:
                    ch = dmASCIIPalette[val];
                    fprintf(out, "%c%c", ch, ch);
                    break;
                case FFMT_ANSI:
                    fprintf(out, "\x1b[%sm##\x1b[0m",
                        dmC64GetANSIFromC64Color(optColorMap[val]));
                    break;
            }
        }
    }
    else
    {
        for (int i = 0; i < DM_ASC_NBITS; i++)
        {
            int k = dir ? i : (DM_ASC_NBITS - i - 1);
            Uint8 val = (byte & (1ULL << k)) >> k;
            switch (format)
            {
                case FFMT_ASCII:
                    fputc(val ? '#' : '.', out);
                    break;
                case FFMT_ANSI:
                    fprintf(out, "\x1b[%sm#\x1b[0m",
                        dmC64GetANSIFromC64Color(optColorMap[val]));
                    break;
            }
        }
    }
}


void dmDumpCharASCII(FILE *outFile, const Uint8 *buf, const size_t offs, const int fmt, const BOOL multicolor)
{
    for (size_t yc = 0; yc < D64_CHR_HEIGHT_UT; yc++)
    {
        fprintf(outFile, "%04" DM_PRIx_SIZE_T " : ", offs + yc);
        dmPrintByte(outFile, buf[yc], fmt, multicolor, FALSE);
        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 < D64_SPR_HEIGHT_UT; yc++)
    {
        fprintf(outFile, "%04" DM_PRIx_SIZE_T " ", offs + bufOffs);
        for (xc = 0; xc < D64_SPR_WIDTH_UT; xc++)
        {
            dmPrintByte(outFile, buf[bufOffs], fmt, multicolor, FALSE);
            fprintf(outFile, " ");
            bufOffs++;
        }
        fprintf(outFile, "\n");
    }
}


// XXX TODO: we need to evaluate the color vector itself, not just the distance
float dmGetColorDist(const DMColor *c1, const DMColor *c2, const BOOL alpha)
{
    const float
        dr = (c1->r - c2->r) / 255.0,
        dg = (c1->g - c2->g) / 255.0,
        db = (c1->b - c2->b) / 255.0;

    if (alpha)
    {
        const float da = (c1->a - c2->a) / 255.0;
        return (dr * dr + dg * dg + db * db + da * da) / 4.0;
    }
    else
        return (dr * dr + dg * dg + db * db) / 3.0;
}


int dmScanUsedColors(const DMImage *src, const BOOL warn, BOOL *used, int *nused)
{
    BOOL warned = FALSE;
    *nused = 0;

    if (src == NULL || used == NULL || nused == NULL)
        return DMERR_NULLPTR;

    if (src->pal == NULL || src->pixfmt != DM_PIXFMT_PALETTE)
    {
        return dmError(DMERR_INVALID_DATA,
            "Source image is not paletted.\n");
    }

    for (int index = 0; index < src->pal->ncolors; index++)
        used[index] = FALSE;

    for (int yc = 0; yc < src->height; yc++)
    {
        const Uint8 *dp = src->data + src->pitch * yc;
        for (int xc = 0; xc < src->width; xc++)
        {
            Uint8 col = dp[xc];
            if (col < src->pal->ncolors)
            {
                if (!used[col])
                {
                    used[col] = TRUE;
                    (*nused)++;
                }
            }
            else
            if (warn && !warned)
            {
                dmErrorMsg("Image contains color indices that are out of bounds of the palette.\n");
                warned = TRUE;
            }
        }
    }

    return DMERR_OK;
}


int dmDoRemapImageColors(DMImage **pdst, const DMImage *src,
    const int *mapping, const DMPalette *dpal)
{
    DMImage *dst = NULL;
    int res = DMERR_OK;

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

    // Allocate target image
    if ((dst = *pdst = dmImageAlloc(src->width, src->height,
        src->pixfmt, -1)) == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "Could not allocate image for re-mapped data.\n");
        goto out;
    }

    for (int yc = 0; yc < src->height; yc++)
    {
        Uint8 *sp = src->data + src->pitch * yc;
        Uint8 *dp = dst->data + dst->pitch * yc;
        for (int xc = 0; xc < src->width; xc++)
        {
            dp[xc] = mapping[sp[xc]];
        }
    }

    if ((res = dmPaletteCopy(&dst->pal, dpal)) != DMERR_OK)
    {
        dmErrorMsg("Error installing remapped palette to destination image: %s\n",
            dmErrorStr(res));
        goto out;
    }

out:
    return res;
}


int dmRemapImageColors(DMImage **pdst, const DMImage *src,
    const DMPalette *dpal,
    const float maxDist, const int noMatchColor,
    const BOOL alpha, const BOOL removeUnused)
{
    DMPalette *tpal = NULL;
    const DMPalette *ppal;
    BOOL *used = NULL;
    int *mapping = NULL, *mapped = NULL;
    int res = DMERR_OK;
    BOOL fail = FALSE;

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

    if (src->pal == NULL || src->pixfmt != DM_PIXFMT_PALETTE)
    {
        res = dmError(DMERR_INVALID_DATA,
            "Source image is not paletted.\n");
        goto out;
    }

    dmMsg(1, "Remapping image from %d to %d colors @ maxDist=",
        src->pal->ncolors,
        dpal->ncolors);

    if (maxDist < 0)
        dmPrint(1, "auto");
    else
        dmPrint(1, "%1.3f", maxDist);

    dmPrint(1, ", %s, ",
        alpha ? "match alpha" : "ignore alpha");

    if (noMatchColor < 0)
        dmPrint(1, "fail on 'no match'\n");
    else
        dmPrint(1, "use color #%d if no match\n", noMatchColor);

    // Allocate remapping tables
    if ((mapped  = dmMalloc0(src->pal->ncolors * sizeof(*mapped))) == NULL ||
        (used    = dmMalloc0(src->pal->ncolors * sizeof(*used))) == NULL ||
        (mapping = dmMalloc(src->pal->ncolors * sizeof(*mapping))) == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "Could not allocate memory for color remap tables.\n");
        goto out;
    }

    for (int index = 0; index < src->pal->ncolors; index++)
        mapping[index] = -1;

    // Populate remap table
    for (int sc = 0; sc < src->pal->ncolors; sc++)
    {
        // Check if we can find a match in destination palette dpal
        float closestDist = 1000000, dist = 0;
        int closestDC = -1;

        for (int dc = 0; dc < dpal->ncolors; dc++)
        {
            dist = dmGetColorDist(&src->pal->colors[sc], &dpal->colors[dc], alpha);
            if (dist < closestDist)
            {
                closestDist = dist;
                closestDC = dc;
            }
        }

        // Did we find a close-enough match?
        if (maxDist >= 0 && closestDist > maxDist)
        {
            // No, either error out or use noMatchColor color index
            if (noMatchColor < 0)
            {
                DMColor *dcol = &dpal->colors[closestDC];

                dmPrint(0,
                    "No match for source color #%d. Closest: #%d (%02x %02x %02x) [dist=%1.3f > %1.3f]\n",
                    sc, closestDC, dcol->r, dcol->g, dcol->b,
                    closestDist, maxDist);
                fail = TRUE;
            }
            else
            {
                closestDC = noMatchColor;
            }
        }
        else
        {
            DMColor *scol = &src->pal->colors[sc],
                    *dcol = &dpal->colors[closestDC];

            dmPrint(3, "Palette match #%d (%02x %02x %02x) -> #%d (%02x %02x %02x) [dist=%1.3f]\n",
                sc, scol->r, scol->g, scol->b,
                closestDC, dcol->r, dcol->g, dcol->b,
                closestDist);
        }

        mapping[sc] = closestDC;
    }

    if (fail)
    {
        res = DMERR_INVALID_DATA;
        goto out;
    }

    // Remove unused colors if requested
    if (removeUnused)
    {
        int nused;

        if (noMatchColor >= 0)
        {
            dmErrorMsg("WARNING! Removing unused colors with 'no-match' color index set may have unintended results.\n");
        }

        // Get the actually used colors
        if ((res = dmScanUsedColors(src, TRUE, used, &nused)) != DMERR_OK)
            goto out;

        dmMsg(2, "Found %d used color indices.\n", nused);

        // Remove duplicates from the mapped colour indices
        for (int index = 0; index < src->pal->ncolors; index++)
        {
            for (int n = 0; n < src->pal->ncolors; n++)
            if (n != index &&
                mapping[index] == mapping[n] &&
                used[n] && used[index])
            {
                used[n] = FALSE;
            }
        }

        if (noMatchColor >= 0)
            used[noMatchColor] = TRUE;

        // Re-count number of actually used indices
        nused = 0;
        for (int index = 0; index < src->pal->ncolors; index++)
        if (used[index])
            nused++;

        dmMsg(2, "After mapped dupe removal, %d color indices used.\n", nused);

        if ((res = dmPaletteAlloc(&tpal, nused, -1)) != DMERR_OK)
        {
            dmErrorMsg("Could not allocate memory for remap palette.\n");
            goto out;
        }

        // Copy colors from dpal to tpal, also mapping the reordered indices
        nused = 0;
        for (int index = 0; index < src->pal->ncolors; index++)
        if (used[index])
        {
            // Copy the color to tpal
            memcpy(&tpal->colors[nused], &dpal->colors[mapping[index]], sizeof(DMColor));

            // Save current mapping to mapped[]
            mapped[nused] = mapping[index];

            // Reorder the mapping
            mapping[index] = nused;
            nused++;
        }
        else
        {
            // "Unused" color, find matching mapping from mapped[]
            for (int n = 0; n < nused; n++)
            if (mapping[index] == mapped[n])
            {
                mapping[index] = n;
                break;
            }
        }

        ppal = tpal;
    }
    else
        ppal = dpal;

    // Perform image data remapping
    res = dmDoRemapImageColors(pdst, src, mapping, ppal);

out:
    dmPaletteFree(tpal);
    dmFree(mapping);
    dmFree(mapped);
    dmFree(used);
    return res;
}


int dmMapImageColors(DMImage **pdst, const DMImage *src,
    const DMMapValue *mapTable, const int nmapTable,
    const float maxDist, const int noMatchColor,
    const BOOL alpha, const BOOL removeUnused)
{
    DMPalette *tpal = NULL;
    BOOL *mapped = NULL, *used = NULL;
    int *mapping = NULL;
    int nused, res = DMERR_OK;
    BOOL fail = FALSE;

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

    if (src->pal == NULL || src->pixfmt != DM_PIXFMT_PALETTE)
    {
        res = dmError(DMERR_INVALID_DATA,
            "Source image is not paletted.\n");
        goto out;
    }

    // Allocate remapping tables
    if ((mapped  = dmMalloc(src->pal->ncolors * sizeof(*mapped))) == NULL ||
        (used    = dmMalloc(src->pal->ncolors * sizeof(*used))) == NULL ||
        (mapping = dmMalloc(src->pal->ncolors * sizeof(*mapping))) == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "Could not allocate memory for color remap tables.\n");
        goto out;
    }

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

    if ((res = dmPaletteAlloc(&tpal, src->pal->ncolors, -1)) != DMERR_OK)
    {
        dmErrorMsg("Could not allocate memory for remap palette.\n");
        goto out;
    }

    dmMsg(1, "Remapping %d input image of %d colors, %s, ",
        optNRemapTable, src->pal->ncolors,
        alpha ? "match alpha" : "ignore alpha");

    if (noMatchColor < 0)
        dmPrint(1, "fail on 'no match'\n");
    else
        dmPrint(1, "use color #%d if no match\n", noMatchColor);

    // Match and mark mapped colors
    for (int index = 0; index < nmapTable; index++)
    {
        const DMMapValue *map = &mapTable[index];
        if (map->triplet)
        {
            float closestDist = 1000000, dist = 0;
            int closestDC = -1;

            for (int n = 0; n < src->pal->ncolors; n++)
            {
                dist = dmGetColorDist(&src->pal->colors[n], &map->color, map->alpha && alpha);
                if (dist < closestDist)
                {
                    closestDist = dist;
                    closestDC = n;
                }
            }

            // Did we find a close-enough match?
            if (maxDist >= 0 && closestDist > maxDist)
            {
                // No, either error out or use noMatchColor color index
                if (noMatchColor < 0)
                {
                    DMColor *dcol = &src->pal->colors[closestDC];

                    dmMsg(3, "No RGBA match found for map index %d, #%02x%02x%02x%02x. Closest: #%d (#%02x%02x%02x%02x) [dist=%1.3f > %1.3f]\n",
                        index, map->color.r, map->color.g, map->color.b, map->color.a,
                        closestDC, dcol->r, dcol->g, dcol->b, dcol->a, closestDist, maxDist);

                    fail = TRUE;
                }
                else
                {
                    DMColor *dcol = &src->pal->colors[noMatchColor];
                    closestDC = noMatchColor;

                    dmMsg(3, "RGBA noMatch #%02x%02x%02x%02x: #%d -> #%d #%02x%02x%02x%02x [dist=%1.3f]\n",
                        map->color.r, map->color.g, map->color.b, map->color.a,
                        map->to,
                        closestDC, dcol->r, dcol->g, dcol->b, dcol->a, closestDist);

                    mapping[closestDC] = map->to;
                    mapped[map->to] = TRUE;
                }
            }
            else
            {
                DMColor *dcol = &src->pal->colors[closestDC];

                dmMsg(3, "RGBA match #%02x%02x%02x%02x: #%d -> #%d #%02x%02x%02x%02x [dist=%1.3f]\n",
                    map->color.r, map->color.g, map->color.b, map->color.a,
                    map->to,
                    closestDC, dcol->r, dcol->g, dcol->b, dcol->a, closestDist);

                mapping[closestDC] = map->to;
                mapped[map->to] = TRUE;
            }
        }
        else
        {
            dmMsg(3, "Map index: %d -> %d\n",
                map->from, map->to);

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

    if (fail)
    {
        res = DMERR_INVALID_DATA;
        goto out;
    }

    // Fill the unmapped colors
    if (removeUnused)
    {
        dmMsg(2, "Scanning for used colors.\n");

        if ((res = dmScanUsedColors(src, TRUE, used, &nused)) != DMERR_OK)
            goto out;

        dmMsg(2, "Removing unused colors: %d -> %d.\n",
            src->pal->ncolors, nused);
    }

    for (int index = 0; index < src->pal->ncolors; index++)
    if (mapping[index] < 0 &&
        (!removeUnused || used[index]))
    {
        for (int n = 0; n < src->pal->ncolors; n++)
        if (!mapped[n])
        {
            mapping[index] = n;
            mapped[n] = TRUE;
            break;
        }
    }

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

    // Perform image data remapping
    res = dmDoRemapImageColors(pdst, src, mapping, tpal);

out:
    dmPaletteFree(tpal);
    dmFree(mapping);
    dmFree(mapped);
    dmFree(used);
    return res;
}


int dmDumpC64Block(const char *fprefix, const char *fext, const DMC64MemBlock *blk, const int index)
{
    int res = DMERR_OK;
    if (blk != NULL && blk->data != NULL)
    {
        char *filename = dm_strdup_printf("%s_%s_%d.bin", fprefix, fext, index);
        if (filename == NULL)
            return DMERR_MALLOC;

        res = dmWriteDataFile(NULL, filename, blk->data, blk->size);
        dmFree(filename);
    }
    return res;
}


int dmDumpC64Bitmap(const char *fprefix, const DMC64Image *img)
{
    int res = DMERR_OK;

    for (int i = 0; i < img->nblocks; i++)
    {
        res = dmDumpC64Block(fprefix, "bitmap", &img->bitmap[i], i);
        res = dmDumpC64Block(fprefix, "color", &img->color[i], i);
        res = dmDumpC64Block(fprefix, "screen", &img->screen[i], i);
        res = dmDumpC64Block(fprefix, "chardata", &img->charData[i], i);

        if ((size_t) i < sizeof(img->extraData) / sizeof(img->extraData[0]))
            res = dmDumpC64Block(fprefix, "extradata", &img->extraData[i], i);
    }

    return res;
}


int dmConvertC64Bitmap(DMC64Image **pdst, const DMC64Image *src,
    const DMC64ImageFormat *dstFmt, const DMC64ImageFormat *srcFmt)
{
    DMC64Image *dst;
    DMC64MemBlock *srcBlk = NULL, *dstBlk = NULL;
    const char *blkname = NULL;

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

    // Allocate the destination image
    if ((dst = *pdst = dmC64ImageAlloc(dstFmt)) == NULL)
        return DMERR_MALLOC;

    // Copy rest of the structure ..
    dst->d020    = src->d020;
    dst->bgcolor = src->bgcolor;
    dst->d022    = src->d022;
    dst->d023    = src->d023;
    dst->d024    = src->d024;

    // And some extraInfo fields ..
    dst->extraInfo[D64_EI_CHAR_CASE] = src->extraInfo[D64_EI_CHAR_CASE];
    dst->extraInfo[D64_EI_CHAR_CUSTOM] = src->extraInfo[D64_EI_CHAR_CUSTOM];

    // Try to do some simple fixups
    if ((dst->extraInfo[D64_EI_MODE] & D64_FMT_MODE_MASK) == D64_FMT_MC &&
        (src->extraInfo[D64_EI_MODE] & D64_FMT_MODE_MASK) == D64_FMT_HIRES)
    {
        dmC64MemBlockCopy(&dst->screen[0], &src->screen[0]);
    }
    else
    if ((dst->extraInfo[D64_EI_MODE] & D64_FMT_MODE_MASK) == D64_FMT_HIRES &&
        (src->extraInfo[D64_EI_MODE] & D64_FMT_MODE_MASK) == D64_FMT_MC)
    {
        // XXX TODO: Handle FLI mc->hires differently?
    }

    if ((dst->extraInfo[D64_EI_MODE] & D64_FMT_FLI) &&
        (src->extraInfo[D64_EI_MODE] & D64_FMT_FLI) == 0)
    {
        dmMsg(1, "Upconverting multicolor to FLI.\n");
        for (int i = 0; i < dst->nblocks; i++)
        {
            if (dst->color[i].data == NULL)
                dmC64MemBlockCopy(&dst->color[i], &src->color[0]);

            if (dst->screen[i].data == NULL)
                dmC64MemBlockCopy(&dst->screen[i], &src->screen[0]);

            if (dst->bitmap[i].data == NULL)
                dmC64MemBlockCopy(&dst->bitmap[i], &src->bitmap[0]);
        }
    }
    else
    if ((src->extraInfo[D64_EI_MODE] & D64_FMT_FLI) &&
        (dst->extraInfo[D64_EI_MODE] & D64_FMT_FLI) == 0)
    {
        dmMsg(1, "Downconverting FLI to multicolor.\n");
    }

    // Do per opcode copies
    for (int opn = 0; opn < D64_MAX_ENCDEC_OPS; opn++)
    {
        const DMC64EncDecOp *op = fmtGetEncDecOp(dstFmt, opn);
        size_t size;

        if (op->type == DO_LAST)
            break;

        size = dmC64GetOpSubjectSize(op, dstFmt->format);
        switch (op->type)
        {
            case DO_COPY:
            case DO_SET_MEM:
            case DO_SET_MEM_HI:
            case DO_SET_MEM_LO:
            case DO_SET_OP:
                srcBlk = (DMC64MemBlock *) dmC64GetOpMemBlock(src, op->subject, op->bank);
                dstBlk = (DMC64MemBlock *) dmC64GetOpMemBlock(dst, op->subject, op->bank);
                blkname = dmC64GetOpSubjectName(op->subject);

                // Skip if we did previous fixups/upconverts
                if (dstBlk != NULL && dstBlk->data != NULL)
                    break;

                if (srcBlk != NULL && srcBlk->data != NULL && srcBlk->size >= size)
                {
                    // The block exists in source and is of sufficient size, so copy it
                    dmMsg(3, "Copying whole block '%s' "
                        "op #%d, offs=%d ($%04x), bank=%d, size=%" DM_PRIu_SIZE_T " ($%04" DM_PRIx_SIZE_T ")\n",
                        blkname, opn, op->offs, op->offs, op->bank, size, size);

                    dmC64MemBlockCopy(dstBlk, srcBlk);
                }
                else
                switch (op->subject)
                {
                    case DS_COLOR_RAM:
                    case DS_SCREEN_RAM:
                    case DS_BITMAP_RAM:
                    case DS_CHAR_DATA:
                    case DS_EXTRA_DATA:
                        if ((dmC64MemBlockAlloc(dstBlk, size)) != DMERR_OK)
                        {
                            return dmError(DMERR_MALLOC,
                                "Could not allocate '%s' block! "
                                "op #%d, offs=%d ($%04x), bank=%d, size=%" DM_PRIu_SIZE_T " ($%04" DM_PRIx_SIZE_T ")\n",
                                blkname, opn, op->offs, op->offs, op->bank, size, size);
                        }
                        if (srcBlk == NULL || srcBlk->data == NULL)
                        {
                            dmMsg(3, "Creating whole block '%s' "
                                "op #%d, offs=%d ($%04x), bank=%d, size=%" DM_PRIu_SIZE_T " ($%04" DM_PRIx_SIZE_T ")\n",
                                blkname, opn, op->offs, op->offs, op->bank, size, size);
                        }
                        else
                        {
                            dmMsg(3, "Creating block '%s' from partial data "
                                "op #%d, offs=%d ($%04x), bank=%d, size=%" DM_PRIu_SIZE_T " ($%04" DM_PRIx_SIZE_T ")\n",
                                blkname, opn, op->offs, op->offs, op->bank, size, size);
                        }
                        switch (op->type)
                        {
                            case DO_COPY:
                                // If some data exists, copy it. Rest is zero.
                                // Otherwise just set to zero.
                                if (srcBlk != NULL && srcBlk->data != NULL)
                                    memcpy(dstBlk->data, srcBlk->data, srcBlk->size);
                                break;

                            case DO_SET_MEM:
                                // Leave allocate data to zero.
                                break;

                            case DO_SET_OP:
                                memset(dstBlk->data, op->offs, size);
                                break;

                            default:
                                return dmError(DMERR_INTERNAL,
                                    "Unhandled op type #%d in "
                                    "op #%d, offs=%d ($%04x), bank=%d, size=%" DM_PRIu_SIZE_T " ($%04" DM_PRIx_SIZE_T ")\n",
                                    op->type, opn, op->offs, op->offs, op->bank, size, size);
                        }
                        break;
                }
                break;
        }
    }

    return DMERR_OK;
}


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

    dmGrowBufInit(&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)
    {
        dmErrorMsg("Error encoding bitmap data: %s\n", dmErrorStr(res));
        goto out;
    }

    // And output the file
    dmMsg(1, "Writing output file '%s', %" DM_PRIu_SIZE_T " bytes.\n",
        filename, buf.len);

    res = dmWriteDataFile(NULL, filename, buf.data, buf.len);

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


int dmWriteIFFMasterRAWHeaderFile(
    const char *hdrFilename, const char *dataFilename,
    const char *prefix, const DMImage *img,
    const DMImageWriteSpec *spec)
{
    DMResource *fp;
    int res;

    if ((res = dmf_open_stdio(hdrFilename, "wb", &fp)) != DMERR_OK)
    {
        return dmError(res,
            "Could not open file '%s' for writing: %s\n",
            hdrFilename, dmErrorStr(res));
    }

    res = dmWriteIFFMasterRAWHeader(fp, dataFilename, prefix, img, spec);

    dmf_close(fp);
    return res;
}


int dmWriteImage(const char *filename, DMImage *pimage,
    DMImageWriteSpec *spec, const DMImageFormat *fmt)
{
    int res = DMERR_OK;
    DMImage *image = pimage;
    BOOL allocated = FALSE;

    // Check if writing is even supported
    if (fmt->write == NULL || (fmt->flags & DM_FMT_WR) == 0)
    {
        return dmError(DMERR_NOT_SUPPORTED,
            "Writing of '%s' format is not supported.\n",
            fmt->name);
    }

    dmMsg(1, "Outputting '%s' image %d x %d -> %d x %d [%d x %d]\n",
        fmt->name,
        pimage->width, pimage->height,
        pimage->width * spec->scaleX, pimage->height * spec->scaleY,
        spec->scaleX, spec->scaleY);

    if (image->pixfmt == DM_PIXFMT_PALETTE)
    {
        switch (optRemapMode)
        {
            case REMAP_NONE:
                if (optPaletteData != NULL)
                {
                    DMPalette *tpal;

                    dmMsg(1, "Replacing image palette %d colors with %d colors.\n",
                        image->pal->ncolors, optPaletteData->ncolors);

                    if ((res = dmPaletteCopy(&tpal, optPaletteData)) != DMERR_OK)
                        return res;

                    if (image->pal->ncolors != optPaletteData->ncolors)
                    {
                        dmMsg(1, "Trying to resize to %d colors.\n",
                            image->pal->ncolors);

                        if ((res = dmPaletteResize(&tpal, image->pal->ncolors)) != DMERR_OK)
                            return res;
                    }

                    dmPaletteFree(image->pal);
                    image->pal = tpal;
                }
                break;

            case REMAP_MAPPED:
                if (optPaletteData != NULL)
                {
                    dmErrorMsg(
                        "WARNING: Color remapping requested, but palette replacement (-p) set. This will have no effect.\n");
                }

                if ((res = dmMapImageColors(
                    &image, pimage, optRemapTable, optNRemapTable,
                    optRemapMaxDist, optRemapNoMatchColor,
                    optRemapMatchAlpha, optRemapRemove)) != DMERR_OK)
                    goto out;

                allocated = TRUE;
                break;

            case REMAP_AUTO:
                if (optPaletteData == NULL)
                {
                    dmErrorMsg(
                        "Color auto-remapping requested, but target palette not set? (-p option)\n");
                    goto out;
                }

                if ((res = dmRemapImageColors(
                    &image, pimage, optPaletteData,
                    optRemapMaxDist, optRemapNoMatchColor,
                    optRemapMatchAlpha, optRemapRemove)) != DMERR_OK)
                    goto out;

                allocated = TRUE;
                break;
        }
    }
    else
    if (optRemapMode != REMAP_NONE)
    {
        dmErrorMsg("Color remapping requested, but image is not paletted?\n");
        goto out;
    }

    // Determine number of planes, if paletted
    if (spec->nplanes == 0)
    {
        if (image->pixfmt == DM_PIXFMT_PALETTE &&
            image->pal != NULL)
            spec->nplanes = dmGetNPlanesFromNColors(image->pal->ncolors);
        else
        if (image->pixfmt == DM_PIXFMT_GRAYSCALE)
            spec->nplanes = image->bpp;
    }

    if (spec->nplanes <= 0)
        spec->nplanes = 4;

    spec->fmtid = fmt->fmtid;

    // Do some format-specific adjustments and other things
    switch (fmt->fmtid)
    {
        case DM_IMGFMT_PNG:
            if (optUsePalette)
                spec->pixfmt = (image->pixfmt == DM_PIXFMT_GRAYSCALE) ? DM_PIXFMT_GRAYSCALE : DM_PIXFMT_PALETTE;
            else
                spec->pixfmt = DM_PIXFMT_RGBA;
            break;

        case DM_IMGFMT_PPM:
            if (optUsePalette && image->pixfmt == DM_PIXFMT_GRAYSCALE)
                spec->pixfmt = DM_PIXFMT_GRAYSCALE;
            else
                spec->pixfmt = DM_PIXFMT_RGB;
            break;

        case DM_IMGFMT_RAW:
        case DM_IMGFMT_ARAW:
            {
                char *prefix = NULL, *hdrFilename = NULL;
                if ((hdrFilename = dm_strdup_fext(filename, "%s.inc")) == NULL ||
                    (prefix = dm_strdup_fext(filename, "img_%s")) == NULL)
                {
                    res = dmError(DMERR_MALLOC,
                        "Could not allocate memory for filename strings? :O\n");
                    goto out;
                }

                // Replace any non-alphanumerics in palette ID
                for (int i = 0; prefix[i]; i++)
                    prefix[i] = isalnum(prefix[i]) ? tolower(prefix[i]) : '_';

                dmMsg(2, "%d bitplanes, %s planes output.\n",
                    spec->nplanes,
                    spec->planar ? "planar/interleaved" : "non-interleaved");
                dmMsg(2, "%s datafile '%s', ID prefix '%s'.\n",
                    fmt->fmtid == DM_IMGFMT_ARAW ? "ARAW" : "RAW",
                    hdrFilename, prefix);

                res = dmWriteIFFMasterRAWHeaderFile(
                    hdrFilename, filename, prefix, image, spec);

                dmFree(prefix);
                dmFree(hdrFilename);
            }
            break;

        default:
            spec->pixfmt = optUsePalette ? DM_PIXFMT_PALETTE : DM_PIXFMT_RGB;
            break;
    }

    // If no error has occured thus far, write the image
    if (res == DMERR_OK)
    {
        DMResource *fp;
        char *str;
        switch (spec->pixfmt)
        {
            case DM_PIXFMT_PALETTE   : str = "indexed/paletted"; break;
            case DM_PIXFMT_RGB       : str = "24bit RGB"; break;
            case DM_PIXFMT_RGBA      : str = "32bit RGBA"; break;
            case DM_PIXFMT_GRAYSCALE : str = "grayscale"; break;
            default                  : str = "???"; break;
        }
        dmMsg(1, "Using %s output.\n", str);

        if ((res = dmf_open_stdio(filename, "wb", &fp)) != DMERR_OK)
        {
            dmErrorMsg("Could not open file '%s' for writing: %s\n",
                filename, dmErrorStr(res));
            goto out;
        }

        res = fmt->write(fp, image, spec);

        dmf_close(fp);
    }

out:
    if (allocated)
        dmImageFree(image);

    return res;
}


int dmWritePalette(const char *filename, const DMPalette *palette, const DMPaletteFormat *fmt)
{
    DMResource *fp = NULL;
    int res;

    // Check if writing is even supported
    if (fmt->write == NULL || (fmt->flags & DM_FMT_WR) == 0)
    {
        return dmError(DMERR_NOT_SUPPORTED,
            "Writing of '%s' format is not supported.\n",
            fmt->name);
    }

    dmMsg(1, "Outputting '%s' format palette of %d entries.\n",
        fmt->name, palette->ncolors);

    if ((res = dmf_open_stdio(filename, "wb", &fp)) != DMERR_OK)
    {
        dmErrorMsg("Could not open file '%s' for writing: %s\n",
            filename, dmErrorStr(res));
        goto out;
    }

    res = fmt->write(fp, palette);

out:
    dmf_close(fp);

    return res;
}


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 + D64_CHR_WIDTH_PX > image->width ||
        yoffs + D64_CHR_HEIGHT_PX > image->height)
        return FALSE;

    for (yc = 0; yc < D64_CHR_HEIGHT_UT; 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 + D64_SPR_WIDTH_PX > image->width ||
        yoffs + D64_SPR_HEIGHT_PX > image->height)
        return FALSE;

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

    return TRUE;
}


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

    switch (outFormat)
    {
        case FFMT_CHAR:
            outBufSize = D64_CHR_SIZE;
            outBlockW = image->width / D64_CHR_WIDTH_PX;
            outBlockH = image->height / D64_CHR_HEIGHT_PX;
            outType = "char";
            break;

        case FFMT_SPRITE:
            outBufSize = D64_SPR_SIZE;
            outBlockW = image->width / D64_SPR_WIDTH_PX;
            outBlockH = image->height / D64_SPR_HEIGHT_PX;
            outType = "sprite";
            break;

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

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

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

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

    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(tmpBuf, image,
                    bx * D64_CHR_WIDTH_PX, by * D64_CHR_HEIGHT_PX,
                    multicolor))
                {
                    ret = DMERR_DATA_ERROR;
                    goto out;
                }
                break;

            case FFMT_SPRITE:
                if (!dmConvertImage2Sprite(tmpBuf, image,
                    bx * D64_SPR_WIDTH_PX, by * D64_SPR_HEIGHT_PX,
                    multicolor))
                {
                    ret = DMERR_DATA_ERROR;
                    goto out;
                }
                break;
        }

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

out:
    // Cleanup
    if (outFile != NULL)
        fclose(outFile);

    dmFree(tmpBuf);

    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 (optInType)
    {
        case FFMT_CHAR:
            outSize    = D64_CHR_SIZE;
            outWidth   = D64_CHR_WIDTH_UT;
            outWidthPX = D64_CHR_WIDTH_PX;
            outHeight  = D64_CHR_HEIGHT_UT;
            break;

        case FFMT_SPRITE:
            outSize    = D64_SPR_SIZE;
            outWidth   = D64_SPR_WIDTH_UT;
            outWidthPX = D64_SPR_WIDTH_PX;
            outHeight  = D64_SPR_HEIGHT_UT;
            break;

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

    offs = 0;
    itemCount = 0;

    if (optOutType == FFMT_ANSI || optOutType == 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 out;
        }

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

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

        fclose(outFile);
    }
    else
    if (optOutType == 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 out;
            }

            outImage = dmImageAlloc(outWidthPX, outHeight, DM_PIXFMT_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 out;
            }

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

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

        if ((err = dmC64SetImagePalette(outImage, &optC64Spec, FALSE)) != DMERR_OK)
        {
            dmErrorMsg("Could not allocate C64 palette for output image: %d\n", err);
            goto out;
        }

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

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

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

                ret = dmWriteImage(outFilename, outImage, &optSpec,
                    &dmImageFormatList[optOutFormat]);

                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,
                &dmImageFormatList[optOutFormat]);

            if (ret != DMERR_OK)
            {
                dmError(ret, "Error writing output image '%s': %s.\n",
                    optOutFilename, dmErrorStr(ret));
            }
        }

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

out:
    return ret;
}


int main(int argc, char *argv[])
{
    FILE *inFile = NULL;
    const DMC64ImageFormat *inC64Fmt = NULL;
    DMConvFormat inFormat, outFormat;
    DMC64Image *inC64Image = NULL, *outC64Image = NULL;
    DMImage *inImage = NULL, *outImage = NULL;
    Uint8 *dataBuf = NULL, *dataBufOrig = NULL;
    size_t dataSize, dataSizeOrig, dataRealOffs;
    int i, n, res = DMERR_OK;

    // Default color mapping
    for (i = 0; i < D64_NCOLORS; i++)
        optColorMap[i] = i;

    // Initialize c64 image conversion spec
    memset(&optC64Spec, 0, sizeof(optC64Spec));

    // Initialize list of additional conversion formats
    if ((res = dmLib64GFXInit()) != DMERR_OK)
    {
        dmErrorMsg("Could not initialize lib64gfx: %s\n",
            dmErrorStr(res));
        goto out;
    }

    nconvFormatList = ndmImageFormatList + ndmPaletteFormatList + nbaseFormatList;
    convFormatList = dmCalloc(nconvFormatList, sizeof(DMConvFormat));

    for (n = i = 0; i < ndmImageFormatList; i++)
    {
        const DMImageFormat *sfmt = &dmImageFormatList[i];
        DMConvFormat *dfmt = &convFormatList[n++];
        dfmt->name   = sfmt->name;
        dfmt->fext   = sfmt->fext;
        dfmt->flags  = sfmt->flags;
        dfmt->type   = FFMT_IMAGE;
        dfmt->format = sfmt->fmtid;
    }

    for (i = 0; i < ndmPaletteFormatList; i++)
    {
        const DMPaletteFormat *sfmt = &dmPaletteFormatList[i];
        DMConvFormat *dfmt = &convFormatList[n++];
        dfmt->name   = sfmt->name;
        dfmt->fext   = sfmt->fext;
        dfmt->flags  = sfmt->flags;
        dfmt->type   = FFMT_PALETTE;
        dfmt->format = sfmt->fmtid;
    }


    for (i = 0; i < nbaseFormatList; i++)
        memcpy(&convFormatList[n++], &baseFormatList[i], sizeof(DMConvFormat));

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

    if (!dmArgsProcess(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, OPTH_BAILOUT))
        goto out;

    switch (optShowHelp)
    {
        case 1:
            argShowHelp();
            goto out;

        case 2:
            argShowHelp();
            argShowFormats();
            argShowC64Formats(stdout, TRUE, TRUE);
            argShowC64PaletteHelp(stdout);

            for (int n = 0; n < optListN; n++)
            {
                const char *str = argGetHelpTopic(optList[n].id);
                if (str != NULL)
                    fprintf(stdout, "\n%s\n", str);
            }
            goto out;

        case 3:
            argShowFormats();
            argShowC64Formats(stdout, TRUE, dmVerbosity > 0);
            goto out;
    }

    // Determine input format, if not specified
    if (optInType == FFMT_AUTO && optInFilename != NULL)
    {
        char *dext = strrchr(optInFilename, '.');
        if (dext)
        {
            if (dmGetFormatByExt(dext + 1, &optInType, &optInFormat) ||
                dmGetC64FormatByExt(dext + 1, &optInType, &optInFormat))
            {
                dmMsg(3, "Guessed input type as %s\n",
                    formatTypeList[optInType]);
            }
        }
    }

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

    // Determine output format, if not specified
    if (optOutType == FFMT_AUTO && optOutFilename != NULL)
    {
        char *dext = strrchr(optOutFilename, '.');
        if (dext)
        {
            if (dmGetFormatByExt(dext + 1, &optOutType, &optOutFormat) ||
                dmGetC64FormatByExt(dext + 1, &optOutType, &optOutFormat))
            {
                dmMsg(3, "Guessed output type as %s\n",
                    formatTypeList[optOutType]);
            }
        }
    }
    else
    if (optOutType == FFMT_AUTO)
        optOutType = FFMT_ASCII;

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

    if ((res = dmReadDataFile(inFile, NULL, &dataBufOrig, &dataSizeOrig)) != DMERR_OK)
    {
        dmErrorMsg("Could not read input: %s.\n", dmErrorStr(res));
        goto out;
    }

    fclose(inFile);

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

    if (optInSkipNeg)
    {
        dataBuf = dataBufOrig + dataSizeOrig - optInSkip;
        dataSize = optInSkip;
        dataRealOffs = dataSizeOrig - optInSkip;

        dmMsg(1, "Input skip -%d (-0x%x). "
            "Offset %" DM_PRIu_SIZE_T " (0x%" DM_PRIx_SIZE_T "), "
            "size %" DM_PRIu_SIZE_T " (0x%" DM_PRIx_SIZE_T ").\n",
            optInSkip, optInSkip,
            dataRealOffs, dataRealOffs,
            dataSize, dataSize);

    }
    else
    {
        dataBuf = dataBufOrig + optInSkip;
        dataSize = dataSizeOrig - optInSkip;
        dataRealOffs = optInSkip;

        dmMsg(1, "Input skip %d (0x%x), "
            "size %" DM_PRIu_SIZE_T " (0x%" DM_PRIx_SIZE_T ").\n",
            optInSkip, optInSkip,
            dataSize, dataSize);
    }

    // Check for forced input format here
    if (optForcedInSubFormat >= 0)
        optInFormat = optForcedInSubFormat;

    // Perform probing, if required
    if (optInType == FFMT_AUTO || optInType == FFMT_BITMAP)
    {
        // Probe for format
        const DMC64ImageFormat *forced = NULL;
        DMGrowBuf tbuf;

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

        res = dmC64DecodeBMP(&inC64Image,
            dmGrowBufConstCreateFrom(&tbuf, dataBuf, dataSize),
            -1, -1, &inC64Fmt, forced);

        if (forced == NULL && inC64Fmt != NULL && res == DMERR_OK)
        {
            dmMsg(1, "Probed '%s' format image, type %d, %s\n",
                inC64Fmt->name, inC64Fmt->format->mode, inC64Fmt->fext);

            optInType = FFMT_BITMAP;
        }
        else
        if (res != DMERR_OK && (forced != NULL || optInType == FFMT_BITMAP))
        {
            dmErrorMsg("Could not decode input image: %s.\n", dmErrorStr(res));
            goto out;
        }
    }

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

    if (optInType == FFMT_AUTO || optInType == FFMT_PALETTE)
    {
        const DMPaletteFormat *pfmt = NULL;
        int index;
        dmMsg(4, "Trying to probe palette formats.\n");
        if (dmPaletteProbeGeneric(dataBuf, dataSize, &pfmt, &index) > 0 &&
            pfmt->read != NULL)
        {
            optInType = FFMT_PALETTE;
            optInFormat = index;
            dmMsg(1, "Probed '%s' format palette.\n", pfmt->name);
        }
    }

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

    if (dmGetConvFormat(optInType, optInFormat, &inFormat) &&
        dmGetConvFormat(optOutType, optOutFormat, &outFormat))
    {
        dmMsg(1, "Attempting conversion %s (%s) -> %s (%s)\n",
            inFormat.name, inFormat.fext,
            outFormat.name, outFormat.fext);
    }

    // Check if we need to scale the output
    if (optScaleMode != SCALE_SET)
    {
        // Default to 1:1 scalefactors
        int scaleX = 1, scaleY = 1;

        // For C64 formats, use the aspect ratios from them
        if (inC64Fmt != NULL)
        {
            scaleX = inC64Fmt->format->aspectX;
            scaleY = inC64Fmt->format->aspectY;
        }

        // Then, depending on the scaling mode, apply scale
        switch (optScaleMode)
        {
            case SCALE_AUTO:
                optSpec.scaleX = scaleX;
                optSpec.scaleY = scaleY;
                break;

            case SCALE_RELATIVE:
                optSpec.scaleX *= scaleX;
                optSpec.scaleY *= scaleY;
                break;
        }
    }

    // Handle palette stuff that is generic for different operation modes
    if (optPaletteFile != NULL &&
        (res = dmHandleExternalPalette(optPaletteFile, &optPaletteData)) != DMERR_OK)
        goto out;

    switch (optInType)
    {
        case FFMT_SPRITE:
        case FFMT_CHAR:
        case FFMT_BITMAP:
            if (optPaletteData == NULL)
            {
                // No palette file specified, use internal palette
                if (optC64Palette == NULL)
                    optC64Palette = &dmC64DefaultPalettes[0];

                dmMsg(1, "Using internal palette '%s' (%s).\n",
                    optC64Palette->name, optC64Palette->desc);

                if ((res = dmC64PaletteFromC64Palette(&optPaletteData, optC64Palette, FALSE)) != DMERR_OK)
                {
                    dmErrorMsg("Could not set up palette: %s.\n",
                        dmErrorStr(res));
                    goto out;
                }
            }

            if (optPaletteData->ncolors < D64_NCOLORS)
            {
                dmErrorMsg("Palette does not have enough colors (%d < %d)\n",
                    optPaletteData->ncolors, D64_NCOLORS);
                goto out;
            }

            if (optPaletteData->ncolors > D64_NCOLORS)
            {
                dmMsg(1, "Palette has %d colors, using only first %d.\n",
                    optPaletteData->ncolors, D64_NCOLORS);
            }

            optC64Spec.pal = optPaletteData;
            break;

        default:
            if (optC64Palette != NULL)
            {
                dmMsg(1, "Using internal palette '%s' (%s).\n",
                    optC64Palette->name, optC64Palette->desc);

                if ((res = dmC64PaletteFromC64Palette(&optPaletteData, optC64Palette, FALSE)) != DMERR_OK)
                {
                    dmErrorMsg("Could not set up palette: %s.\n",
                        dmErrorStr(res));
                    goto out;
                }
            }
    }

    switch (optInType)
    {
        case FFMT_SPRITE:
        case FFMT_CHAR:
            dmDumpSpritesAndChars(dataBuf, dataSize, dataRealOffs);
            break;

        case FFMT_PALETTE:
            {
                const DMPaletteFormat *pfmt = &dmPaletteFormatList[optInFormat];
                DMResource *fp;

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

                // Read palette file
                if ((res = dmf_open_memio(NULL, optInFilename, dataBuf, dataSize, &fp)) != DMERR_OK)
                {
                    dmErrorMsg("Could not create MemIO handle for input.\n");
                    goto out;
                }

                // Read input
                if (pfmt->read != NULL)
                    res = pfmt->read(fp, &optPaletteData);
                else
                    dmErrorMsg("Unsupported input palette format.\n");

                dmf_close(fp);

                if (res != DMERR_OK)
                {
                    dmErrorMsg("Palette could not be read.\n");
                    goto out;
                }
            }

            if (optPaletteData == NULL)
                goto out;

            switch (optOutType)
            {
                case FFMT_PALETTE:
                    res = dmWritePalette(optOutFilename, optPaletteData, &dmPaletteFormatList[optOutFormat]);
                    break;

                case FFMT_IMAGE:
                    // Allocate image
                    if ((inImage = dmImageAlloc(16, 16, DM_PIXFMT_PALETTE,
                        dmGetNPlanesFromNColors(optPaletteData->ncolors))) == NULL)
                    {
                        res = dmError(DMERR_MALLOC,
                            "Could not allocate memory for image.\n");
                        goto out;
                    }

                    if ((res = dmPaletteCopy(&inImage->pal, optPaletteData)) != DMERR_OK)
                    {
                        dmErrorMsg("Could not allocate image palette: %s\n",
                            dmErrorStr(res));
                        goto out;
                    }

                    res = dmWriteImage(optOutFilename, inImage, &optSpec,
                        &dmImageFormatList[optOutFormat]);
                    break;

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

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

            switch (optOutType)
            {
                case FFMT_IMAGE:
                case FFMT_CHAR:
                case FFMT_SPRITE:
                    // Set character data if required
                    if ((inC64Image->extraInfo[D64_EI_MODE] & D64_FMT_CHAR) &&
                        inC64Image->charData[0].data == NULL)
                    {
                        // Check character ROM filename
                        if (optCharROMFilename == NULL)
                            optCharROMFilename = dmGetChargenROMPath();

                        // Attempt to read character ROM
                        dmMsg(1, "Using character ROM file '%s'.\n",
                            optCharROMFilename);

                        if ((res = dmReadDataFile(NULL, optCharROMFilename,
                            &inC64Image->charData[0].data,
                            &inC64Image->charData[0].size)) != DMERR_OK)
                        {
                            dmErrorMsg("Could not read character ROM from '%s'.\n",
                                optCharROMFilename);
                            goto out;
                        }
                    }

                    // Convert the image
                    res = dmC64ConvertBMP2Image(&outImage, inC64Image, &optC64Spec);

                    if (res != DMERR_OK || outImage == NULL)
                    {
                        dmErrorMsg("Error in bitmap to image conversion: %s.\n",
                            dmErrorStr(res));
                        goto out;
                    }

                    switch (optOutType)
                    {
                        case FFMT_IMAGE:
                            res = dmWriteImage(optOutFilename, outImage, &optSpec,
                                &dmImageFormatList[optOutFormat]);
                            break;

                        case FFMT_CHAR:
                        case FFMT_SPRITE:
                            res = dmWriteSpritesAndChars(optOutFilename, outImage,
                                optOutType, optInMulticolor);
                            break;
                    }
                    break;

                case FFMT_PALETTE:
                    res = dmWritePalette(optOutFilename, optPaletteData, &dmPaletteFormatList[optOutFormat]);
                    break;

                case FFMT_DUMP:
                    dmDumpC64Bitmap(optOutFilename, inC64Image);
                    break;

                case FFMT_BITMAP:
                    if ((res = dmConvertC64Bitmap(&outC64Image, inC64Image,
                        &dmC64ImageFormats[optOutFormat], inC64Fmt)) != DMERR_OK)
                    {
                        dmErrorMsg("Error in bitmap format conversion.\n");
                        goto out;
                    }
                    if (dmVerbosity >= 2)
                    {
                        dmPrint(0, "INPUT:\n");  dmC64ImageDump(stderr, inC64Image, inC64Fmt, "  ");
                        dmPrint(0, "OUTPUT:\n"); dmC64ImageDump(stderr, outC64Image, &dmC64ImageFormats[optOutFormat], "  ");
                    }
                    res = dmWriteBitmap(optOutFilename, outC64Image, &dmC64ImageFormats[optOutFormat]);
                    break;

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

        case FFMT_IMAGE:
            {
                const DMImageFormat *ifmt = &dmImageFormatList[optInFormat];
                DMResource *fp;

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

                if ((res = dmf_open_memio(NULL, optInFilename, dataBuf, dataSize, &fp)) != DMERR_OK)
                {
                    dmErrorMsg("Could not create MemIO handle for input.\n");
                    goto out;
                }

                // Read input
                if (ifmt->read != NULL)
                    res = ifmt->read(fp, &inImage);
                else
                    dmErrorMsg("Unsupported input image format for image conversion.\n");

                dmf_close(fp);

                if (res != DMERR_OK || inImage == NULL)
                    goto out;

                switch (optOutType)
                {
                    case FFMT_IMAGE:
                        res = dmWriteImage(optOutFilename, inImage, &optSpec,
                            &dmImageFormatList[optOutFormat]);
                        break;

                    case FFMT_PALETTE:
                        if (inImage->pal == NULL || inImage->pixfmt != DM_PIXFMT_PALETTE)
                        {
                            dmErrorMsg("Source image is not a paletted format or has no palette.\n");
                            goto out;
                        }
                        res = dmWritePalette(optOutFilename, inImage->pal, &dmPaletteFormatList[optOutFormat]);
                        break;

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

                    case FFMT_BITMAP:
                        {
                            DMC64Image *tmpC64Image = NULL;
                            res = dmC64ConvertImage2BMP(&tmpC64Image, inImage,
                                &dmC64ImageFormats[optOutFormat], &optC64Spec);

                            if (res != DMERR_OK || tmpC64Image == NULL)
                            {
                                dmC64ImageFree(tmpC64Image);
                                dmErrorMsg("Error in image to bitmap conversion: %s.\n",
                                    dmErrorStr(res));
                                goto out;
                            }

                            if ((res = dmConvertC64Bitmap(&outC64Image, tmpC64Image,
                                &dmC64ImageFormats[optOutFormat], &dmC64ImageFormats[optOutFormat])) != DMERR_OK)
                            {
                                dmC64ImageFree(tmpC64Image);
                                dmErrorMsg("Error in bitmap format conversion: %s.\n",
                                    dmErrorStr(res));
                                goto out;
                            }

                            res = dmWriteBitmap(optOutFilename, outC64Image, &dmC64ImageFormats[optOutFormat]);
                            dmC64ImageFree(tmpC64Image);
                        }
                        break;

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

    if (res != DMERR_OK)
    {
        dmErrorMsg("Error writing output data: %s\n",
            dmErrorStr(res));
    }

out:
    // Cleanup
    dmFree(convFormatList);
    dmFree(dataBufOrig);
    dmPaletteFree(optPaletteData);
    dmC64ImageFree(inC64Image);
    dmC64ImageFree(outC64Image);
    dmImageFree(inImage);
    dmImageFree(outImage);
    dmLib64GFXClose();

    return res;
}