SDL_image: Support for Saving TGA (#576)

From 85c3c03ae23a2520dbc41e472c02dad247c909d9 Mon Sep 17 00:00:00 2001
From: Xen <[EMAIL REDACTED]>
Date: Thu, 31 Jul 2025 05:48:42 +0300
Subject: [PATCH] Support for Saving TGA (#576)

Ability to save SDL_Surface into a SDL_IOStream or file in the TGA format.
---
 include/SDL3_image/SDL_image.h |  32 ++++++
 src/IMG_tga.c                  | 195 +++++++++++++++++++++++++++++++++
 2 files changed, 227 insertions(+)

diff --git a/include/SDL3_image/SDL_image.h b/include/SDL3_image/SDL_image.h
index 0fd618dc..23e53048 100644
--- a/include/SDL3_image/SDL_image.h
+++ b/include/SDL3_image/SDL_image.h
@@ -2012,6 +2012,38 @@ extern SDL_DECLSPEC bool SDLCALL IMG_SaveWEBP_IO(SDL_Surface *surface, SDL_IOStr
  */
 extern SDL_DECLSPEC bool SDLCALL IMG_SaveWEBP(SDL_Surface *surface, const char *file, float quality);
 
+/**
+ * Save an SDL_Surface into TGA image data, via an SDL_IOStream.
+ *
+ * If you just want to save to a filename, you can use IMG_SaveTGA() instead.
+ *
+ * \param surface the SDL surface to save.
+ * \param dst the SDL_IOStream to save the image data to.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \since This function is available since SDL_image 3.2.18.
+ *
+ * \sa IMG_SaveTGA
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveTGA_IO(SDL_Surface *surface, SDL_IOStream *dst);
+
+/**
+ * Save an SDL_Surface into a TGA image file.
+ *
+ * If the file already exists, it will be overwritten.
+ *
+ * \param surface the SDL surface to save.
+ * \param file path on the filesystem to write new file to.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \since This function is available since SDL_image 3.2.18.
+ *
+ * \sa IMG_SaveTGA_IO
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveTGA(SDL_Surface *surface, const char *file);
+
 /**
  * Animated image support
  *
diff --git a/src/IMG_tga.c b/src/IMG_tga.c
index 0358bd25..ca39f5a3 100644
--- a/src/IMG_tga.c
+++ b/src/IMG_tga.c
@@ -335,6 +335,187 @@ SDL_Surface *IMG_LoadTGA_IO(SDL_IOStream *src)
     return NULL;
 }
 
+bool IMG_SaveTGA_IO(SDL_Surface *surface, SDL_IOStream *dst)
+{
+    Sint64 start;
+    const char *error = NULL;
+    struct TGAheader hdr;
+    bool retval = false;
+    SDL_Palette *surface_palette = NULL;
+    SDL_Surface *temp_surface = NULL;
+
+    if (!surface || !dst) {
+        SDL_SetError("Invalid parameters to IMG_SaveTGA_IO");
+        return false;
+    }
+
+    start = SDL_TellIO(dst);
+
+    SDL_zero(hdr);
+
+    hdr.infolen = 0;
+    hdr.has_cmap = 0;
+    hdr.type = TGA_TYPE_RGB;
+    SETLE16(hdr.cmap_start, 0);
+    SETLE16(hdr.cmap_len, 0);
+    hdr.cmap_bits = 0;
+    SETLE16(hdr.xorigin, 0);
+    SETLE16(hdr.yorigin, 0);
+    SETLE16(hdr.width, surface->w);
+    SETLE16(hdr.height, surface->h);
+    hdr.flags = TGA_ORIGIN_UPPER;
+
+    const SDL_PixelFormatDetails* pixelFormatDetails = SDL_GetPixelFormatDetails(surface->format);
+    if (!pixelFormatDetails) {
+        error = "Failed to get SDL_PixelFormatDetails for surface format";
+        goto error;
+    }
+
+    surface_palette = SDL_GetSurfacePalette(surface);
+
+    switch (surface->format) {
+    case SDL_PIXELFORMAT_INDEX8:
+        hdr.has_cmap = 1;
+        hdr.type = TGA_TYPE_INDEXED;
+        hdr.pixel_bits = 8;
+        if (surface_palette) {
+            SETLE16(hdr.cmap_len, surface_palette->ncolors);
+        } else {
+            SETLE16(hdr.cmap_len, 0);
+        }
+        hdr.cmap_bits = 24;
+        break;
+    case SDL_PIXELFORMAT_BGR24:
+    case SDL_PIXELFORMAT_RGB24:
+        hdr.type = TGA_TYPE_RGB;
+        hdr.pixel_bits = 24;
+        break;
+    case SDL_PIXELFORMAT_BGRA32:
+    case SDL_PIXELFORMAT_RGBA32:
+    case SDL_PIXELFORMAT_XRGB8888:
+        hdr.type = TGA_TYPE_RGB;
+        hdr.pixel_bits = 32;
+        hdr.flags |= 0x08;
+        break;
+    case SDL_PIXELFORMAT_XRGB1555:
+    case SDL_PIXELFORMAT_ARGB1555:
+        hdr.type = TGA_TYPE_RGB;
+        hdr.pixel_bits = 16;
+        if (surface->format == SDL_PIXELFORMAT_ARGB1555) {
+            hdr.flags |= 0x01;
+        }
+        break;
+    default:
+        error = "Unsupported SDL_Surface format for TGA saving. Supported formats are INDEX8, BGR24, RGB24, BGRA32, RGBA32, XRGB8888, XRGB1555, ARGB1555.";
+        goto error;
+    }
+
+    if (SDL_WriteIO(dst, &hdr, sizeof(hdr)) != sizeof(hdr)) {
+        error = "Error writing TGA header";
+        goto error;
+    }
+
+    if (hdr.has_cmap && surface_palette) {
+        SDL_Color *colors = surface_palette->colors;
+        int ncolors = LE16(hdr.cmap_len);
+        Uint8 color_entry[4];
+        for (int i = 0; i < ncolors; ++i) {
+            switch (hdr.cmap_bits) {
+            case 24:
+                color_entry[0] = colors[i].b;
+                color_entry[1] = colors[i].g;
+                color_entry[2] = colors[i].r;
+                if (SDL_WriteIO(dst, color_entry, 3) != 3) {
+                    error = "Error writing TGA colormap entry (24-bit)";
+                    goto error;
+                }
+                break;
+            case 32:
+                color_entry[0] = colors[i].b;
+                color_entry[1] = colors[i].g;
+                color_entry[2] = colors[i].r;
+                color_entry[3] = colors[i].a;
+                if (SDL_WriteIO(dst, color_entry, 4) != 4) {
+                    error = "Error writing TGA colormap entry (32-bit)";
+                    goto error;
+                }
+                break;
+            default:
+                error = "Unsupported TGA colormap bit depth for saving";
+                goto error;
+            }
+        }
+    }
+    
+    Uint8 *pixels_to_write = (Uint8 *)surface->pixels;
+    int pitch_to_write = surface->pitch;
+    int bytes_per_pixel = pixelFormatDetails->bytes_per_pixel;
+    Uint32 target_format = SDL_PIXELFORMAT_UNKNOWN;
+
+    switch (surface->format) {
+    case SDL_PIXELFORMAT_RGB24:
+        target_format = SDL_PIXELFORMAT_BGR24;
+        break;
+    case SDL_PIXELFORMAT_RGBA32:
+    case SDL_PIXELFORMAT_XRGB8888:
+        target_format = SDL_PIXELFORMAT_BGRA32;
+        break;
+    case SDL_PIXELFORMAT_XRGB1555:
+    case SDL_PIXELFORMAT_ARGB1555:
+        target_format = SDL_PIXELFORMAT_XRGB1555;
+        break;
+    default:
+        break;
+    }
+
+    if (target_format != SDL_PIXELFORMAT_UNKNOWN && target_format != surface->format) {
+        temp_surface = SDL_ConvertSurface(surface, target_format);
+        if (!temp_surface) {
+            error = "Failed to convert surface format for TGA saving";
+            goto error;
+        }
+        pixels_to_write = (Uint8 *)temp_surface->pixels;
+        pitch_to_write = temp_surface->pitch;
+        const SDL_PixelFormatDetails *tempPixelFormatDetails = SDL_GetPixelFormatDetails(temp_surface->format);
+        if (tempPixelFormatDetails) {
+            bytes_per_pixel = tempPixelFormatDetails->bytes_per_pixel;
+        } else {
+            error = "Failed to get SDL_PixelFormatDetails for converted surface format";
+            goto error;
+        }
+    }
+
+    for (int y = 0; y < surface->h; ++y) {
+        if (SDL_WriteIO(dst, pixels_to_write + y * pitch_to_write, (size_t)surface->w * bytes_per_pixel) != (size_t)(surface->w * bytes_per_pixel)) {
+            error = "Error writing TGA pixel data";
+            goto error;
+        }
+    }
+
+    retval = true;
+
+error:
+    if (temp_surface) {
+        SDL_DestroySurface(temp_surface);
+    }
+    if (!retval) {
+        SDL_SeekIO(dst, start, SDL_IO_SEEK_SET);
+        SDL_SetError("%s", error);
+    }
+    return retval;
+}
+
+bool IMG_SaveTGA(SDL_Surface *surface, const char *file)
+{
+    SDL_IOStream *dst = SDL_IOFromFile(file, "wb");
+    if (!dst) {
+        return false;
+    }
+    bool retval = IMG_SaveTGA_IO(surface, dst);
+    SDL_CloseIO(dst);
+    return retval;
+}
+
 #else
 
 /* dummy TGA load routine */
@@ -344,6 +525,20 @@ SDL_Surface *IMG_LoadTGA_IO(SDL_IOStream *src)
     return NULL;
 }
 
+bool IMG_SaveTGA_IO(SDL_Surface *surface, SDL_IOStream *dst)
+{
+    (void)surface;
+    (void)dst;
+    return SDL_SetError("TGA support not enabled");
+}
+
+bool IMG_SaveTGA(SDL_Surface *surface, const char *file)
+{
+    (void)surface;
+    (void)file;
+    return SDL_SetError("TGA support not enabled");
+}
+
 #endif /* LOAD_TGA */
 
 #endif /* !defined(__APPLE__) || defined(SDL_IMAGE_USE_COMMON_BACKEND) */