view tools/gfxconv.c @ 1931:410679d2fe8a

"Enable" the image->c64 bitmap conversion path in gfxconv. It does not work without the necessary bits elsewhere, though. Also add DMC64ImageConvSpec structure for delivering conversion parameters, though it is not yet used either.
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 28 Jun 2018 17:26:30 +0300
parents b49814dd8469
children c5a46cb4cce5
line wrap: on
line source

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


#define DM_MAX_COLORS 256

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


enum
{
    FFMT_AUTO = 0,

    FFMT_ASCII,
    FFMT_ANSI,
    FFMT_BITMAP,
    FFMT_CHAR,
    FFMT_SPRITE,

    FFMT_IMAGE,

    FFMT_LAST
};


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


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


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


static const DMConvFormat baseFormatList[] =
{
    { "ASCII text"                           , "asc"   , DM_FMT_WR   , FFMT_ASCII  , 0 },
    { "ANSI colored text"                    , "ansi"  , DM_FMT_WR   , FFMT_ANSI   , 0 },
    { "C64 bitmap image"                     , NULL    , DM_FMT_RDWR , FFMT_BITMAP , -1  },
    { "C64 character/font data"              , "chr"   , DM_FMT_RDWR , FFMT_CHAR   , 0 },
    { "C64 sprite data"                      , "spr"   , DM_FMT_RDWR , FFMT_SPRITE , 0 },
};

static const int 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;
unsigned int optInSkip = 0;

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

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

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

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

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


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

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

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

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


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

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


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


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

    if (opt == NULL)
        goto error;

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

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

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

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

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

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

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

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

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

    dmFree(opt);
    return TRUE;

error:
    dmFree(opt);
    return FALSE;
}


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

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

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

    return TRUE;
}


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

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

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

        (*nvalues)++;

        if (!end)
            break;

        start = end + 1;
    }

    return TRUE;
}


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

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

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

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

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

error:
    fclose(fp);
    return res;
}


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


