SDL_image: Handle orientation metadata

From 3f1a272e90a25fd29a1c5fb36def2af8d2e1f99c Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 8 Jan 2026 15:41:13 -0800
Subject: [PATCH] Handle orientation metadata

This supports the orientation in TIFF files and the XMP orientation metadata in PNG files when using libpng. It does not currently support Exif metadata.

Fixes https://github.com/libsdl-org/SDL_image/issues/468
---
 src/IMG.c         | 66 +++++++++++++++++++++++++++++++++++++++++++++++
 src/IMG_ImageIO.m | 20 +++-----------
 src/IMG_libpng.c  | 23 +++++++++++++++++
 src/IMG_tif.c     | 40 +++++++++++++++++++++++++++-
 src/IMG_utils.h   | 23 +++++++++++++++++
 5 files changed, 154 insertions(+), 18 deletions(-)
 create mode 100644 src/IMG_utils.h

diff --git a/src/IMG.c b/src/IMG.c
index 9d07c120..ca913e9b 100644
--- a/src/IMG.c
+++ b/src/IMG.c
@@ -546,3 +546,69 @@ Uint64 IMG_TimebaseDuration(Uint64 pts, Uint64 duration, Uint64 src_numerator, U
     Uint64 b = ( ( ( pts * 2 ) + 1 ) * src_numerator * dst_denominator ) / ( 2 * src_denominator * dst_numerator );
     return (a - b);
 }
+
+SDL_Surface *IMG_ApplyOrientation(SDL_Surface *surface, int orientation)
+{
+    SDL_Surface *tmp;
+    switch (orientation)
+    {
+    case 1:
+        // Normal (no rotation required)
+        break;
+    case 2:
+        // Flipped horizontally
+        if (!SDL_FlipSurface(surface, SDL_FLIP_HORIZONTAL)) {
+            SDL_DestroySurface(surface);
+            return NULL;
+        }
+        break;
+    case 3:
+        // Upside-down (180 degrees rotation)
+        tmp = SDL_RotateSurface(surface, 180.0f);
+        SDL_DestroySurface(surface);
+        surface = tmp;
+        break;
+    case 4:
+        // Flipped vertically
+        if (!SDL_FlipSurface(surface, SDL_FLIP_VERTICAL)) {
+            SDL_DestroySurface(surface);
+            return NULL;
+        }
+        break;
+    case 5:
+        // Flip horizontally and rotate 90 degrees counterclockwise
+        if (!SDL_FlipSurface(surface, SDL_FLIP_HORIZONTAL)) {
+            SDL_DestroySurface(surface);
+            return NULL;
+        }
+        tmp = SDL_RotateSurface(surface, -90.0f);
+        SDL_DestroySurface(surface);
+        surface = tmp;
+        break;
+    case 6:
+        // Rotate 90 degrees clockwise
+        tmp = SDL_RotateSurface(surface, 90.0f);
+        SDL_DestroySurface(surface);
+        surface = tmp;
+        break;
+    case 7:
+        // Flip horizontally and rotate 90 degrees clockwise
+        if (!SDL_FlipSurface(surface, SDL_FLIP_HORIZONTAL)) {
+            SDL_DestroySurface(surface);
+            return NULL;
+        }
+        tmp = SDL_RotateSurface(surface, 90.0f);
+        SDL_DestroySurface(surface);
+        surface = tmp;
+        break;
+    case 8:
+        // Rotate 90 degrees counterclockwise
+        tmp = SDL_RotateSurface(surface, -90.0f);
+        SDL_DestroySurface(surface);
+        surface = tmp;
+        break;
+    default:
+        break;
+    }
+    return surface;
+}
diff --git a/src/IMG_ImageIO.m b/src/IMG_ImageIO.m
index aa889fc3..d17d937a 100644
--- a/src/IMG_ImageIO.m
+++ b/src/IMG_ImageIO.m
@@ -11,6 +11,8 @@
 
 #include <SDL3_image/SDL_image.h>
 
+#include "IMG_utils.h"
+
 // Used because CGDataProviderCreate became deprecated in 10.5
 #include <AvailabilityMacros.h>
 #include <TargetConditionals.h>
@@ -355,25 +357,9 @@ static CFDictionaryRef CreateHintDictionary(CFStringRef uti_string_hint)
     if (surface && properties) {
         CFNumberRef numval;
         if (CFDictionaryGetValueIfPresent(properties, kCGImagePropertyOrientation, (const void **)&numval)) {
-            float rotation = 0.0f;
             CGImagePropertyOrientation orientation;
             CFNumberGetValue(numval, kCFNumberSInt32Type, &orientation);
-            switch (orientation) {
-            case kCGImagePropertyOrientationRight:
-                rotation = 90.0f;
-                break;
-            case kCGImagePropertyOrientationDown:
-                rotation = 180.0f;
-                break;
-            case kCGImagePropertyOrientationLeft:
-                rotation = 270.0f;
-                break;
-            default:
-                break;
-            }
-            if (rotation != 0.0f) {
-                SDL_SetFloatProperty(SDL_GetSurfaceProperties(surface), SDL_PROP_SURFACE_ROTATION_FLOAT, rotation);
-            }
+            surface = IMG_ApplyOrientation(surface, orientation);
         }
     }
     return surface;
diff --git a/src/IMG_libpng.c b/src/IMG_libpng.c
index 55b7fc82..ffbad87b 100644
--- a/src/IMG_libpng.c
+++ b/src/IMG_libpng.c
@@ -32,6 +32,7 @@
 #include "IMG_libpng.h"
 #include "IMG_anim_encoder.h"
 #include "IMG_anim_decoder.h"
