changeset 473:73bfe73553eb

Implement palette remapping option for image outputs.
author Matti Hamalainen <ccr@tnsp.org>
date Wed, 07 Nov 2012 00:55:43 +0200
parents 0359697eeb46
children 95d1facfdb77
files gfxconv.c
diffstat 1 files changed, 362 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/gfxconv.c	Wed Nov 07 00:55:12 2012 +0200
+++ b/gfxconv.c	Wed Nov 07 00:55:43 2012 +0200
@@ -15,6 +15,12 @@
 
 //#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,
@@ -91,9 +97,13 @@
 static const int nconvFormatList = sizeof(convFormatList) / sizeof(convFormatList[0]);
 
 
-#define ASC_NBITS    8
-#define ASC_NCOLORS  4
-static const char dmASCIIPalette[ASC_NCOLORS] = ".:X#";
+typedef struct
+{
+    BOOL triplet;
+    DMColor color;
+    int from, to;
+} DMMapValue;
+
 
 
 char    *optInFilename = NULL,
@@ -107,7 +117,10 @@
         optForcedFormat = -1;
 int     optInSkip = 0;
 BOOL    optInMulticolor = FALSE,
-        optSequential = FALSE;
+        optSequential = FALSE,
+        optRemapColors = FALSE;
+int     optNRemapTable = 0;
+DMMapValue optRemapTable[DM_MAX_COLORS];
 int     optColors[C64_MAX_COLORS];
 
 DMImageSpec optSpec =
@@ -127,7 +140,8 @@
     {  1, 'i', "informat",     "Set input format ([s]prite, [c]har, [b]itmap, [i]mage)", OPT_ARGREQ },
     {  2, 'm', "multicolor",   "Input is multicolor", OPT_NONE },
     {  4, 's', "skip",         "Skip bytes in input", OPT_ARGREQ },
-    {  5, 'f', "format",       "Output format (see list below)", 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 },
@@ -137,18 +151,16 @@
     { 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 },
 };
 
 static const int optListN = sizeof(optList) / sizeof(optList[0]);
 
 
-void argShowHelp()
+void argShowFormats()
 {
     int i;
 
-    dmPrintBanner(stdout, dmProgName, "[options] <input file>");
-    dmArgsPrintHelp(stdout, optList, optListN);
-
     printf("\n"
     "Available input/output formats:\n"
     "  EXT | I | O | Description\n"
@@ -174,13 +186,20 @@
             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"
+    "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"
@@ -223,6 +242,178 @@
 }
 
 
+static BOOL dmParseMapOptionMapItem(char *opt, DMMapValue *value, const int nmax, const char *msg)
+{
+    char *split = strchr(opt, ':');
+
+    if (split == NULL)
+    {
+        dmError("Invalid %s value '%s', expected <(#|%)RRGGBB|[$|0x]index>:<[$|0x]index>.\n", msg, opt);
+        return FALSE;
+    }
+
+    *split = 0;
+
+    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)
+        {
+            colA = 0;
+            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 after #.\n", msg, opt);
+                return FALSE;
+            }
+        }
+        value->color.r = colR;
+        value->color.g = colG;
+        value->color.b = colB;
+        value->color.a = colA;
+        value->triplet = TRUE;
+    }
+    else
+    {
+        if (!dmGetIntVal(opt, &value->from))
+        {
+            dmError("Invalid %s value '%s', could not parse.\n", msg, opt);
+            return FALSE;
+        }
+        value->triplet = FALSE;
+    }
+    
+    if (!dmGetIntVal(split + 1, &value->to))
+    {
+        dmError("Invalid %s value '%s', could not parse.\n", msg, opt);
+        return FALSE;
+    }
+
+    if (!value->triplet && (value->from < 0 || value->from > 255))
+    {
+        dmError("Invalid %s map source color index value %d.\n", msg, value->from);
+        return FALSE;
+    }
+
+    if (value->to < 0 || value->to > nmax)
+    {
+        dmError("Invalid %s map destination color index value %d.\n", msg, value->to);
+        return FALSE;
+    }
+    
+    return TRUE;
+}
+
+
+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, 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)
@@ -232,6 +423,11 @@
             exit(0);
             break;
 
+        case 17:
+            argShowFormats();
+            exit(0);
+            break;
+
         case 15:
             dmVerbosity++;
             break;
