view gfxconv.c @ 510:43ea59887c69

Start work on making C64 formats encoding possible by changing DMDecodeOps to DMEncDecOps and adding fields and op enums for custom encode functions, renaming, etc. Split generic op sanity checking into a separate function in preparation for its use in generic encoding function.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 19 Nov 2012 15:06:01 +0200
parents e8e244036ee4
children 6f141f760c54
line wrap: on
line source

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

//#define UNFINISHED 1

#define DM_MAX_COLORS 256

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

enum
{
    FFMT_AUTO = 0,

    FFMT_ASCII,
    FFMT_ANSI,
    FFMT_IMAGE,

    FFMT_CHAR,
    FFMT_SPRITE,
    FFMT_BITMAP,

    FFMT_LAST
};


typedef struct
{
    char *name;
    char *fext;
    BOOL in, out;
    int format;
    int subformat;
} DMConvFormat;


static DMConvFormat convFormatList[] =
{
    {
        "ASCII text", "asc", FALSE, TRUE,
        FFMT_ASCII  , 0,
    },
    {
        "ANSI colored text", "ansi", FALSE, TRUE,
        FFMT_ANSI   , 0,
    },
    {
        "PNG image file", "png", TRUE, TRUE,
        FFMT_IMAGE  , IMGFMT_PNG,
    },
    {
        "PPM image file", "ppm", FALSE, TRUE,
        FFMT_IMAGE  , IMGFMT_PPM,
    },
    {
        "PCX image file", "pcx", TRUE, TRUE,
        FFMT_IMAGE  , IMGFMT_PCX,
    },
    {
        "IFF ILBM file", "lbm", TRUE, FALSE,
        FFMT_IMAGE  , IMGFMT_ILBM,
    },

    {
        "IFFMaster RAW image file", "araw", FALSE, TRUE,
        FFMT_IMAGE  , IMGFMT_ARAW,
    },

    {
        "C64 bitmap image file", NULL, TRUE, FALSE,
        FFMT_BITMAP , 0,
    },

    {
        "C64 character/font data", "chr", TRUE, TRUE,
        FFMT_CHAR   , 0
    },
    {
        "C64 sprite data", "spr", TRUE, TRUE,
        FFMT_SPRITE , 0
    },
};

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


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



char    *optInFilename = NULL,
        *optOutFilename = NULL;
int     optInFormat = FFMT_AUTO,
        optOutFormat = FFMT_ASCII,
        optInSubFormat = IMGFMT_PNG,
        optOutSubFormat = IMGFMT_PNG,
        optItemCount = -1,
        optPlanedWidth = 1,
        optForcedFormat = -1;
int     optInSkip = 0;
BOOL    optInMulticolor = FALSE,
        optSequential = FALSE,
        optRemapColors = FALSE,
        optRemapRemove = FALSE;
int     optNRemapTable = 0;
DMMapValue optRemapTable[DM_MAX_COLORS];
int     optColors[C64_MAX_COLORS];

DMImageSpec optSpec =
{
    .scale = 1,
    .nplanes = 4,
    .interleave = FALSE,
    .paletted = FALSE,
    .format = 0,
};

static DMOptArg optList[] =
{
    {  0, '?', "help",         "Show this help", OPT_NONE },
    { 15, 'v', "verbose",      "Increase verbosity", OPT_NONE },
    {  3, 'o', "output",       "Output filename", OPT_ARGREQ },
    {  1, 'i', "informat",     "Set input format ([s]prite, [c]har, [b]itmap, [i]mage)", OPT_ARGREQ },
    {  2, 'm', "multicolor",   "Input is multicolor / output in multicolor", OPT_NONE },
    {  4, 's', "skip",         "Skip bytes in input", OPT_ARGREQ },
    {  5, 'f', "format",       "Output format (see --formats)", OPT_ARGREQ },
    { 17, 'F', "formats",      "Output format (see list below)", OPT_NONE },
    {  8, 'q', "sequential",   "Output sequential files (image output only)", OPT_NONE },
    {  6, 'c', "colormap",     "Color mappings (see below for information)", OPT_ARGREQ },
    {  7, 'n', "numitems",     "How many 'items' to view (default: all)", OPT_ARGREQ },
    {  9, 'S', "scale",        "Scale output by x (image output only)", OPT_ARGREQ },
    { 10, 'b', "bformat",      "Force input bitmap format (see below)", OPT_ARGREQ },
    { 11, 'w', "width",        "Item width (number of items per row, min 1)", OPT_ARGREQ },
    { 12, 'P', "paletted",     "Use indexed/paletted output (png, pcx output only)", OPT_NONE },
    { 13, 'B', "bplanes",      "Bits per pixel OR # of bitplanes (certain output formats)", OPT_ARGREQ },
    { 14, 'I', "interleave",   "Interleave scanlines (default: output whole planes)", OPT_NONE },
    { 16, 'R', "remap",        "Remap output image colors (-R <(#RRGGBB|index):index>[,<..>] | -R @map.txt)", OPT_ARGREQ },
    { 18, 'r', "remap-remove", "Remove unused colors from remapped palette (requires -R)", OPT_NONE },
};

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


