view tools/gfxconv.c @ 2208:90ec1ec89c56

Revamp the palette handling in lib64gfx somewhat, add helper functions to lib64util for handling external palette file options and add support for specifying one of the "internal" palettes or external (.act) palette file to gfxconv and 64vw.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 14 Jun 2019 05:01:12 +0300
parents cbac4912992c
children f8bba7a82ec2
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_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)" },
};

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    *optC64PaletteFile = 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"
    "Available C64 bitmap formats (-f <frmt>):\n"
    " frmt | RW | Type            | Description\n"
    "------+----+-----------------+-------------------------------------\n"
    );

    for (int i = 0; i < ndmC64ImageFormats; i++)
    {
        const DMC64ImageFormat *fmt = dmC64ImageFormatsSorted[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"
    "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 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: -m 0,8,3,15 .. for 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.\n"
    "\n"
    );
}


/* 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, &optC64PaletteFile);

        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 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->fmt->type & D64_FMT_MODE_MASK) == D64_FMT_MC &&
        (src->fmt->type & D64_FMT_MODE_MASK) == D64_FMT_HIRES)
    {
        dmC64MemBlockCopy(&dst->screen[0], &src->screen[0]);
    }
    else
    if ((dst->fmt->type & D64_FMT_MODE_MASK) == D64_FMT_HIRES &&
        (src->fmt->type & D64_FMT_MODE_MASK) == D64_FMT_MC)
    {
        // XXX TODO: Handle FLI mc->hires differently?
    }

    if ((dst->fmt->type & D64_FMT_FLI) && (src->fmt->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->fmt->type & D64_FMT_FLI) && (dst->fmt->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->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 *buf = 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 ((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 * D64_CHR_WIDTH_PX, by * D64_CHR_HEIGHT_PX,
                    multicolor))
                {
                    ret = DMERR_DATA_ERROR;
                    goto error;
                }
                break;

            case FFMT_SPRITE:
                if (!dmConvertImage2Sprite(buf, image,
                    bx * D64_SPR_WIDTH_PX, by * D64_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    = 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->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 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 (optC64PaletteFile != NULL)
    {
        if ((res = dmHandleExternalPalette(optC64PaletteFile, &optC64Spec.pal)) != DMERR_OK)
            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 ((inC64Fmt->format->type & 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, inC64Fmt, &optC64Spec);

                    if (res != DMERR_OK || outImage == NULL)
                    {
                        dmErrorMsg("Error in bitmap to image conversion.\n");
                        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_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;
}