+#include "IMG_utils.h"
 
 #ifdef SDL_IMAGE_LIBPNG
 #ifdef INCLUDE_PNG_FRAMEWORK
@@ -173,6 +174,7 @@ static struct
     png_byte (*png_get_color_type)(png_const_structrp png_ptr, png_const_inforp info_ptr);
     png_uint_32 (*png_get_image_width)(png_const_structrp png_ptr, png_const_inforp info_ptr);
     png_uint_32 (*png_get_image_height)(png_const_structrp png_ptr, png_const_inforp info_ptr);
+    int (*png_get_text)(png_const_structp png_ptr, png_infop info_ptr, png_textp *text_ptr, int *num_text);
 
     void (*png_write_flush)(png_structrp png_ptr);
 } lib;
@@ -277,6 +279,7 @@ bool IMG_InitPNG(void)
         FUNCTION_LOADER_LIBPNG(png_get_color_type, png_byte(*)(png_const_structrp png_ptr, png_const_inforp info_ptr))
         FUNCTION_LOADER_LIBPNG(png_get_image_width, png_uint_32(*)(png_const_structrp png_ptr, png_const_inforp info_ptr))
         FUNCTION_LOADER_LIBPNG(png_get_image_height, png_uint_32(*)(png_const_structrp png_ptr, png_const_inforp info_ptr))
+        FUNCTION_LOADER_LIBPNG(png_get_text, int (*)(png_const_structrp png_ptr, png_inforp info_ptr, png_textp *text_ptr, int *num_text))
 
         FUNCTION_LOADER_LIBPNG(png_write_flush, void (*)(png_structrp png_ptr))
     }
@@ -491,6 +494,26 @@ static bool LIBPNG_LoadPNG_IO_Internal(SDL_IOStream *src, struct png_load_vars *
     }
 #endif
 
+    png_textp text_ptr = NULL;
+    int num_text = 0;
+    if (lib.png_get_text(vars->png_ptr, vars->info_ptr, &text_ptr, &num_text) > 0) {
+        for (int i = 0; i < num_text; ++i, ++text_ptr) {
+            if (SDL_strcmp(text_ptr->key, "XML:com.adobe.xmp") == 0) {
+                // Look for tiff:Orientation in the XMP data
+                int orientation;
+                const char *value = SDL_strstr(text_ptr->text, "tiff:Orientation=\"");
+                if (value) {
+                    value += 18;
+                    orientation = (*value - '0');
+                    vars->surface = IMG_ApplyOrientation(vars->surface, orientation);
+                    if (!vars->surface) {
+                        return false;
+                    }
+                }
+            }
+        }
+    }
+
     return true;
 }
 
diff --git a/src/IMG_tif.c b/src/IMG_tif.c
index c2598d23..b16a0c53 100644
--- a/src/IMG_tif.c
+++ b/src/IMG_tif.c
@@ -176,6 +176,7 @@ SDL_Surface* IMG_LoadTIF_IO(SDL_IOStream * src)
     TIFF* tiff = NULL;
     SDL_Surface* surface = NULL;
     Uint32 img_width, img_height;
+    Uint16 img_orientation = 1;
 
     if ( !src ) {
         /* The error message has been set in SDL_IOFromFile */
@@ -196,16 +197,53 @@ SDL_Surface* IMG_LoadTIF_IO(SDL_IOStream * src)
     /* Retrieve the dimensions of the image from the TIFF tags */
     lib.TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &img_width);
     lib.TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &img_height);
+    lib.TIFFGetField(tiff, TIFFTAG_ORIENTATION, &img_orientation);
 
     surface = SDL_CreateSurface(img_width, img_height, SDL_PIXELFORMAT_ABGR8888);
     if(!surface)
         goto error;
 
-    if(!lib.TIFFReadRGBAImageOriented(tiff, img_width, img_height, (Uint32 *)surface->pixels, ORIENTATION_TOPLEFT, 0))
+    int load_orientation;
+    switch (img_orientation) {
+    case 5:
+    case 6:
+    case 7:
+    case 8:
+        load_orientation = ORIENTATION_TOPRIGHT;
+        break;
+    default:
+        load_orientation = ORIENTATION_TOPLEFT;
+        break;
+    }
+    if(!lib.TIFFReadRGBAImageOriented(tiff, img_width, img_height, (Uint32 *)surface->pixels, load_orientation, 0)) {
         goto error;
+    }
 
     lib.TIFFClose(tiff);
 
+    SDL_Surface *rotated;
+    switch (img_orientation) {
+    case 5:
+    case 7:
+        rotated = SDL_RotateSurface(surface, 270.0f);
+        if (!rotated) {
+            goto error;
+        }
+        SDL_DestroySurface(surface);
+        surface = rotated;
+        break;
+    case 6:
+    case 8:
+        rotated = SDL_RotateSurface(surface, 90.0f);
+        if (!rotated) {
+            goto error;
+        }
+        SDL_DestroySurface(surface);
+        surface = rotated;
+        break;
+    default:
+        break;
+    }
     return surface;
 
 error:
diff --git a/src/IMG_utils.h b/src/IMG_utils.h
new file mode 100644
index 00000000..b465dfa1
--- /dev/null
+++ b/src/IMG_utils.h
@@ -0,0 +1,23 @@
+/*
+  SDL_image:  An example image loading library for use with SDL
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+extern SDL_Surface *IMG_ApplyOrientation(SDL_Surface *surface, int orientation);
+