@@ -283,29 +479,17 @@
 
         case 6:
             {
-                int index = 0, tmp;
-                char *s, *p = optArg;
+                int index, ncolors;
+                if (!dmParseMapOptionString(optArg, optColors,
+                    &ncolors, C64_MAX_COLORS, FALSE, "color table option"))
+                    return FALSE;
 
-                while (index < C64_MAX_COLORS && *p != 0 && (s = strchr(p, ':')) != NULL)
-                {
-                    *s = 0;
-                    if (sscanf(p, "%d", &tmp) == 1)
-                        optColors[index++] = tmp;
-                    p = s + 1;
-                }
-                
-                if (*p && index < C64_MAX_COLORS)
-                {
-                    if (sscanf(p, "%d", &tmp) == 1)
-                        optColors[index++] = tmp;
-                }
-                
                 dmMsg(1, "Set color table: ");
-                for (tmp = 0; tmp < index; tmp++)
+                for (index = 0; index < ncolors; index++)
                 {
                     dmPrint(1, "[%d:%d]%s",
-                        tmp, optColors[tmp],
-                        (tmp < index - 1) ? ", " : "");
+                        index, optColors[index],
+                        (index < ncolors) ? ", " : "");
                 }
                 dmPrint(1, "\n");
             }
@@ -367,6 +551,33 @@
             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;
+
         default:
             dmError("Unknown option '%s'.\n", currArg);
             return FALSE;
@@ -518,6 +729,117 @@
 #endif
 
 
+int dmRemapImageColors(DMImage *image)
+{
+    dmMsg(1, "Remapping %d output image colors.\n", optNRemapTable);
+    DMColor *npal = dmCalloc(image->ncolors, sizeof(DMColor));
+    int *dpal = dmMalloc(image->ncolors * sizeof(int));
+    BOOL *spal = dmCalloc(image->ncolors, sizeof(BOOL));
+    int index, xc, yc;
+
+    if (npal == NULL || spal == NULL || dpal == NULL)
+    {
+        dmError("Could not allocate memory for remapped palette.\n");
+        return DMERR_MALLOC;
+    }
+
+    for (index = 0; index < image->ncolors; index++)
+        dpal[index] = -1;
+
+    // Find and mark mapped colors    
+    for (index = 0; index < optNRemapTable; index++)
+    {
+        DMMapValue *map = &optRemapTable[index];
+        if (map->triplet)
+        {
+            BOOL found = FALSE;
+            int n;
+            for (n = 0; n < image->ncolors; n++)
+            {
+                if (memcmp(&(image->pal[n]), &(map->color), sizeof(DMColor)) == 0)
+                {
+                    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);
+                    
+                    dpal[map->to] = n;
+                    spal[n] = 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 -> %d\n",
+                index,
+                map->from, map->to);
+            
+            dpal[map->to] = map->from;
+            spal[map->from] = TRUE;
+        }
+    }
+    
+    // Fill in the rest
+    dmMsg(3, "Placing non-mapped palette entries.\n");
+    for (index = 0; index < image->ncolors; index++)
+    {
+        if (dpal[index] < 0)
+        {
+            int src;
+            for (src = 0; src < image->ncolors; src++)
+            {
+                if (!spal[src])
+                {
+                    dpal[index] = src;
+                    spal[src] = TRUE;
+                    break;
+                }
+            }
+        }
+    }
+    
+    // Copy palette entries
+    dmMsg(3, "Creating new palette.\n");
+    for (index = 0; index < image->ncolors; index++)
+    {
+        if (dpal[index] >= 0 && dpal[index] < image->ncolors)
+        {
+            memcpy(&npal[index], &(image->pal[dpal[index]]), sizeof(DMColor));
+        }
+    }
+
+    // Remap image
+    dmMsg(3, "Remapping image.\n");
+    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 && dpal[col] >= 0 && dpal[col] < image->ncolors)
+            {
+                dp[xc] = dpal[col];
+            }
+        }
+    }
+
+    // Set new palette, free memory
+    dmFree(image->pal);
+    image->pal = npal;
+    dmFree(spal);
+    dmFree(dpal);
+    return DMERR_OK;
+}
+
+
 int dmWriteImage(const char *filename, DMImage *image, DMImageSpec *spec, int iformat, BOOL info)
 {
     if (info)
@@ -529,6 +851,14 @@
             spec->scale);
     }
 
+    // Perform color remapping
+    if (optRemapColors)
+    {
+        int res;
+        if ((res = dmRemapImageColors(image)) != DMERR_OK)
+            return res;
+    }
+
     switch (iformat)
     {
 #ifdef DM_USE_LIBPNG
@@ -608,7 +938,7 @@
             }
 
         default:
-            return FALSE;
+            return DMERR_INVALID_DATA;
     }
 }
 
@@ -806,10 +1136,10 @@
 
     // Default colors
     for (i = 0; i < C64_MAX_COLORS; i++)
-        optColors[i] = i + 1;
+        optColors[i] = i;
 
     // Initialize and parse commandline
-    dmInitProg("gfxconv", "Simple graphics converter", "0.5", NULL, NULL);
+    dmInitProg("gfxconv", "Simple graphics converter", "0.6", NULL, NULL);
 
     if (!dmArgsProcess(argc, argv, optList, optListN,
         argHandleOpt, argHandleFile, TRUE))