changeset 2481:f3d9cdb0a295

Some work towards more flexible palette remapping.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 28 Apr 2020 00:47:06 +0300
parents c7a0913e1032
children 7151597d8ec6
files tools/gfxconv.c
diffstat 1 files changed, 440 insertions(+), 120 deletions(-) [+]
line wrap: on
line diff
--- a/tools/gfxconv.c	Mon Apr 27 21:37:15 2020 +0300
+++ b/tools/gfxconv.c	Tue Apr 28 00:47:06 2020 +0300
@@ -77,6 +77,13 @@
 };
 
 
+enum
+{
+    REMAP_NONE = 0,
+    REMAP_AUTO,
+    REMAP_MAPPED,
+};
+
 typedef struct
 {
     char *name;       // Descriptive name of the format
@@ -134,11 +141,14 @@
 
 BOOL    optInMulticolor = FALSE,
         optSequential = FALSE,
-        optRemapColors = FALSE,
         optRemapRemove = FALSE,
+        optRemapMatchAlpha = FALSE,
         optUsePalette = FALSE;
+int     optRemapMode = REMAP_NONE;
 int     optNRemapTable = 0,
         optScaleMode = SCALE_AUTO;
+float   optRemapMaxDist = 0;
+int     optRemapFail = 0;
 DMMapValue optRemapTable[DM_MAX_COLORS];
 int     optColorMap[D64_NCOLORS];
 char    *optCharROMFilename = NULL;
@@ -184,7 +194,10 @@
     { 38, 'C', "compress"        , "Use compression -C <0-9>, 0 = disable, default is 9", OPT_ARGREQ },
     { 42, 'R', "remap"           , "Remap output image colors (-R <(#RRGGBB|index):index>[,<..>][+remove] | -R auto[+remove] | -R @map.txt[+remove])", OPT_ARGREQ },
     { 44,   0, "char-rom"        , "Set character ROM file to be used.", OPT_ARGREQ },
-    { 46, 'p', "palette"         , "Set C64 palette to be used (see list with -p help).", OPT_ARGREQ },
+    { 46, 'p', "palette"         , "Set palette to be used (see list with -p help). "
+                                   "For paletted image file input, this will replace the "
+                                   "image's original palette. Color remapping will not be "
+                                   "done unless -R option is also specified.", OPT_ARGREQ },
 };
 
 static const int optListN = sizeof(optList) / sizeof(optList[0]);
@@ -932,36 +945,84 @@
             break;
 
         case 42:
-            if ((tmpStr = dm_strrcasecmp(optArg, "+remove")) != NULL)
+            while ((tmpStr = strchr(optArg, '+')) != NULL)
             {
-                optRemapRemove = TRUE;
+                char *topt = tmpStr + 1,
+                     *tend = strchr(topt, '+');
+
                 *tmpStr = 0;
+                if (tend != NULL)
+                {
+                    *tend = 0;
+                    tmpStr = tend + 1;
+                }
+                else
+                    tmpStr = tend;
+
+                if (strcasecmp(topt, "remove") == 0)
+                    optRemapRemove = TRUE;
+                else
+                if (strcasecmp(topt, "alpha") == 0)
+                    optRemapMatchAlpha = TRUE;
+                else
+                {
+                    dmErrorMsg("Unknown -R option flag '%s'.\n", topt);
+                    return FALSE;
+                }
             }
 
-            if (optArg[0] == '@')
+            if (strcasecmp(optArg, "auto") == 0)
             {
-                if (optArg[1] != 0)
+                if (optRemapMode != REMAP_NONE && optRemapMode != REMAP_AUTO)
+                {
+                    dmErrorMsg("Remap mode already set to something else than 'auto'. You can only have one remapping mode.\n");
+                    return FALSE;
+                }
+
+                // Check if we have more?
+                if (optArg[4] == ':')
+                {
+                }
+
+                optRemapMode = REMAP_AUTO;
+            }
+            else
+            {
+                if (optRemapMode != REMAP_NONE && optRemapMode != REMAP_MAPPED)
                 {
-                    int res;
-                    if ((res = dmParseColorRemapFile(optArg + 1,
-                        optRemapTable, &optNRemapTable, DM_MAX_COLORS)) != DMERR_OK)
+                    dmErrorMsg("Remap mode already set to something else than 'mapped'. You can only have one remapping mode.\n");
+                    return FALSE;
+                }
+
+                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
                 {
-                    dmErrorMsg("No remap filename given.\n");
-                    return FALSE;
+                    if (!dmParseMapOptionString(optArg, optRemapTable,
+                        &optNRemapTable, DM_MAX_COLORS, TRUE, "color remap option"))
+                        return FALSE;
                 }
+
+                optRemapMode = REMAP_MAPPED;
             }