void argShowFormats()
{
    int i;

    printf("\n"
    "Available input/output formats:\n"
    "  EXT | I | O | Description\n"
    "------+---+---+--------------------------------\n"
    );
    
    for (i = 0; i < nconvFormatList; i++)
    {
        DMConvFormat *fmt = &convFormatList[i];
        printf("%-5s | %c | %c | %s\n",
            fmt->fext ? fmt->fext : "",
            fmt->in ? 'X' : ' ',
            fmt->out ? 'X' : ' ',
            fmt->name);
    }

    printf("\nAvailable bitmap formats:\n");
    for (i = 0; i < ndmC64ImageFormats; i++)
    {
        DMC64ImageFormat *fmt = &dmC64ImageFormats[i];
        printf("%3d | %-5s | %-15s | %s\n",
            i, fmt->extension,
            dmC64ImageTypeNames[fmt->type],
            fmt->name);
    }
}


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

    printf(
    "\n"
    "Color map definitions are used for ANSI and image output, to declare what\n"
    "output colors of the C64 palette are used for each single color/multi color\n"
    "bit-combination. For example, if the input is multi color sprite or char,\n"
    "you can define colors like: -c 0,8,3,15 .. for single color: -c 0,1\n"
    "The numbers are palette indexes, and the order is for bit(pair)-values\n"
    "00, 01, 10, 11 (multi color) and 0, 1 (single color). NOTICE! 255 is the\n"
    "special color that can be used for transparency.\n"
    );
}


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


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

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


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

    if (opt == NULL)
        goto error;

    if ((end = split = strchr(opt, ':')) == NULL)
    {
        dmError("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 == '%')
    {
        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)
        {
            dmError("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))
        {
            dmError("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))
    {
        dmError("Invalid %s value '%s', could not parse destination value '%s'.\n", msg, popt, split);
        goto error;
    }

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

    if (value->to < 0 || value->to > nmax)
    {
        dmError("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
    {
        int *value = (int *) pvalue;
        char *split = strchr(opt, ':');
        if (split != NULL)
        {
            dmError("Unexpected ':' in indexed %s '%s'.\n", msg, opt);
            return FALSE;
        }

        if (!dmGetIntVal(opt, &value[index]))
        {
            dmError("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 *end, *start = opt;

    *nvalues = 0;
    while (*nvalues < nmax && *start && (end = strchr(start, ',')) != NULL)
    {
        if (!dmParseMapOptionItem(start, end, values, *nvalues, nmax, requireIndex, msg))
            return FALSE;

        start = end + 1;
        (*nvalues)++;
    }
    
    if (*start && *nvalues < nmax)
    {
        if (!dmParseMapOptionItem(start, NULL, values, *nvalues, nmax, requireIndex, msg))
            return FALSE;

        (*nvalues)++;
    }

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

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

error:
    fclose(fp);
    return res;
}


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

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

        case 15:
            dmVerbosity++;
            break;

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

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

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

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

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

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

        case 8:
            optSequential = TRUE;
            break;

        case 9:
            {
                int tmp = atoi(optArg);
                if (tmp < 1 || tmp > 50)
                {
                    dmError("Invalid scale value '%s'.\n", optArg);
                    return FALSE;
                }
                optSpec.scale = tmp;
            }
            break;

        case 11:
            {
                int tmp = atoi(optArg);
                if (tmp < 1 || tmp > 512)
                {
                    dmError("Invalid width value '%s'.\n", optArg);
                    return FALSE;
                }
                optPlanedWidth = tmp;
            }
            break;

        case 12:
            optSpec.paletted = TRUE;
            break;

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

        case 14:
            optSpec.interleave = TRUE;
            break;


        case 16:
            if (optArg[0] == '@')
            {
                if (optArg[1] != 0)
                {
                    int res;
                    if ((res = dmParseColorRemapFile(optArg + 1,
                        optRemapTable, &optNRemapTable, DM_MAX_COLORS)) != DMERR_OK)
                        return FALSE;
                }
                else
                {
                    dmError("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 18:
            optRemapRemove = TRUE;
            break;

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

    return TRUE;
}


BOOL argHandleFile(char *currArg)
{
    if (!optInFilename)
        optInFilename = currArg;
    else
    {
        dmError("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 = ASC_NBITS; i; i -= 2)
        {
            int val = (byte & (3ULL << (i - 2))) >> (i - 2);
            char ch;
            switch (format)
            {
                case FFMT_ASCII:
                    ch = dmASCIIPalette[val];
                    fprintf(out, "%c%c", ch, ch);
                    break;
                case FFMT_ANSI:
                    fprintf(out, "%c[0;%d;%dm##%c[0m",
                        0x1b,
                        1,
                        31 + optColors[val],
                        0x1b);
                    break;
            }
        }
    }
    else
    {
        for (i = ASC_NBITS; i; i--)
        {
            int val = (byte & (1ULL << (i - 1))) >> (i - 1);
            char ch;
            switch (format)
            {
                case FFMT_ASCII:
                    ch = val ? '#' : '.';
                    fputc(ch, out);
                    break;
                case FFMT_ANSI:
                    fprintf(out, "%c[0;%d;%dm %c[0m",
                        0x1b,
                        1,
                        31 + optColors[val],
                        0x1b);
                    break;
            }
        }
    }
}


void dmDumpCharASCII(FILE *outFile, const Uint8 *buf, int *offs, int format, BOOL multicolor)
{
    int yc;

    for (yc = 0; yc < C64_CHR_HEIGHT; yc++)
    {
        fprintf(outFile, "%04x : ", *offs);
        dmPrintByte(outFile, buf[yc], format, multicolor);
        fprintf(outFile, "\n");
        (*offs)++;
    }
}


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

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


#ifdef UNFINISHED
int dmConvertBMP2(DMImage *screen, const DM64Image *img)
{
    int yc;
    Uint8 *dp = screen->data;
    
    for (yc = 0; yc < screen->height; yc++)
    {
        Uint8 *d = dp;
        const int y = yc / 8, yb = yc & 7;
        const int scroffsy = y * C64_SCR_CH_WIDTH;
        const int bmoffsy = y * C64_SCR_WIDTH;
        int xc;

        for (xc = 0; xc < screen->width / 2; xc++)
        {
            const int x = xc / 4;
            const int scroffs = scroffsy + x;
            const int b = img->bitmap[0][bmoffsy + (x * 8) + yb];
            const int v = 6 - ((xc * 2) & 6);
            Uint8 c;

            switch ((b >> v) & 3)
            {
                case 0: c = img->bgcolor; break;
                case 1: c = img->screen[0][scroffs] >> 4; break;
                case 2: c = img->screen[0][scroffs] & 15; break;
                case 3: c = img->color[0][scroffs] & 15; break;
            }
            
            *d++ = c;
            *d++ = c;
        }

        dp += screen->pitch;
    }
    
    return 0;
}
#endif


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

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

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

    for (index = 0; index < image->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 < image->height; yc++)
    {
        Uint8 *dp = image->data + image->pitch * yc;
        for (xc = 0; xc < image->width; xc++)
        {
            Uint8 col = dp[xc];
            if (col < image->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 < image->ncolors; n++)
            {
                if (dmCompareColor(&(image->pal[n]), &(map->color), map->alpha))
                {
                    dmMsg(3, "RGBA match #%02x%02x%02x%02x: %d -> %d\n",
                        map->color.r, map->color.g, map->color.b, map->color.a,
                        n,
                        map->to);

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

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

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

    // Calculate final number of palette colors
    ncolors = 0;
    for (index = 0; index < image->ncolors; index++)
    {
        if (mapping[index] + 1 > ncolors)
            ncolors = mapping[index] + 1;
    }
    
    // Copy palette entries
    for (index = 0; index < image->ncolors; index++)
    {
        if (mapping[index] >= 0)
        {
            memcpy(&npal[mapping[index]], &(image->pal[index]), sizeof(DMColor));
        }
    }

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

    // Set new palette, free memory
    dmFree(image->pal);
    image->pal = npal;
    image->ncolors = ncolors;

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


int dmWriteImage(const char *filename, DMImage *image, DMImageSpec *spec, int iformat, BOOL info)
{
    if (info)
    {
        dmMsg(1, "Outputting %s image %d x %d -> %d x %d [%d]\n",
            dmImageFormatList[iformat].fext,
            image->width, image->height,
            image->width * spec->scale, image->height * spec->scale,
            spec->scale);
    }

    // Perform color remapping
    if (optRemapColors)
    {
        int res;
        if ((res = dmRemapImageColors(image)) != DMERR_OK)
            return res;
    }

    switch (iformat)
    {
#ifdef DM_USE_LIBPNG
        case IMGFMT_PNG:
            if (info) dmMsg(2, "%s output.\n", spec->paletted ? "Indexed 8bpp" : "32bit RGBA");
            spec->format = spec->paletted ? DM_IFMT_PALETTE : DM_IFMT_RGBA;
            return dmWritePNGImage(filename, image, spec);
#endif

        case IMGFMT_PPM:
            if (info) dmMsg(2, "24bit RGB output.\n");
            spec->format = DM_IFMT_RGB;
            return dmWritePPMImage(filename, image, spec);

        case IMGFMT_PCX:
            if (info) dmMsg(2, "%s output.\n", spec->paletted ? "Indexed 8bpp" : "24bit RGB");
            return dmWritePCXImage(filename, image, spec);

        case IMGFMT_ARAW:
            {
                FILE *fp;
                char *dataFilename, *fext, *tmpFilename = dm_strdup(filename);
                
                // Form data file filename
                if (tmpFilename == NULL)
                    return DMERR_MALLOC;

                fext = strrchr(tmpFilename, '.');
                if (fext != NULL)
                    *fext = 0;
                dataFilename = dm_strdup_printf("%s.inc", tmpFilename);
                dmFree(tmpFilename);

                // Open data file for writing
                if ((fp = fopen(dataFilename, "w")) == NULL)
                    dmError("Could not create '%s'.\n", dataFilename);
                dmFree(dataFilename);

                if (fp != NULL)
                {
                    // Strip extension
                    int i;
                    char *palID = dm_strdup_printf("img_%s", filename);
                    char *fext = strrchr(palID, '.');
                    if (fext != NULL)
                        *fext = 0;

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

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

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

                    fprintf(fp,
                        "%s: incbin \"%s\"\n",
                        palID, filename);

                    fclose(fp);
                    dmFree(palID);
                }

                if (info) dmMsg(2, "%d bitplanes, %s interleave.\n", spec->nplanes, spec->interleave ? "with" : "without");
                return dmWriteIFFMasterRAWImage(filename, image, spec);
            }

        default:
            return DMERR_INVALID_DATA;
    }
}


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

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

    return byte;
}


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

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

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

    return TRUE;
}


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

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

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

    return TRUE;
}


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

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

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

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

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

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

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

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

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

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

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

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

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

int dmDumpSpritesAndChars(FILE *inFile)
{
    int dataOffs, itemCount, outWidth, outWidthPX, outHeight;
    size_t bufSize;
    Uint8 *bufData;

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

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

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

    if ((bufData = dmMalloc(bufSize)) == NULL)
    {
        dmError("Could not allocate temporary buffer of %d bytes.\n", bufSize);
        return -2;
    }


    dataOffs = optInSkip;
    itemCount = 0;

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

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

        while (!feof(inFile) && !error && (optItemCount < 0 || itemCount < optItemCount))
        {
            memset(bufData, 0, bufSize);

            if (fread(bufData, 1, bufSize, inFile) != bufSize)
            {
                dmError("Could not read full bufferful (%d bytes) of data at 0x%x.\n",
                    bufSize, dataOffs);
                error = TRUE;
            }
            
            fprintf(outFile, "---- : -------------- #%d\n", itemCount);

            switch (optInFormat)
            {
                case FFMT_CHAR:
                    dmDumpCharASCII(outFile, bufData, &dataOffs, optOutFormat, optInMulticolor);
                    break;
                case FFMT_SPRITE:
                    dmDumpSpriteASCII(outFile, bufData, &dataOffs, optOutFormat, optInMulticolor);
                    break;
            }
            itemCount++;
        }

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

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

            outImage = dmImageAlloc(outWidthPX, outHeight);
            dmMsg(1, "Outputting sequence of %d images @ %d x %d -> %d x %d.\n",
                optItemCount,
                outImage->width, outImage->height,
                outImage->width * optSpec.scale, outImage->height * optSpec.scale);
        }
        else
        {
            int outIWidth, outIHeight;
            if (optItemCount <= 0)
            {
                dmError("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);
        }

        outImage->constpal = TRUE;
        outImage->pal      = dmC64Palette;
        outImage->ncolors  = C64_NCOLORS;
        outImage->ctransp  = 255;
        
        while (!feof(inFile) && (optItemCount < 0 || itemCount < optItemCount))
        {
            memset(bufData, 0, bufSize);

            if (fread(bufData, 1, bufSize, inFile) != bufSize)
            {
                dmError("Could not read full bufferful (%d bytes) of data at 0x%x.\n",
                    bufSize, dataOffs);
                break;
            }

            if ((err = dmC64ConvertCSData(outImage, outX * outWidthPX, outY * outHeight,
                bufData, outWidth, outHeight, optInMulticolor, optColors)) != DMERR_OK)
            {
                dmError("Internal error in conversion of raw data to bitmap: %d.\n", err);
                break;
            }

            if (optSequential)
            {
                outFilename = dm_strdup_printf("%s%04d.%s", optOutFilename, itemCount, convFormatList[optOutFormat].fext);
                if (outFilename == NULL)
                {
                    dmError("Could not allocate memory for filename template?\n");
                    goto error;
                }
                
                dmWriteImage(optOutFilename, outImage, &optSpec, optOutSubFormat, TRUE);
                dmFree(outFilename);
            }
            else
            {
                if (++outX >= optPlanedWidth)
                {
                    outX = 0;
                    outY++;
                }
            }
            
            itemCount++;
        }

        if (!optSequential)
        {
            dmWriteImage(optOutFilename, outImage, &optSpec, optOutSubFormat, TRUE);
        }
        
        dmImageFree(outImage);
    }

    dmFree(bufData);
    return 0;

error:
    dmFree(bufData);
    return -1;
}


int main(int argc, char *argv[])
{
    FILE *inFile;
    DMC64ImageFormat *cfmt;
    DMC64Image cimage;
    Uint8 *dataBuf = NULL;
    size_t dataSize;
    int i;

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

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

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

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

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

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

    if (dmReadDataFile(inFile, NULL, &dataBuf, &dataSize) != 0)
        goto error;

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

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

        res = dmC64DecodeBMP(&cimage, dataBuf, dataSize, optInSkip, optInSkip + 2, &cfmt, forced);
        if (forced == NULL && cfmt != NULL)
        {
            dmMsg(1,"Probed %s format image, type %d, %s\n",
                cfmt->name, cfmt->type, cfmt->extension);
        }
        
        if (res == 0)
            optInFormat = FFMT_BITMAP;
    }

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

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

    // Skip, if needed
    if (fseek(inFile, optInSkip, SEEK_SET) != 0)
    {
        int res = dmGetErrno();
        dmError("Could not seek to file position %d (0x%x): %s\n",
            optInSkip, optInSkip, dmErrorStr(res));
        goto error;
    }
    
    int inFormat = dmGetConvFormat(optInFormat, optInSubFormat),
        outFormat = dmGetConvFormat(optOutFormat, optOutSubFormat);
    
    if (inFormat != -1 && outFormat != -1)
    {
        char *inFmtName = convFormatList[inFormat].name,
             *inFmtExt = convFormatList[inFormat].fext,
             *outFmtName = convFormatList[outFormat].name,
             *outFmtExt = convFormatList[outFormat].fext;

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

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

    switch (optInFormat)
    {
        case FFMT_SPRITE:
        case FFMT_CHAR:
            dmDumpSpritesAndChars(inFile);
            break;
        
        case FFMT_BITMAP:
            {
                DMImage *outImage = NULL;
                int res = DMERR_OK;

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

                switch (optOutFormat)
                {
                    case FFMT_IMAGE:
                        if ((outImage = dmImageAlloc(C64_SCR_WIDTH, C64_SCR_HEIGHT)) == NULL)
                        {
                            dmError("Could not allocate output image surface %d x %d.\n",
                                C64_SCR_WIDTH, C64_SCR_HEIGHT);
                            goto error;
                        }
                        
                        outImage->pal      = (DMColor *) &dmC64Palette;
                        outImage->ncolors  = C64_NCOLORS;
                        outImage->constpal = TRUE;
                        
                        if (cfmt->convertFrom != NULL)
                            res = cfmt->convertFrom(outImage, &cimage);
                        else
                            res = dmC64ConvertGenericBMP2Image(outImage, &cimage);

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

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

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

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

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

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

                if (res != DMERR_OK || outImage == NULL)
                    break;
                
                switch (optOutFormat)
                {
                    case FFMT_IMAGE:
                        res = dmWriteImage(optOutFilename, outImage, &optSpec, optOutSubFormat, TRUE);
                        break;

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

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

    fclose(inFile);

    dmFree(dataBuf);
    exit(0);
    return 0;

error:
    dmFree(dataBuf);
    return -3;
    exit(3);
}