view tools/gfxconv.c @ 2295:046056326041

Show error code message when bitmap image conversion fails.
author Matti Hamalainen <ccr@tnsp.org>
date Wed, 03 Jul 2019 13:57:42 +0300
parents 9269a32ba7f9
children de570106b6cc
line wrap: on
line source

/*
 * gfxconv - Convert various graphics formats
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2012-2019 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "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_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",
};


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


enum
{
    SCALE_SET,
    SCALE_RELATIVE,
    SCALE_AUTO,
};


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

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;
BOOL    optInSkipNeg = FALSE;

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

BOOL    optInMulticolor = FALSE,
        optSequential = FALSE,
        optRemapColors = FALSE,
        optRemapRemove = FALSE,
        optUsePalette = FALSE;
int     optNRemapTable = 0,
        optScaleMode = SCALE_AUTO;
DMMapValue optRemapTable[DM_MAX_COLORS];
int     optColorMap[D64_NCOLORS];
char    *optCharROMFilename = NULL;
DMC64Palette *optC64Palette = NULL;
char    *optPaletteFile = 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 },
    { 15, 'v', "verbose",       "Increase verbosity", OPT_NONE },
    {  3, 'o', "output",        "Output filename", OPT_ARGREQ },
    {  4, 's', "skip",          "Skip N bytes in input from start (negative value will be offset from input end)", OPT_ARGREQ },
    {  1, 'i', "informat",      "Set input format (see --formats)", OPT_ARGREQ },
    {  5, 'f', "format",        "Set output 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, 'm', "colormap",      "Set color index 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 specified value(s) (see below)", 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 plane (some output formats)", OPT_ARGREQ },
    { 14, 'I', "interleave",    "Interleaved/planar output (some output formats)", OPT_NONE },
    { 20, 'C', "compress",      "Use compression -C <0-9>, 0 = disable, default is 9", OPT_ARGREQ },
    { 16, 'R', "remap",         "Remap output image colors (-R <(#RRGGBB|index):index>[,<..>][+remove] | -R @map.txt[+remove])", OPT_ARGREQ },
    { 21,   0, "char-rom",      "Set character ROM file to be used.", OPT_ARGREQ },
    { 22, 'p', "palette" ,      "Set C64 palette to be used (see list with -p help).", 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"
    );

    argShowC64Formats(stdout, TRUE);
}


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

    fprintf(stdout,
    "\n"
    "Output image scaling (-S)\n"
    "-------------------------\n"
    "Scaling option '-S <n>', '-S <x>:<y>', '-S <x>:<y>*<n>' can be used to set\n"
    "the direct or relative scale integer factor(s). '-S <n>' sets both height\n"
    "and width scale factor to <n>. '-S <x>:<y>*<n>' scales width by X*n and\n"
    "height Y*n. Certain input formats set their default aspect/scale factors.\n"
    "By prepending -S parameters with asterisk ('*') you can scale relative to\n"
    "those values. (e.g. '-S *2' for example.) NOTE! Only integer scale factors\n"
    "can be used at the moment.\n"
    "\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 always desirable, for example\n"
    "when converting multiple images to same palette. You can also specify the\n"
    "+remove option by itself to remove all unused colors: -R +remove\n"
    "\n"
    "Color index mapping (-m)\n"
    "------------------------\n"
    "Color index map definitions are used for sprite/char data input (and ANSI\n"
    "output), to set what colors of the C64 palette are used for each single\n"
    "color/multi color bit-combination.\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"
    "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; -m 255,2 would use transparency for\n"
    "'0' bits and and C64 color 2 for '1' bits.\n"
    "\n"
    "Default character ROM file for this build is:\n"
    "%s\n",
    DM_DEF_CHARGEN
    );
}


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


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, NULL))
        {
            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, NULL))
    {
        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], 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();
        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, NULL);
}


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, &optInSkipNeg))
            {
                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, D64_NCOLORS, FALSE, "color index option"))
                    return FALSE;

                dmMsg(1, "Set color index 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, NULL) ||
                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;

                // 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, 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, NULL) ||
                tmpUInt < 1 || tmpUInt > 512)
            {
                dmErrorMsg("Invalid planed width value '%s' [1 .. 512]\n",
                    optArg);
                return FALSE;
            }
            optPlanedWidth = tmpUInt;
            break;

        case 12:
            optUsePalette = TRUE;
            break;

        case 13:
            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 18:
            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 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;

        case 20:
            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 21:
            optCharROMFilename = optArg;
            break;

        case 22:
            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, 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 < D64_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 < 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);
            fprintf(outFile, " ");
            bufOffs++;
        }
        fprintf(outFile, "\n");
    }
}


int dmRemapImageColors(DMImage **pdst, const DMImage *src)
{
    DMPalette *tmpPal = NULL;
    int  *mapping = dmMalloc(src->pal->ncolors * sizeof(int));
    BOOL *mapped  = dmMalloc(src->pal->ncolors * sizeof(BOOL));
    BOOL *used    = dmMalloc(src->pal->ncolors * sizeof(BOOL));
    int n, index, xc, yc, ncolors, res = DMERR_OK;
    DMImage *dst;

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

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


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

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

    for (index = 0; index < src->pal->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->pal->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->pal->ncolors; n++)
            {
                if (dmCompareColor(&(src->pal->colors[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->pal->ncolors; index++)
    if (mapping[index] < 0 &&
        (!optRemapRemove || (optRemapRemove && used[index])))
    {
        for (n = 0; n < src->pal->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->pal->ncolors; index++)
    {
        if (mapping[index] + 1 > ncolors)
            ncolors = mapping[index] + 1;
    }

    // Copy palette entries
    for (index = 0; index < src->pal->ncolors; index++)
    {
        if (mapping[index] >= 0)
        {
            memcpy(&tmpPal->colors[mapping[index]], &(src->pal->colors[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->pal->ncolors &&
                mapping[col] >= 0 &&
                mapping[col] < src->pal->ncolors)
                dp[xc] = mapping[col];
            else
                dp[xc] = 0;
        }
    }

    // Set new palette, free memory
    if ((res = dmPaletteCopy(&dst->pal, tmpPal)) != DMERR_OK)
    {
        res = dmError(DMERR_MALLOC,
            "Could not allocate memory for final remapped palette.\n");
        goto error;
    }

error:
    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, "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;

    // 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 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->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=%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 DMImageWriteSpec *spec)
{
    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);

    dmf_close(fp);
    return res;
}


int dmWriteImage(const char *filename, DMImage *pimage, DMImageWriteSpec *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 && image->pixfmt == DM_PIXFMT_PALETTE)
    {
        if ((res = dmRemapImageColors(&image, pimage)) != DMERR_OK)
            return res;

        allocated = TRUE;
    }

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

                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;

        if (info)
        {
            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.\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 + 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 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 ((tmpBuf = 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(tmpBuf, image,
                    bx * D64_CHR_WIDTH_PX, by * D64_CHR_HEIGHT_PX,
                    multicolor))
                {
                    ret = DMERR_DATA_ERROR;
                    goto error;
                }
                break;

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

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

error:
    // 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 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_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 error;
            }

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

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

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

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

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

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

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

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

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

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

        dmMsg(1, "Input skip %d (0x%x), size %d (0x%x).\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),
            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->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 exit;
        }
    }

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

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

    if (optScaleMode != SCALE_SET)
    {
        int scaleX = 1, scaleY = 1;
        if (inC64Fmt != NULL)
        {
            scaleX = inC64Fmt->format->aspectX;
            scaleY = inC64Fmt->format->aspectY;
        }

        switch (optScaleMode)
        {
            case SCALE_AUTO:
                optSpec.scaleX = scaleX;
                optSpec.scaleY = scaleY;
                break;

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

    if (optPaletteFile != NULL)
    {
        if ((res = dmHandleExternalPalette(optPaletteFile, &optC64Spec.pal)) != DMERR_OK)
            goto exit;

        if (optC64Spec.pal->ncolors < D64_NCOLORS)
        {
            dmErrorMsg("Palette does not have enough colors (%d < %d)\n",
                optC64Spec.pal->ncolors, D64_NCOLORS);
            goto exit;
        }
    }
    else
    {
        // 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);

        optC64Spec.cpal = optC64Palette;

        if ((res = dmC64PaletteFromC64Palette(&optC64Spec.pal, optC64Palette, FALSE)) != DMERR_OK)
        {
            dmErrorMsg("Could not setup palette: %s\n",
                dmErrorStr(res));
            goto exit;
        }
    }

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

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

            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 = DM_DEF_CHARGEN;

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

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

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

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

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

                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], &optC64Spec);

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

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

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

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

    return 0;
}