-            else
-            if (dm_strcasecmp(optArg, "auto") != 0)
+
+            if (optRemapMatchAlpha)
             {
-                if (!dmParseMapOptionString(optArg, optRemapTable,
-                    &optNRemapTable, DM_MAX_COLORS, TRUE, "color remap option"))
-                    return FALSE;
+                dmErrorMsg("WARNING: Palette alpha matching may have unexpected results.\n");
             }
-
-            optRemapColors = TRUE;
             break;
 
         case 44:
@@ -1078,72 +1139,302 @@
 }
 
 
-int dmRemoveUnusedImageColors(DMImage **pdst, const DMImage *src)
+float dmGetColorDist(const DMColor *c1, const DMColor *c2, const BOOL alpha)
+{
+    const float
+        dr = (c1->r - c2->r) / 255.0,
+        dg = (c1->g - c2->g) / 255.0,
+        db = (c1->b - c2->b) / 255.0;
+
+    if (alpha)
+    {
+        const float da = (c1->a - c2->a) / 255.0;
+        return (dr * dr + dg * dg + db * db + da * da) / 4.0;
+    }
+    else
+        return (dr * dr + dg * dg + db * db) / 3.0;
+}
+
+
+int dmScanUsedColors(const DMImage *src, const BOOL warn, BOOL *used, int *nused)
+{
+    BOOL warned = FALSE;
+    *nused = 0;
+
+    if (src == NULL || used == NULL || nused == NULL)
+        return DMERR_NULLPTR;
+
+    if (src->pal == NULL || src->pixfmt != DM_PIXFMT_PALETTE)
+    {
+        return dmError(DMERR_INVALID_DATA,
+            "Source image is not paletted.\n");
+    }
+
+    for (int index = 0; index < src->pal->ncolors; index++)
+        used[index] = FALSE;
+
+    for (int yc = 0; yc < src->height; yc++)
+    {
+        const Uint8 *dp = src->data + src->pitch * yc;
+        for (int xc = 0; xc < src->width; xc++)
+        {
+            Uint8 col = dp[xc];
+            if (col < src->pal->ncolors)
+            {
+                if (!used[col])
+                {
+                    used[col] = TRUE;
+                    (*nused)++;
+                }
+            }
+            else
+            if (warn && !warned)
+            {
+                dmErrorMsg("Image contains color indices that are out of bounds of the palette.\n");
+                warned = TRUE;
+            }
+        }
+    }
+
+    return DMERR_OK;
+}
+
+
+int dmDoRemapImageColors(DMImage **pdst, const DMImage *src,
+    const int *mapping, const DMPalette *dpal)
 {
-    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 (mapping == NULL || mapped == NULL || used == NULL)
+    DMImage *dst = NULL;
+    int res = DMERR_OK;
+
+    if (pdst == NULL || src == NULL || mapping == NULL)
+        return DMERR_NULLPTR;
+
+    // Allocate target image
+    if ((dst = *pdst = dmImageAlloc(src->width, src->height,
+        src->pixfmt, -1)) == NULL)
+    {
+        res = dmError(DMERR_MALLOC,
+            "Could not allocate image for re-mapped data.\n");
+        goto out;
+    }
+
+    for (int yc = 0; yc < src->height; yc++)
+    {
+        Uint8 *sp = src->data + src->pitch * yc;
+        Uint8 *dp = dst->data + dst->pitch * yc;
+        for (int xc = 0; xc < src->width; xc++)
+        {
+            dp[xc] = mapping[sp[xc]];
+        }
+    }
+
+    if ((res = dmPaletteCopy(&dst->pal, dpal)) != DMERR_OK)
+    {
+        dmErrorMsg("Error installing remapped palette to destination image: %s\n",
+            dmErrorStr(res));
+        goto out;
+    }
+
+out:
+    return res;
+}
+
+
+int dmRemapImageColors(DMImage **pdst, const DMImage *src,
+    const DMPalette *dpal, const BOOL alpha,
+    const float maxDist, const int noMatch,
+    const BOOL removeUnused)
+{
+    DMPalette *tpal = NULL;
+    const DMPalette *ppal;
+    BOOL *mapped = NULL, *used = NULL;
+    int *mapping = NULL;
+    int res = DMERR_OK;
+
+    if (pdst == NULL || src == NULL || dpal == NULL)
+        return DMERR_NULLPTR;
+
+    if (src->pal == NULL || src->pixfmt != DM_PIXFMT_PALETTE)
+    {
+        res = dmError(DMERR_INVALID_DATA,
+            "Source image is not paletted.\n");
+        goto out;
+    }
+
+    dmMsg(1, "Remapping image from %d to %d colors @ tolerance=%1.2f, %s%s.\n",
+        src->pal->ncolors,
+        dpal->ncolors,
+        maxDist < 0 ? 0 : maxDist,
+        alpha ? "match alpha" : "disregard alpha",
+        noMatch < 0 ? ", fail on 'no match'" : "");
+
+    // Allocate remapping tables
+    if ((mapped  = dmMalloc0(src->pal->ncolors * sizeof(*mapped))) == NULL ||
+        (used    = dmMalloc0(src->pal->ncolors * sizeof(*used))) == NULL ||
+        (mapping = dmMalloc(src->pal->ncolors * sizeof(*mapping))) == NULL)
     {
         res = dmError(DMERR_MALLOC,
-            "Could not allocate memory for reused palette.\n");
+            "Could not allocate memory for color remap tables.\n");
         goto out;
     }
 