BOOL dmParseIntValWithSep(char **arg, unsigned int *value, char *last, 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 &&
        (isxdigit(*ptr) || *ptr == 'x' || *ptr == '$') &&
        *ptr != sep)
        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 dmGetIntVal(start, value);
}


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

    switch (optN)
    {
        case 0:
            argShowHelp();
            exit(0);
            break;

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

        case 15:
            dmVerbosity++;
            break;

        case 1:
            {
                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 3:
            optOutFilename = optArg;
            break;

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

        case 5:
            {
                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 6:
            {
                int index, ncolors;
                if (!dmParseMapOptionString(optArg, optColorMap,
                    &ncolors, C64_NCOLORS, FALSE, "color table option"))
                    return FALSE;

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

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

        case 8:
            optSequential = TRUE;
            break;

        case 9:
            {
                BOOL error = FALSE;
                unsigned int tmpUInt2;
                char *tmpStr = dm_strdup(optArg),
                     *tmpOpt = tmpStr, sep;

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

                        if (sep == '*' &&
                            dmParseIntValWithSep(&tmpOpt, &tmpUInt, &sep, 0))
                        {
                            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>, <w>:<h> 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 11:
            if (!dmGetIntVal(optArg, &tmpUInt) ||
                tmpUInt < 1 || tmpUInt > 512)
            {
                dmErrorMsg("Invalid planed width value '%s' [1 .. 512]\n",
                    optArg);
                return FALSE;
            }
            optPlanedWidth = tmpUInt;
            break;

        case 12:
            optSpec.paletted = TRUE;
            break;

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

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

        case 14:
            optSpec.planar = TRUE;
            break;

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

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

            optRemapColors = TRUE;
            break;

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

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

    return TRUE;
}


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

    return TRUE;
}


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

    if (multicolor)
    {
        for (i = DM_ASC_NBITS; i; i -= 2)
        {
            int val = (byte & (3ULL << (i - 2))) >> (i - 2);
            char ch;
            switch (format)
            {
                case FFMT_ASCII:
                    ch = dmASCIIPalette[val];
                    fprintf(out, "%c%c", ch, ch);
                    break;
                case FFMT_ANSI:
                    fprintf(out, "\x1b[%sm##\x1b[0m",
                        dmC64GetANSIFromC64Color(optColorMap[val]));
                    break;
            }
        }
    }
    else
    {
        for (i = DM_ASC_NBITS; i; i--)
        {
            int val = (byte & (1ULL << (i - 1))) >> (i - 1);
            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 < C64_CHR_HEIGHT_UT; yc++)
    {
        fprintf(outFile, "%04" DM_PRIx_SIZE_T " : ", offs + yc);
        dmPrintByte(outFile, buf[yc], fmt, multicolor);
        fprintf(outFile, "\n");
    }
}


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

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


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

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

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

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

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

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

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

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

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

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

    // Fill in the rest
    if (optRemapRemove)
        dmMsg(2, "Removing unused colors.\n");

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

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

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

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

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

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


int dmConvertC64Bitmap(DMC64Image **pdst, const DMC64Image *src,
    const DMC64ImageFormat *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;

    // Try to do some simple fixups
    if ((dst->type & D64_FMT_FLI) && (src->type & D64_FMT_FLI) == 0)
    {
        dmMsg(1, "Upconverting multicolor to FLI.\n");
        for (int i = 0; i < dst->nbanks; i++)
        {
            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->type & D64_FMT_FLI) && (dst->type & D64_FMT_FLI) == 0)
    {
        dmMsg(1, "Downconverting FLI to multicolor.\n");
    }

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

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

        size = dmC64GetOpSubjectSize(op, dstFmt);
        switch (op->type)
        {
            case DO_COPY:
            case DO_SET_MEM:
            case DO_SET_MEM_HI:
            case DO_SET_MEM_LO:
            case DO_SET_OP:
                dmC64GetOpMemBlock(src, op->subject, op->bank, (const DMC64MemBlock **) &srcBlk);
                dmC64GetOpMemBlock(dst, op->subject, op->bank, (const DMC64MemBlock **) &dstBlk);
                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=%d ($%04x)\n",
                        blkname, i, 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=%d ($%04x)\n",
                                blkname, i, 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=%d ($%04x)\n",
                                blkname, i, 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=%d ($%04x)\n",
                                blkname, i, 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:
                                dmMemset(dstBlk->data, op->offs, size);
                                break;

                            default:
                                return dmError(DMERR_INTERNAL,
                                    "Unhandled op type %d in "
                                    "op #%d, offs=%d ($%04x), bank=%d, size=%d ($%04x) @ %d ($%04x)\n",
                                    op->type, i, 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 error;
    }

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

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


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

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

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

    dmf_close(fp);
    return res;
}


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

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

    if (info)
    {
        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);
    }

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

        allocated = TRUE;
    }

    // Do some format-specific adjustments and other things
    switch (fmt->fmtid)
    {
        case DM_IMGFMT_PNG:
            spec->format = spec->paletted ? DM_COLFMT_PALETTE : DM_COLFMT_RGBA;
            break;

        case DM_IMGFMT_PPM:
            spec->format = DM_COLFMT_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 err;
                }

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

                if (info)
                {
                    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, fmt->fmtid);

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

        case DM_IMGFMT_IFF:
            spec->compression = 1;
            spec->nplanes = 0;
            for (int n = 8; n >= 0;)
            {
                if (image->ncolors & (1 << n))
                {
                    spec->nplanes = n;
                    break;
                }
                else
                    n--;
            }
            break;

        default:
            spec->format = spec->paletted ? DM_COLFMT_PALETTE : DM_COLFMT_RGB;
    }

    // If no error has occured thus far, write the image
    if (res == DMERR_OK)
    {
        DMResource *fp;

        if (info)
        {
            char *str;
            switch (spec->format)
            {
                case DM_COLFMT_PALETTE : str = "indexed/paletted"; break;
                case DM_COLFMT_RGB     : str = "24bit RGB"; break;
                case DM_COLFMT_RGBA    : str = "32bit RGBA"; break;
                default                : str = "???"; break;
            }
            dmMsg(2, "Using %s output.\n", str);
        }

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

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

        dmf_close(fp);
    }

err:
    if (allocated)
        dmImageFree(image);

    return res;
}


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

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

    return byte;
}


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

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

    for (yc = 0; yc < C64_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 + C64_SPR_WIDTH_PX > image->width ||
        yoffs + C64_SPR_HEIGHT_PX > image->height)
        return FALSE;

    for (yc = 0; yc < C64_SPR_HEIGHT_UT; yc++)
    {
        for (xc = 0; xc < C64_SPR_WIDTH_PX / C64_SPR_WIDTH_UT; xc++)
        {
            const Uint8 *sp = image->data + ((yc + yoffs) * image->pitch) + (xc * 8) + xoffs;
            buf[(yc * C64_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 *buf = NULL;
    size_t outBufSize;
    char *outType;

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

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

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

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

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

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

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

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

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

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

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

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


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

    switch (optInType)
    {
        case FFMT_CHAR:
            outSize    = C64_CHR_SIZE;
            outWidth   = C64_CHR_WIDTH_UT;
            outWidthPX = C64_CHR_WIDTH_PX;
            outHeight  = C64_CHR_HEIGHT_UT;
            break;

        case FFMT_SPRITE:
            outSize    = C64_SPR_SIZE;
            outWidth   = C64_SPR_WIDTH_UT;
            outWidthPX = C64_SPR_WIDTH_PX;
            outHeight  = C64_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 error;
        }

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

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

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

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

        dmSetDefaultC64Palette(outImage);

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

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

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

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

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

            offs += outSize;
            itemCount++;
        }

        if (!optSequential)
        {
            ret = dmWriteImage(optOutFilename, outImage, &optSpec,
                &dmImageFormatList[optOutFormat], TRUE);
            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 error;
        }
    }

error:
    return ret;
}


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

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

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

    // Initialize list of additional conversion formats
    dmC64InitializeFormats();
    nconvFormatList = ndmImageFormatList + 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 < nbaseFormatList; i++)
        memcpy(&convFormatList[n++], &baseFormatList[i], sizeof(DMConvFormat));

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

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

    // 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 using --help\n");
            goto error;
        }
        inFile = stdin;
        optInFilename = "stdin";
    }
    else
    if ((inFile = fopen(optInFilename, "rb")) == NULL)
    {
        int res = dmGetErrno();
        dmErrorMsg("Error opening input file '%s', %d: %s\n",
              optInFilename, res, dmErrorStr(res));
        goto error;
    }

    // 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 (dmReadDataFile(inFile, NULL, &dataBufOrig, &dataSizeOrig) != 0)
        goto error;

    fclose(inFile);

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

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

    // 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;
        int res;

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

        res = dmC64DecodeBMP(&inC64Image,
            dmGrowBufConstCreateFrom(&tbuf, dataBuf, dataSize),
            0, 2, &inC64Fmt, forced);

        if (forced == NULL && inC64Fmt != NULL && res == DMERR_OK)
        {
            dmMsg(1, "Probed '%s' format image, type %d, %s\n",
                inC64Fmt->name, inC64Fmt->format->type, 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 error;
        }
    }

    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)
        {
            optInType = FFMT_IMAGE;
            optInFormat = index;
            dmMsg(1, "Probed '%s' format image.\n", ifmt->name);
        }
    }

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

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

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

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

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

                switch (optOutType)
                {
                    case FFMT_IMAGE:
                    case FFMT_CHAR:
                    case FFMT_SPRITE:
                        res = dmC64ConvertBMP2Image(&outImage, inC64Image, inC64Fmt, &imageSpecC64);

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

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

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

                    case FFMT_BITMAP:
                        if ((res = dmConvertC64Bitmap(&outC64Image, inC64Image,
                            &dmC64ImageFormats[optOutFormat], inC64Fmt)) != DMERR_OK)
                        {
                            dmErrorMsg("Error in bitmap format conversion.\n");
                            goto error;
                        }
                        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;
                }

                dmImageFree(outImage);
            }
            break;

        case FFMT_IMAGE:
            {
                const DMImageFormat *ifmt = &dmImageFormatList[optInFormat];
                DMImage *inImage = NULL;
                int res = DMERR_OK;
                DMResource *fp;

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

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

                // 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 error;

                switch (optOutType)
                {
                    case FFMT_IMAGE:
                        res = dmWriteImage(optOutFilename, inImage, &optSpec,
                            &dmImageFormatList[optOutFormat], TRUE);
                        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], &imageSpecC64);

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

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

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

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

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

                dmImageFree(inImage);
            }
            break;
    }

error:
    dmFree(convFormatList);
    dmFree(dataBufOrig);
    dmC64ImageFree(inC64Image);
    dmC64ImageFree(outC64Image);

    return 0;
}