SDL: Use a dither palette when converting RGB images to indexed formats

From b7ec2119dd41041c306d0367c5797eae6621c466 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 16 Jul 2024 12:46:28 -0700
Subject: [PATCH] Use a dither palette when converting RGB images to indexed
 formats

---
 include/SDL3/SDL_surface.h    |  2 +-
 src/dynapi/SDL_dynapi_procs.h |  2 +-
 src/video/SDL_pixels.c        | 18 ++++++------
 src/video/SDL_pixels_c.h      |  2 +-
 src/video/SDL_surface.c       | 55 +++++++++++++++++++++--------------
 5 files changed, 45 insertions(+), 34 deletions(-)

diff --git a/include/SDL3/SDL_surface.h b/include/SDL3/SDL_surface.h
index 25cf66da1221c..6998b12991e48 100644
--- a/include/SDL3/SDL_surface.h
+++ b/include/SDL3/SDL_surface.h
@@ -742,7 +742,7 @@ extern SDL_DECLSPEC SDL_Surface *SDLCALL SDL_ConvertSurface(SDL_Surface *surface
  * \sa SDL_ConvertSurface
  * \sa SDL_DestroySurface
  */
-extern SDL_DECLSPEC SDL_Surface *SDLCALL SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelFormat format, const SDL_Palette *palette, SDL_Colorspace colorspace, SDL_PropertiesID props);
+extern SDL_DECLSPEC SDL_Surface *SDLCALL SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelFormat format, SDL_Palette *palette, SDL_Colorspace colorspace, SDL_PropertiesID props);
 
 /**
  * Copy a block of pixels of one format to another format.
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 2e3f15cef09d7..c33d2779b25d0 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -94,7 +94,7 @@ SDL_DYNAPI_PROC(int,SDL_ConvertEventToRenderCoordinates,(SDL_Renderer *a, SDL_Ev
 SDL_DYNAPI_PROC(int,SDL_ConvertPixels,(int a, int b, SDL_PixelFormat c, const void *d, int e, SDL_PixelFormat f, void *g, int h),(a,b,c,d,e,f,g,h),return)
 SDL_DYNAPI_PROC(int,SDL_ConvertPixelsAndColorspace,(int a, int b, SDL_PixelFormat c, SDL_Colorspace d, SDL_PropertiesID e, const void *f, int g, SDL_PixelFormat h, SDL_Colorspace i, SDL_PropertiesID j, void *k, int l),(a,b,c,d,e,f,g,h,i,j,k,l),return)
 SDL_DYNAPI_PROC(SDL_Surface*,SDL_ConvertSurface,(SDL_Surface *a, SDL_PixelFormat b),(a,b),return)
-SDL_DYNAPI_PROC(SDL_Surface*,SDL_ConvertSurfaceAndColorspace,(SDL_Surface *a, SDL_PixelFormat b, const SDL_Palette *c, SDL_Colorspace d, SDL_PropertiesID e),(a,b,c,d,e),return)
+SDL_DYNAPI_PROC(SDL_Surface*,SDL_ConvertSurfaceAndColorspace,(SDL_Surface *a, SDL_PixelFormat b, SDL_Palette *c, SDL_Colorspace d, SDL_PropertiesID e),(a,b,c,d,e),return)
 SDL_DYNAPI_PROC(int,SDL_CopyProperties,(SDL_PropertiesID a, SDL_PropertiesID b),(a,b),return)
 SDL_DYNAPI_PROC(SDL_AudioStream*,SDL_CreateAudioStream,(const SDL_AudioSpec *a, const SDL_AudioSpec *b),(a,b),return)
 SDL_DYNAPI_PROC(SDL_Cursor*,SDL_CreateColorCursor,(SDL_Surface *a, int b, int c),(a,b,c),return)
diff --git a/src/video/SDL_pixels.c b/src/video/SDL_pixels.c
index e265f8473c254..903a62be999c4 100644
--- a/src/video/SDL_pixels.c
+++ b/src/video/SDL_pixels.c
@@ -1077,28 +1077,28 @@ void SDL_DestroyPalette(SDL_Palette *palette)
 /*
  * Calculate an 8-bit (3 red, 3 green, 2 blue) dithered palette of colors
  */