-    if ((res = dmPaletteAlloc(&tmpPal, src->pal->ncolors, -1)) != DMERR_OK)
+    for (int index = 0; index < src->pal->ncolors; index++)
+        mapping[index] = -1;
+
+    // Populate remap table
+    for (int sc = 0; sc < src->pal->ncolors; sc++)
+    {
+        // Check if we can find a match in destination palette dpal
+        float closestDist = 1000000, dist = 0;
+
+        for (int dc = 0; dc < dpal->ncolors; dc++)
+        {
+            if (maxDist < 0)
+            {
+                if (dmExactCompareColor(&src->pal->colors[sc], &dpal->colors[dc], alpha))
+                {
+                    mapping[sc] = dc;
+                    break;
+                }
+            }
+            else
+            {
+                dist = dmGetColorDist(&src->pal->colors[sc], &dpal->colors[dc], alpha);
+                if (dist < maxDist && dist < closestDist)
+                {
+                    closestDist = dist;
+                    mapping[sc] = dc;
+                }
+            }
+        }
+
+        // Did we find a match?
+        if (mapping[sc] < 0)
+        {
+            // No, either error out or use noMatch color index
+            if (noMatch < 0)
+            {
+                res = dmError(DMERR_INVALID_DATA,
+                    "Could not remap source color #%d, no matches found.\n",
+                    sc);
+                goto out;
+            }
+            else
+            {
+                mapping[sc] = noMatch;
+            }
+        }
+        else
+        {
+            DMColor *cs = &src->pal->colors[sc],
+                    *cd = &dpal->colors[mapping[sc]];
+
+            dmPrint(3, "Palette match #%d (%02x %02x %02x) -> #%d (%02x %02x %02x) [dist=%1.2f]\n",
+                sc, cs->r, cs->g, cs->b,
+                mapping[sc], cd->r, cd->g, cd->b,
+                closestDist);
+        }
+    }
+
+    // Remove unused colors if requested
+    if (removeUnused)
+    {
+        // Get the actually used colors
+        int nused, nc;
+
+        if ((res = dmScanUsedColors(src, TRUE, used, &nused)) != DMERR_OK)
+            goto out;
+
+        dmMsg(2, "Found %d used colors.\n", nused);
+
+        if ((res = dmPaletteAlloc(&tpal, nused, -1)) != DMERR_OK)
+        {
+            dmErrorMsg("Could not allocate memory for remap palette.\n");
+            goto out;
+        }
+
+        // Now, copy colors from dpal to tpal, re-mapping palette data
+        // and reorder the mapping indices to match it.
+        nc = 0;
+        for (int index = 0; index < src->pal->ncolors; index++)
+        if (used[index])
+        {
+            memcpy(&tpal->colors[nc], &dpal->colors[mapping[index]], sizeof(DMColor));
+            mapping[index] = nc;
+            nc++;
+        }
+
+        ppal = tpal;
+    }
+    else
+        ppal = dpal;
+
+    // Perform image data remapping
+    res = dmDoRemapImageColors(pdst, src, mapping, ppal);
+
+out:
+    dmFree(tpal);
+    dmFree(mapping);
+    dmFree(mapped);
+    dmFree(used);
+    return res;
+}
+
+
+int dmMapImageColors(DMImage **pdst, const DMImage *src,
+    const DMMapValue *mapTable, const int nmapTable,
+    const BOOL removeUnused)
+{
+    DMPalette *tpal = NULL;
+    BOOL *mapped = NULL, *used = NULL;
+    int *mapping = NULL;
+    int nused, res = DMERR_OK;
+
+    if (pdst == NULL || src == NULL || mapTable == NULL)
+        return DMERR_NULLPTR;
+
+    if (src->pal == NULL || src->pixfmt != DM_PIXFMT_PALETTE)
+    {
+        res = dmError(DMERR_INVALID_DATA,
+            "Source image is not paletted.\n");
+        goto out;
+    }
+
+    // Allocate remapping tables
+    if ((mapped  = dmMalloc(src->pal->ncolors * sizeof(*mapped))) == NULL ||
+        (used    = dmMalloc(src->pal->ncolors * sizeof(*used))) == NULL ||
+        (mapping = dmMalloc(src->pal->ncolors * sizeof(*mapping))) == NULL)
+    {
+        res = dmError(DMERR_MALLOC,
+            "Could not allocate memory for color remap tables.\n");
+        goto out;
+    }
+
+    for (int index = 0; index < src->pal->ncolors; index++)
+    {
+        mapping[index] = -1;
+        mapped[index] = FALSE;
+    }
+
+    if ((res = dmPaletteAlloc(&tpal, src->pal->ncolors, -1)) != DMERR_OK)
     {
         dmErrorMsg("Could not allocate memory for remap palette.\n");
         goto out;
     }
 
-    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 out;
-    }
-
-    dmMsg(1, "Remapping %d output image colors of %d colors.\n",
+    dmMsg(1, "Remapping %d input image of %d colors.\n",
             optNRemapTable, src->pal->ncolors);
 
-
-    // Find used colors
-    dmMsg(2, "Scanning image for used colors...\n");
-    for (index = 0; index < src->pal->ncolors; index++)
-    {
-        mapping[index] = -1;
-        mapped[index] = used[index] = FALSE;
-    }
-
-    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++)
+    for (int index = 0; index < nmapTable; index++)
     {
-        DMMapValue *map = &optRemapTable[index];
+        const DMMapValue *map = &mapTable[index];
         if (map->triplet)
         {
             BOOL found = FALSE;
-            for (n = 0; n < src->pal->ncolors; n++)
+            for (int n = 0; n < src->pal->ncolors; n++)
             {
-                if (dmExactCompareColor(&(src->pal->colors[n]), &(map->color), map->alpha))
+                if (dmExactCompareColor(&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,
@@ -1173,15 +1464,23 @@
         }
     }
 
-    // Fill in the rest
-    if (optRemapRemove)
-        dmMsg(2, "Removing unused colors.\n");
-
-    for (index = 0; index < src->pal->ncolors; index++)
+    // Fill the unmapped colors
+    if (removeUnused)
+    {
+        dmMsg(2, "Scanning for used colors.\n");
+
+        if ((res = dmScanUsedColors(src, TRUE, used, &nused)) != DMERR_OK)
+            goto out;
+
+        dmMsg(2, "Removing unused colors: %d -> %d.\n",
+            src->pal->ncolors, nused);
+    }
+
+    for (int index = 0; index < src->pal->ncolors; index++)
     if (mapping[index] < 0 &&
-        (!optRemapRemove || (optRemapRemove && used[index])))
+        (!removeUnused || used[index]))
     {
-        for (n = 0; n < src->pal->ncolors; n++)
+        for (int n = 0; n < src->pal->ncolors; n++)
         if (!mapped[n])
         {
             mapping[index] = n;
@@ -1190,50 +1489,18 @@
         }
     }
 
-    // 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++)
+    // Copy mapped palette entries
+    for (int index = 0; index < src->pal->ncolors; index++)
+    if (mapping[index] >= 0)
     {
-        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;
-        }
+        memcpy(&tpal->colors[mapping[index]], &src->pal->colors[index], sizeof(DMColor));
     }
 
-    // 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 out;
-    }
+    // Perform image data remapping
+    res = dmDoRemapImageColors(pdst, src, mapping, tpal);
 
 out:
