# HG changeset patch # User Matti Hamalainen # Date 1588024026 -10800 # Node ID f3d9cdb0a295bbaaf70ff566914226769266bf4c # Parent c7a0913e1032ffe56a06b2c0d8f561a1e66afd72 Some work towards more flexible palette remapping. diff -r c7a0913e1032 -r f3d9cdb0a295 tools/gfxconv.c --- 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