-void SDL_DitherColors(SDL_Color *colors, int bpp)
+void SDL_DitherPalette(SDL_Palette *palette)
 {
     int i;
-    if (bpp != 8) {
+    if (palette->ncolors != 256) {
         return; /* only 8bpp supported right now */
     }
 
-    for (i = 0; i < 256; i++) {
+    for (i = 0; i < palette->ncolors; i++) {
         int r, g, b;
         /* map each bit field to the full [0, 255] interval,
            so 0 is mapped to (0, 0, 0) and 255 to (255, 255, 255) */
         r = i & 0xe0;
         r |= r >> 3 | r >> 6;
-        colors[i].r = (Uint8)r;
+        palette->colors[i].r = (Uint8)r;
         g = (i << 3) & 0xe0;
         g |= g >> 3 | g >> 6;
-        colors[i].g = (Uint8)g;
+        palette->colors[i].g = (Uint8)g;
         b = i & 0x3;
         b |= b << 2;
         b |= b << 4;
-        colors[i].b = (Uint8)b;
-        colors[i].a = SDL_ALPHA_OPAQUE;
+        palette->colors[i].b = (Uint8)b;
+        palette->colors[i].a = SDL_ALPHA_OPAQUE;
     }
 }
 
@@ -1414,9 +1414,9 @@ static Uint8 *MapNto1(const SDL_PixelFormatDetails *src, const SDL_Palette *pal,
         return NULL;
     }
 
-    dithered.ncolors = 256;
-    SDL_DitherColors(colors, 8);
     dithered.colors = colors;
+    dithered.ncolors = SDL_arraysize(colors);
+    SDL_DitherPalette(&dithered);
     return Map1to1(&dithered, pal, identical);
 }
 
diff --git a/src/video/SDL_pixels_c.h b/src/video/SDL_pixels_c.h
index 90f1905d728f7..a4b177efb49e0 100644
--- a/src/video/SDL_pixels_c.h
+++ b/src/video/SDL_pixels_c.h
@@ -48,7 +48,7 @@ extern int SDL_MapSurface(SDL_Surface *src, SDL_Surface *dst);
 extern void SDL_InvalidateAllBlitMap(SDL_Surface *surface);
 
 /* Miscellaneous functions */
-extern void SDL_DitherColors(SDL_Color *colors, int bpp);
+extern void SDL_DitherPalette(SDL_Palette *palette);
 extern Uint8 SDL_FindColor(const SDL_Palette *pal, Uint8 r, Uint8 g, Uint8 b, Uint8 a);
 extern void SDL_DetectPalette(const SDL_Palette *pal, SDL_bool *is_opaque, SDL_bool *has_alpha_channel);
 extern SDL_Surface *SDL_DuplicatePixels(int width, int height, SDL_PixelFormat format, SDL_Colorspace colorspace, void *pixels, int pitch);
diff --git a/src/video/SDL_surface.c b/src/video/SDL_surface.c
index 3cc685c086dfb..f2264ab49dbb1 100644
--- a/src/video/SDL_surface.c
+++ b/src/video/SDL_surface.c
@@ -1342,9 +1342,10 @@ int SDL_FlipSurface(SDL_Surface *surface, SDL_FlipMode flip)
     }
 }
 
-SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelFormat format, const SDL_Palette *palette, SDL_Colorspace colorspace, SDL_PropertiesID props)
+SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelFormat format, SDL_Palette *palette, SDL_Colorspace colorspace, SDL_PropertiesID props)
 {
-    SDL_Surface *convert;
+    SDL_Palette *temp_palette = NULL;
+    SDL_Surface *convert = NULL;
     SDL_Colorspace src_colorspace;
     SDL_PropertiesID src_properties;
     Uint32 copy_flags;
@@ -1359,12 +1360,12 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
 
     if (!SDL_SurfaceValid(surface)) {
         SDL_InvalidParamError("surface");
-        return NULL;
+        goto error;
     }
 
     if (format == SDL_PIXELFORMAT_UNKNOWN) {
         SDL_InvalidParamError("format");
-        return NULL;
+        goto error;
     }
 
     /* Check for empty destination palette! (results in empty image) */
@@ -1377,7 +1378,14 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
         }
         if (i == palette->ncolors) {
             SDL_SetError("Empty destination palette");
-            return NULL;
+            goto error;
+        }
+    } else if (SDL_ISPIXELFORMAT_INDEXED(format)) {
+        // Create a dither palette for conversion
+        temp_palette = SDL_CreatePalette(1 << SDL_BITSPERPIXEL(format));
+        if (temp_palette) {
+            SDL_DitherPalette(temp_palette);
+            palette = temp_palette;
         }
     }
 
@@ -1387,7 +1395,10 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
     /* Create a new surface with the desired format */
     convert = SDL_CreateSurface(surface->w, surface->h, format);
     if (!convert) {
-        return NULL;
+        goto error;
+    }
+    if (SDL_ISPIXELFORMAT_INDEXED(format)) {
+        SDL_SetSurfacePalette(convert, palette);
     }
 
     if (colorspace == SDL_COLORSPACE_UNKNOWN) {
@@ -1397,8 +1408,7 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
 
     if (SDL_ISPIXELFORMAT_FOURCC(format) || SDL_ISPIXELFORMAT_FOURCC(surface->format)) {
         if (SDL_ConvertPixelsAndColorspace(surface->w, surface->h, surface->format, src_colorspace, src_properties, surface->pixels, surface->pitch, convert->format, colorspace, props, convert->pixels, convert->pitch) < 0) {
-            SDL_DestroySurface(convert);
-            return NULL;
+            goto error;
         }
 
         /* Save the original copy flags */
@@ -1407,14 +1417,6 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
         goto end;
     }
 
-    /* Copy the palette if any */
-    if (palette && convert->internal->palette) {
-        SDL_memcpy(convert->internal->palette->colors,
-                   palette->colors,
-                   palette->ncolors * sizeof(SDL_Color));
-        convert->internal->palette->ncolors = palette->ncolors;
-    }
-
     /* Save the original copy flags */
     copy_flags = surface->internal->map.info.flags;
     copy_color.r = surface->internal->map.info.r;
@@ -1509,8 +1511,7 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
 
     /* SDL_BlitSurfaceUnchecked failed, and so the conversion */
     if (ret < 0) {
-        SDL_DestroySurface(convert);
-        return NULL;
+        goto error;
     }
 
     if (copy_flags & SDL_COPY_COLORKEY) {
@@ -1547,8 +1548,7 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
             /* Create a dummy surface to get the colorkey converted */
             tmp = SDL_CreateSurface(1, 1, surface->format);
             if (!tmp) {
-                SDL_DestroySurface(convert);
-                return NULL;
+                goto error;
             }
 
             /* Share the palette, if any */
@@ -1564,8 +1564,7 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
             tmp2 = SDL_ConvertSurfaceAndColorspace(tmp, format, palette, colorspace, props);
             if (!tmp2) {
                 SDL_DestroySurface(tmp);
-                SDL_DestroySurface(convert);
-                return NULL;
+                goto error;
             }
 
             /* Get the converted colorkey */
@@ -1585,6 +1584,9 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
     }
 
 end:
+    if (temp_palette) {
+        SDL_DestroyPalette(temp_palette);
+    }
 
     SDL_SetSurfaceClipRect(convert, &surface->internal->clip_rect);
 
@@ -1601,6 +1603,15 @@ SDL_Surface *SDL_ConvertSurfaceAndColorspace(SDL_Surface *surface, SDL_PixelForm
 
     /* We're ready to go! */
     return convert;
+
+error:
+    if (temp_palette) {
+        SDL_DestroyPalette(temp_palette);
+    }
+    if (convert) {
+        SDL_DestroySurface(convert);
+    }
+    return NULL;
 }
 
 SDL_Surface *SDL_DuplicateSurface(SDL_Surface *surface)