+    dmPaletteFree(tpal);
     dmFree(mapping);
     dmFree(mapped);
     dmFree(used);
@@ -1466,7 +1733,7 @@
     if ((res = dmf_open_stdio(hdrFilename, "wb", &fp)) != DMERR_OK)
     {
         return dmError(res,
-            "RAW: Could not open file '%s' for writing: %s\n",
+            "Could not open file '%s' for writing: %s\n",
             hdrFilename, dmErrorStr(res));
     }
 
@@ -1481,6 +1748,8 @@
     DMImageWriteSpec *spec, const DMImageFormat *fmt)
 {
     int res = DMERR_OK;
+    DMImage *image = pimage;
+    BOOL allocated = FALSE;
 
     // Check if writing is even supported
     if (fmt->write == NULL || (fmt->flags & DM_FMT_WR) == 0)
@@ -1496,15 +1765,66 @@
         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 (image->pixfmt == DM_PIXFMT_PALETTE)
     {
-        if ((res = dmRemoveUnusedImageColors(&image, pimage)) != DMERR_OK)
-            return res;
-
-        allocated = TRUE;
+        switch (optRemapMode)
+        {
+            case REMAP_NONE:
+                if (optPaletteData != NULL)
+                {
+                    DMPalette *tpal;
+
+                    dmMsg(1, "Replacing image palette %d colors with %d colors.\n",
+                        image->pal->ncolors, optPaletteData->ncolors);
+
+                    if ((res = dmPaletteCopy(&tpal, optPaletteData)) != DMERR_OK)
+                        return res;
+
+                    if (image->pal->ncolors != optPaletteData->ncolors)
+                    {
+                        dmMsg(1, "Trying to resize to %d colors.\n",
+                            image->pal->ncolors);
+
+                        if ((res = dmPaletteResize(&tpal, image->pal->ncolors)) != DMERR_OK)
+                            return res;
+                    }
+
+                    dmPaletteFree(image->pal);
+                    image->pal = tpal;
+                }
+                break;
+
+            case REMAP_MAPPED:
+                if ((res = dmMapImageColors(&image, pimage,
+                    optRemapTable, optNRemapTable, optRemapRemove)) != DMERR_OK)
+                    goto out;
+                allocated = TRUE;
+                break;
+
+            case REMAP_AUTO:
+                if (optPaletteData == NULL)
+                {
+                    dmErrorMsg(
+                        "Color auto-remapping requested, but target palette not set? (-p option)\n");
+                    goto out;
+                }
+
+                if ((res = dmRemapImageColors(&image, pimage,
+                    optPaletteData, optRemapMatchAlpha,
+                    //optRemapMaxDist, optRemapFail,
+                    0.5, -1,
+                    optRemapRemove)) != DMERR_OK)
+                    goto out;
+
+                allocated = TRUE;
+                break;
+        }
+    }
+    else
+    if (optRemapMode != REMAP_NONE)
+    {
+        dmErrorMsg("Color remapping requested, but image is not paletted?\n");
+        goto out;
     }
 
     // Determine number of planes, if paletted