Repository: MyreMylar/pygame_gui

Issue:
More control over image placement on elements.
**Is your feature request related to a problem? Please describe.**
Right now when you stick an image on a button it is always centred. It'd be more useful if we could control them like we can the text, possibly have more options like stretching the image to fit the element. 

**Describe the solution you'd like**
 - Ability to align images to the left or right, top or bottom as well as the centre in the theme.
 - Stretch to fit option, ignores placement and resizes the image to fit the drawable shape inside the 
   shadow and border

**Additional context**
Hopefully this would make it easy to do things like have file and folder icons in the file dialog next to their names indicating which was which.

Discussion:
@MyreMylar: See also #145 

Solution patch:
diff --git a/docs/source/theme_reference/theme_button.rst b/docs/source/theme_reference/theme_button.rst
index a159d7a..f54217a 100644
--- a/docs/source/theme_reference/theme_button.rst
+++ b/docs/source/theme_reference/theme_button.rst
@@ -95,32 +95,36 @@ For simple buttons with one image per state, use these parameters:
     - "**path**" - The string path to the image to be displayed. OR
     - "**package** - The name of the python package containing this resource - e.g. 'data.images'
     - "**resource** - The file name of the resource in the python package - e.g. 'splat.png' - Use a 'package' and 'resource' or a 'path' not both.
-    - "**sub_surface_rect**" - An optional rectangle (described like "x,y,width,height") that will be used to grab a smaller portion of the image specified. This allows us to create many image surfaces from one image file.
+    - "**sub_surface_rect**" - A rectangle describing a portion of the image to use. Defaults to the whole image.
     - "**premultiplied**" - Optional parameter to declare that a loaded image already contains premultiplied alpha and does not need premultiplying. Set to "1" to enable, "0" to disable (default).
+    - "**position**" - A tuple of two floats (x, y) from 0.0 to 1.0 specifying where to position the image. (0.0, 0.0) places the top-left corner of the image at the top-left of the button, (1.0, 1.0) places the bottom-right corner at the bottom-right, and (0.5, 0.5) centers the image. Defaults to (0.5, 0.5).
 
  - "**hovered_image**" - The image displayed in the buttons hovered state. It has the following block of sub-parameters:
 
     - "**path**" - The string path to the image to be displayed. OR
     - "**package** - The name of the python package containing this resource - e.g. 'data.images'
     - "**resource** - The file name of the resource in the python package - e.g. 'splat.png' - Use a 'package' and 'resource' or a 'path' not both.
-    - "**sub_surface_rect**" - An optional rectangle (described like "x,y,width,height") that will be used to grab a smaller portion of the image specified. This allows us to create many image surfaces from one image file.
+    - "**sub_surface_rect**" - A rectangle describing a portion of the image to use. Defaults to the whole image.
     - "**premultiplied**" - Optional parameter to declare that a loaded image already contains premultiplied alpha and does not need premultiplying. Set to "1" to enable, "0" to disable (default).
+    - "**position**" - A tuple of two floats (x, y) from 0.0 to 1.0 specifying where to position the image. (0.0, 0.0) places the top-left corner of the image at the top-left of the button, (1.0, 1.0) places the bottom-right corner at the bottom-right, and (0.5, 0.5) centers the image. Defaults to (0.5, 0.5).
 
  - "**selected_image**" - The image displayed in the buttons select focused state. It has the following block of sub-parameters:
 
     - "**path**" - The string path to the image to be displayed. OR
     - "**package** - The name of the python package containing this resource - e.g. 'data.images'
     - "**resource** - The file name of the resource in the python package - e.g. 'splat.png' - Use a 'package' and 'resource' or a 'path' not both.
-    - "**sub_surface_rect**" - An optional rectangle (described like "x,y,width,height") that will be used to grab a smaller portion of the image specified. This allows us to create many image surfaces from one image file.
+    - "**sub_surface_rect**" - A rectangle describing a portion of the image to use. Defaults to the whole image.
     - "**premultiplied**" - Optional parameter to declare that a loaded image already contains premultiplied alpha and does not need premultiplying. Set to "1" to enable, "0" to disable (default).
+    - "**position**" - A tuple of two floats (x, y) from 0.0 to 1.0 specifying where to position the image. (0.0, 0.0) places the top-left corner of the image at the top-left of the button, (1.0, 1.0) places the bottom-right corner at the bottom-right, and (0.5, 0.5) centers the image. Defaults to (0.5, 0.5).
 
  - "**disabled_image**" - The image displayed in the buttons disabled state. It has the following block of sub-parameters:
 
     - "**path**" - The string path to the image to be displayed. OR
     - "**package** - The name of the python package containing this resource - e.g. 'data.images'
     - "**resource** - The file name of the resource in the python package - e.g. 'splat.png' - Use a 'package' and 'resource' or a 'path' not both.
-    - "**sub_surface_rect**" - An optional rectangle (described like "x,y,width,height") that will be used to grab a smaller portion of the image specified. This allows us to create many image surfaces from one image file.
+    - "**sub_surface_rect**" - A rectangle describing a portion of the image to use. Defaults to the whole image.
     - "**premultiplied**" - Optional parameter to declare that a loaded image already contains premultiplied alpha and does not need premultiplying. Set to "1" to enable, "0" to disable (default).
+    - "**position**" - A tuple of two floats (x, y) from 0.0 to 1.0 specifying where to position the image. (0.0, 0.0) places the top-left corner of the image at the top-left of the button, (1.0, 1.0) places the bottom-right corner at the bottom-right, and (0.5, 0.5) centers the image. Defaults to (0.5, 0.5).
 
 **Multi-Image Mode (New)**
 
@@ -133,8 +137,9 @@ For advanced buttons with layered visual effects, use these parameters to specif
     - "**path**" - The string path to the image to be displayed. OR
     - "**package** - The name of the python package containing this resource - e.g. 'data.images'
     - "**resource** - The file name of the resource in the python package - e.g. 'splat.png' - Use a 'package' and 'resource' or a 'path' not both.
-    - "**sub_surface_rect**" - An optional rectangle (described like "x,y,width,height") that will be used to grab a smaller portion of the image specified.
+    - "**sub_surface_rect**" - A rectangle describing a portion of the image to use. Defaults to the whole image.
     - "**premultiplied**" - Optional parameter to declare that a loaded image already contains premultiplied alpha and does not need premultiplying. Set to "1" to enable, "0" to disable (default).
+    - "**position**" - A tuple of two floats (x, y) from 0.0 to 1.0 specifying where to position the image. (0.0, 0.0) places the top-left corner of the image at the top-left of the button, (1.0, 1.0) places the bottom-right corner at the bottom-right, and (0.5, 0.5) centers the image. Defaults to (0.5, 0.5).
 
  - "**hovered_images**" - A list of images displayed in the button's hovered state. Uses the same sub-parameters as normal_images.
 
@@ -235,8 +240,9 @@ Here are examples of button blocks in JSON theme files using the parameters desc
             {
                 "normal_image": {
                     "package": "data.images",
-                    "resource": "buttons.png",
-                    "sub_surface_rect": "0,0,32,32"
+                    "resource": "splat.png",
+                    "sub_surface_rect": "0,0,32,32",
+                    "position": [0.8, 0.2]
                 },
                 "hovered_image": {
                     "package": "data.images",
@@ -289,59 +295,51 @@ Here are examples of button blocks in JSON theme files using the parameters desc
         {
             "colours":
             {
-                "normal_text": "#FFFFFF",
-                "hovered_text": "#FFFF00",
-                "selected_text": "#00FF00",
-                "disabled_text": "#808080"
+                "normal_bg": "#25292e",
+                "hovered_bg": "#35393e",
+                "disabled_bg": "#25292e",
+                "selected_bg": "#25292e",
+                "active_bg": "#193784",
+                "normal_text": "#c5cbd8",
+                "hovered_text": "#FFFFFF",
+                "selected_text": "#FFFFFF",
+                "disabled_text": "#6d736f",
+                "active_text": "#FFFFFF"
             },
             "images":
             {
                 "normal_images": [
                     {
                         "id": "background",
-                        "path": "images/button_bg.png",
-                        "layer": 0
+                        "layer": 0,
+                        "path": "data/images/button_bg.png",
+                        "position": [0.5, 0.5]
                     },
                     {
                         "id": "icon",
-                        "path": "images/button_icon.png",
-                        "layer": 1
-                    }
-                ],
-                "hovered_images": [
-                    {
-                        "id": "background",
-                        "path": "images/button_bg.png",
-                        "layer": 0
-                    },
-                    {
-                        "id": "icon",
-                        "path": "images/button_icon.png",
-                        "layer": 1
+                        "layer": 1,
+                        "path": "data/images/button_icon.png",
+                        "position": [0.1, 0.5]
                     },
                     {
-                        "id": "glow_effect",
-                        "path": "images/button_glow.png",
-                        "layer": 2
+                        "id": "decoration",
+                        "layer": 2,
+                        "path": "data/images/button_decoration.png",
+                        "position": [0.9, 0.1]
                     }
                 ],
-                "selected_images": [
+                "hovered_images": [
                     {
-                        "id": "background_selected",
-                        "path": "images/button_bg_selected.png",
-                        "layer": 0
+                        "id": "background",
+                        "layer": 0,
+                        "path": "data/images/button_bg_hover.png",
+                        "position": [0.5, 0.5]
                     },
                     {
                         "id": "icon",
-                        "path": "images/button_icon.png",
-                        "layer": 1
-                    }
-                ],
-                "disabled_images": [
-                    {
-                        "id": "background_disabled",
-                        "path": "images/button_bg_disabled.png",
-                        "layer": 0
+                        "layer": 1,
+                        "path": "data/images/button_icon_hover.png",
+                        "position": [0.1, 0.5]
                     }
                 ]
             },
diff --git a/docs/source/theme_reference/theme_check_box.rst b/docs/source/theme_reference/theme_check_box.rst
index 74481b7..a831595 100644
--- a/docs/source/theme_reference/theme_check_box.rst
+++ b/docs/source/theme_reference/theme_check_box.rst
@@ -62,30 +62,28 @@ For simple checkboxes with one image per state, use these parameters:
 
  - "**disabled_image**" - The image displayed in the checkbox's disabled state. Uses the same sub-parameters as normal_image.
 
-**Multi-Image Mode (New)**
+**Multi-Image Mode**
 
-For advanced checkboxes with layered visual effects, use these parameters to specify multiple images per state:
+For more complex checkboxes with multiple images per state and precise positioning control, use these parameters:
 
- - "**normal_images**" - A list of images displayed in the checkbox's default state, rendered in layer order. Each image in the list has these sub-parameters:
+ - "**normal_images**" - A list of images for the normal state. Each image has the following parameters:
 
-    - "**id**" - A unique identifier for this image layer (e.g., "background", "border", "icon").
-    - "**layer**" - The rendering layer order (lower numbers render first/behind, higher numbers render last/on top).
+    - "**id**" - A unique identifier for this image.
     - "**path**" - The string path to the image to be displayed. OR
-    - "**package** - The name of the python package containing this resource - e.g. 'data.images'
-    - "**resource** - The file name of the resource in the python package - e.g. 'splat.png' - Use a 'package' and 'resource' or a 'path' not both.
-    - "**sub_surface_rect**" - An optional rectangle (described like "x,y,width,height") that will be used to grab a smaller portion of the image specified.
-    - "**premultiplied**" - Optional parameter to declare that a loaded image already contains premultiplied alpha and does not need premultiplying. Set to "1" to enable, "0" to disable (default).
-
- - "**hovered_images**" - A list of images displayed in the checkbox's hovered state. Uses the same sub-parameters as normal_images.
-
- - "**selected_images**" - A list of images displayed in the checkbox's checked/indeterminate state. Uses the same sub-parameters as normal_images.
-
- - "**disabled_images**" - A list of images displayed in the checkbox's disabled state. Uses the same sub-parameters as normal_images.
-
-**Image Mode Notes:**
-
-- The checkbox automatically detects whether to use single-image or multi-image mode based on the parameters provided.
-- Multi-image mode allows for complex visual effects like layered backgrounds, borders, check marks, and decorative elements.
+    - "**package**" - The name of the python package containing this resource.
+    - "**resource**" - The file name of the resource in the python package.
+    - "**layer**" - The layer number for rendering order (lower numbers render first).
+    - "**position**" - Optional tuple of (x, y) values between 0.0 and 1.0 representing relative position. Defaults to (0.5, 0.5) for center.
+    - "**premultiplied**" - Optional parameter to declare that a loaded image already contains premultiplied alpha.
+
+ - "**hovered_images**" - A list of images for the hovered state. Uses the same parameters as normal_images.
+ - "**selected_images**" - A list of images for the checked/indeterminate state. Uses the same parameters as normal_images.
+ - "**disabled_images**" - A list of images for the disabled state. Uses the same parameters as normal_images.
+
+**Image Positioning Notes**:
+- The "position" parameter accepts values between 0.0 and 1.0 for both x and y coordinates.
+- (0.0, 0.0) represents top-left, (1.0, 1.0) represents bottom-right.
+- If no position is specified, the image will be centered (0.5, 0.5).
 - Images are rendered in layer order (lowest layer number first), allowing precise control over visual composition.
 - If a state doesn't specify images, it will fall back to the normal state images.
 - Both modes work alongside the traditional text-based check symbols.
diff --git a/docs/source/theme_reference/theme_panel.rst b/docs/source/theme_reference/theme_panel.rst
index 8d6156e..555d09b 100644
--- a/docs/source/theme_reference/theme_panel.rst
+++ b/docs/source/theme_reference/theme_panel.rst
@@ -29,10 +29,11 @@ For simple panels with one background image, use this parameter:
 
  - "**background_image**" - The image displayed on the panel. It has the following block of sub-parameters:
 
+    - "**position**" - Optional tuple of (x, y) values between 0.0 and 1.0 representing relative position. Defaults to (0.5, 0.5) for center.
     - "**path**" - The string path to the image to be displayed. OR
     - "**package** - The name of the python package containing this resource - e.g. 'data.images'
     - "**resource** - The file name of the resource in the python package - e.g. 'splat.png' - Use a 'package' and 'resource' or a 'path' not both.
-    - "**sub_surface_rect**" - An optional rectangle (described like "x,y,width,height") that will be used to grab a smaller portion of the image specified. This allows us to create many image surfaces from one image file.
+    - "**sub_surface_rect**" - An optional rectangle (described like "x,y,width,height") that will be used to grab a smaller portion of the image specified.
     - "**premultiplied**" - Optional parameter to declare that a loaded image already contains premultiplied alpha and does not need premultiplying. Set to "1" to enable, "0" to disable (default).
 
 **Multi-Image Mode (New)**
@@ -43,6 +44,7 @@ For advanced panels with layered visual effects, use this parameter to specify m
 
     - "**id**" - A unique identifier for this image layer (e.g., "background", "texture", "overlay").
     - "**layer**" - The rendering layer order (lower numbers render first/behind, higher numbers render last/on top).
+    - "**position**" - Optional tuple of (x, y) values between 0.0 and 1.0 representing relative position. Defaults to (0.5, 0.5) for center.
     - "**path**" - The string path to the image to be displayed. OR
     - "**package** - The name of the python package containing this resource - e.g. 'data.images'
     - "**resource** - The file name of the resource in the python package - e.g. 'splat.png' - Use a 'package' and 'resource' or a 'path' not both.
@@ -54,7 +56,13 @@ For advanced panels with layered visual effects, use this parameter to specify m
 - The panel automatically detects whether to use single-image or multi-image mode based on the parameters provided.
 - Multi-image mode allows for complex visual effects like layered backgrounds, textures, patterns, and overlays.
 - Images are rendered in layer order (lowest layer number first), allowing precise control over visual composition.
-- Both modes are fully compatible with panel scaling and positioning.
+- Both modes support image positioning for precise control over where images appear within the panel.
+- Position values are relative coordinates between 0.0 and 1.0, where:
+  - (0.0, 0.0) represents the top-left corner
+  - (1.0, 0.0) represents the top-right corner
+  - (0.0, 1.0) represents the bottom-left corner
+  - (1.0, 1.0) represents the bottom-right corner
+  - (0.5, 0.5) represents the center (default)
 
 Misc
 ----
@@ -149,3 +157,50 @@ Here are examples of panel blocks in JSON theme files using the parameters descr
             }
         }
     }
+
+**Multi-Image Mode Example with Positioning:**
+
+.. code-block:: json
+   :caption: panel_multi_image_positioned.json
+   :linenos:
+
+    {
+        "panel":
+        {
+            "colours":
+            {
+                "dark_bg":"#21282D",
+                "normal_border": "#999999"
+            },
+            "images":
+            {
+                "background_images": [
+                    {
+                        "id": "background",
+                        "path": "data/images/background.png",
+                        "layer": 0,
+                        "position": [0.5, 0.5]  // Centered
+                    },
+                    {
+                        "id": "top_left_logo",
+                        "path": "data/images/logo.png",
+                        "layer": 1,
+                        "position": [0.0, 0.0]  // Top-left corner
+                    },
+                    {
+                        "id": "bottom_right_watermark",
+                        "path": "data/images/watermark.png",
+                        "layer": 2,
+                        "position": [1.0, 1.0]  // Bottom-right corner
+                    }
+                ]
+            },
+            "misc":
+            {
+                "shape": "rounded_rectangle",
+                "shape_corner_radius": "10",
+                "border_width": "1",
+                "shadow_width": "15"
+            }
+        }
+    }
diff --git a/pygame_gui/core/drawable_shapes/drawable_shape.py b/pygame_gui/core/drawable_shapes/drawable_shape.py
index 2467398..af4caa0 100644
--- a/pygame_gui/core/drawable_shapes/drawable_shape.py
+++ b/pygame_gui/core/drawable_shapes/drawable_shape.py
@@ -568,12 +568,36 @@ class DrawableShape:
             images = self.theming[images_key]
             if images:  # Only process if there are actually images
                 state_surface = self.states[state_str].surface
+
+                # Get corresponding position data
+                positions_key = images_key.replace("_images", "_image_positions")
+                positions = self.theming.get(positions_key, [])
+
                 # Draw each image in layer order (they should already be sorted by layer)
-                for image in images:
+                for i, image in enumerate(images):
                     if image is not None:
-                        # Center the image on the surface
+                        # Get position for this image (default to center if not specified)
+                        if i < len(positions):
+                            pos_x, pos_y = positions[i]
+                        else:
+                            pos_x, pos_y = 0.5, 0.5  # Default to center
+
+                        # Calculate actual pixel position
+                        surface_rect = state_surface.get_rect()
                         image_rect = image.get_rect()
-                        image_rect.center = state_surface.get_rect().center
+
+                        # Position represents where to place the image based on relative coordinates
+                        # (1.0, 1.0) should place the bottom-right corner of the image at the
+                        # bottom-right of the element
+                        target_x = surface_rect.width * pos_x - image_rect.width * pos_x
+                        target_y = (
+                            surface_rect.height * pos_y - image_rect.height * pos_y
+                        )
+
+                        # Set the image position
+                        image_rect.x = int(target_x)
+                        image_rect.y = int(target_y)
+
                         state_surface.blit(image, image_rect)
 
         # Handle text
diff --git a/pygame_gui/core/ui_appearance_theme.py b/pygame_gui/core/ui_appearance_theme.py
index 568dd5c..0584fad 100644
--- a/pygame_gui/core/ui_appearance_theme.py
+++ b/pygame_gui/core/ui_appearance_theme.py
@@ -5,7 +5,7 @@ import warnings
 
 from contextlib import contextmanager
 from importlib.resources import files, as_file
-from typing import Union, List, Dict, Any, Optional, cast
+from typing import Union, List, Dict, Any, Optional, cast, Tuple
 
 import pygame
 
@@ -530,6 +530,56 @@ class UIThemeValidator:
                                 f"Must be string or list/tuple, got {type(rect_data).__name__}"
                             )
 
+                    # Validate position if present (new parameter)
+                    if "position" in img_item:
+                        position_data = img_item["position"]
+                        if isinstance(position_data, str):
+                            try:
+                                pos_parts = position_data.strip().split(",")
+                                if len(pos_parts) != 2:
+                                    errors.append(
+                                        f"{element_type}.images.{image_name}[{i}].position: "
+                                        f"Must have 2 comma-separated values, got {len(pos_parts)}"
+                                    )
+                                else:
+                                    for part in pos_parts:
+                                        val = float(part.strip())
+                                        if not 0.0 <= val <= 1.0:
+                                            errors.append(
+                                                f"{element_type}.images.{image_name}[{i}].position: "
+                                                f"Values must be between 0.0 and 1.0, got {val}"
+                                            )
+                            except ValueError:
+                                errors.append(
+                                    f"{element_type}.images.{image_name}[{i}].position: "
+                                    f"All values must be floats: '{position_data}'"
+                                )
+                        elif isinstance(position_data, (list, tuple)):
+                            if len(position_data) != 2:
+                                errors.append(
+                                    f"{element_type}.images.{image_name}[{i}].position: "
+                                    f"Must have 2 values, got {len(position_data)}"
+                                )
+                            elif not all(
+                                isinstance(x, (int, float)) for x in position_data
+                            ):
+                                errors.append(
+                                    f"{element_type}.images.{image_name}[{i}].position: "
+                                    f"All values must be numbers"
+                                )
+                            else:
+                                for val in position_data:
+                                    if not 0.0 <= val <= 1.0:
+                                        errors.append(
+                                            f"{element_type}.images.{image_name}[{i}].position: "
+                                            f"Values must be between 0.0 and 1.0, got {val}"
+                                        )
+                        else:
+                            errors.append(
+                                f"{element_type}.images.{image_name}[{i}].position: "
+                                f"Must be string or list/tuple, got {type(position_data).__name__}"
+                            )
+
             # Handle single image format (existing validation)
             elif isinstance(image_data, dict):
                 # Check for required path or package/resource
@@ -603,6 +653,56 @@ class UIThemeValidator:
                             f"{element_type}.images.{image_name}.sub_surface_rect: "
                             f"Must be string or list/tuple, got {type(rect_data).__name__}"
                         )
+
+                # Validate position if present (new parameter)
+                if "position" in image_data:
+                    position_data = image_data["position"]
+                    if isinstance(position_data, str):
+                        try:
+                            pos_parts = position_data.strip().split(",")
+                            if len(pos_parts) != 2:
+                                errors.append(
+                                    f"{element_type}.images.{image_name}.position: "
+                                    f"Must have 2 comma-separated values, got {len(pos_parts)}"
+                                )
+                            else:
+                                for part in pos_parts:
+                                    val = float(part.strip())
+                                    if not 0.0 <= val <= 1.0:
+                                        errors.append(
+                                            f"{element_type}.images.{image_name}.position: "
+                                            f"Values must be between 0.0 and 1.0, got {val}"
+                                        )
+                        except ValueError:
+                            errors.append(
+                                f"{element_type}.images.{image_name}.position: "
+                                f"All values must be floats: '{position_data}'"
+                            )
+                    elif isinstance(position_data, (list, tuple)):
+                        if len(position_data) != 2:
+                            errors.append(
+                                f"{element_type}.images.{image_name}.position: "
+                                f"Must have 2 values, got {len(position_data)}"
+                            )
+                        elif not all(
+                            isinstance(x, (int, float)) for x in position_data
+                        ):
+                            errors.append(
+                                f"{element_type}.images.{image_name}.position: "
+                                f"All values must be numbers"
+                            )
+                        else:
+                            for val in position_data:
+                                if not 0.0 <= val <= 1.0:
+                                    errors.append(
+                                        f"{element_type}.images.{image_name}.position: "
+                                        f"Values must be between 0.0 and 1.0, got {val}"
+                                    )
+                    else:
+                        errors.append(
+                            f"{element_type}.images.{image_name}.position: "
+                            f"Must be string or list/tuple, got {type(position_data).__name__}"
+                        )
             else:
                 errors.append(
                     f"{element_type}.images.{image_name}: "
@@ -784,7 +884,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
 
         self.ui_element_fonts_info: Dict[str, Dict[str, FontThemeInfo]] = {}
         self.ui_element_image_locs: Dict[
-            str, Dict[str, Dict[str, str | bool | pygame.Rect]]
+            str, Dict[str, Dict[str, str | bool | pygame.Rect | Tuple[float, float]]]
         ] = {}
         self.ele_font_res: Dict[str, Dict[str, FontResource]] = {}
         self.ui_element_image_surfaces: Dict[
@@ -1047,9 +1147,8 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                                 if image_resource is not None:
                                     if "sub_surface_rect" in img_data:
                                         surface_id = (
-                                            image_resource.image_id
-                                            + str(img_data["sub_surface_rect"])
-                                            + f"_layer_{img_data['layer']}"
+                                            f"{image_resource.image_id}_{element_key}_"
+                                            f"{image_id}_{img_data['id']}_layer_{img_data['layer']}"
                                         )
                                         if surface_id in self.surface_resources:
                                             surf_resource = self.surface_resources[
@@ -1084,8 +1183,8 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                                                 )
                                     else:
                                         surface_id = (
-                                            image_resource.image_id
-                                            + f"_layer_{img_data['layer']}"
+                                            f"{image_resource.image_id}_{element_key}_"
+                                            f"{image_id}_{img_data['id']}_layer_{img_data['layer']}"
                                         )
                                         if surface_id in self.surface_resources:
                                             surf_resource = self.surface_resources[
@@ -1108,6 +1207,11 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                                     # Note: These attributes may need to be added to SurfaceResource class
                                     setattr(surf_resource, "layer", img_data["layer"])
                                     setattr(surf_resource, "image_id", img_data["id"])
+                                    setattr(
+                                        surf_resource,
+                                        "position",
+                                        img_data.get("position", (0.5, 0.5)),
+                                    )
                                     multi_surfaces.append(surf_resource)
 
                         # Store the list of surface resources
@@ -1142,8 +1246,9 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
 
                         if image_resource is not None:
                             if "sub_surface_rect" in single_image_data:
-                                surface_id = image_resource.image_id + str(
-                                    single_image_data["sub_surface_rect"]
+                                surface_id = (
+                                    f"{image_resource.image_id}_{element_key}_"
+                                    f"{image_id}_{str(single_image_data['sub_surface_rect'])}"
                                 )
                                 if surface_id in self.surface_resources:
                                     surf_resource = self.surface_resources[surface_id]
@@ -1173,7 +1278,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                                             surf_resource
                                         )
                             else:
-                                surface_id = image_resource.image_id
+                                surface_id = f"{image_resource.image_id}_{element_key}_{image_id}"
                                 if surface_id in self.surface_resources:
                                     surf_resource = self.surface_resources[surface_id]
                                 else:
@@ -1189,6 +1294,12 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                                             surf_resource.image_resource.loaded_surface
                                         )
 
+                            # Add position information to the surface resource
+                            setattr(
+                                surf_resource,
+                                "position",
+                                single_image_data.get("position", (0.5, 0.5)),
+                            )
                             self.ui_element_image_surfaces[element_key][image_id] = (
                                 surf_resource
                             )
@@ -1634,6 +1745,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                         {
                             "id": getattr(surf, "image_id", "unnamed"),
                             "layer": getattr(surf, "layer", 0),
+                            "position": getattr(surf, "position", (0.5, 0.5)),
                             "surface": surf.surface,
                         }
                         for surf in surfaces
@@ -1646,6 +1758,9 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                         {
                             "id": "single",
                             "layer": 0,
+                            "position": getattr(
+                                surface_resource, "position", (0.5, 0.5)
+                            ),
                             "surface": surface_resource.surface,
                         }
                     ]
@@ -1971,7 +2086,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                 if locale_key not in self.ui_element_fonts_info:
                     self.ui_element_fonts_info[element_name][locale_key] = {
                         "name": "",
-                        "size": 0,
+                        "size": self.font_dict.default_font.size,
                         "bold": False,
                         "italic": False,
                         "antialiased": True,
@@ -2270,6 +2385,57 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                                 img_resource_data["changed"] = True
                             img_resource_data["sub_surface_rect"] = rect
 
+                    # Handle position if present (new parameter)
+                    if "position" in img_item:
+                        position_data = img_item["position"]
+                        position = None
+
+                        if isinstance(position_data, str):
+                            try:
+                                pos_parts = position_data.strip().split(",")
+                                if len(pos_parts) == 2:
+                                    position = (
+                                        float(pos_parts[0].strip()),
+                                        float(pos_parts[1].strip()),
+                                    )
+                            except (ValueError, TypeError):
+                                warnings.warn(
+                                    f"Unable to parse position from string: {position_data}"
+                                )
+                        elif (
+                            isinstance(position_data, (list, tuple))
+                            and len(position_data) == 2
+                        ):
+                            try:
+                                position = (
+                                    float(position_data[0]),
+                                    float(position_data[1]),
+                                )
+                            except (ValueError, TypeError):
+                                warnings.warn(
+                                    f"Unable to parse position from list/tuple: {position_data}"
+                                )
+
+                        if position is not None:
+                            # Clamp values to valid range
+                            position = (
+                                max(0.0, min(1.0, position[0])),
+                                max(0.0, min(1.0, position[1])),
+                            )
+
+                            if (
+                                existing_img
+                                and existing_img.get("position") != position
+                            ):
+                                img_resource_data["changed"] = True
+                            img_resource_data["position"] = position
+                        else:
+                            # Default position (center)
+                            img_resource_data["position"] = (0.5, 0.5)
+                    else:
+                        # Default position (center) if not specified
+                        img_resource_data["position"] = (0.5, 0.5)
+
                     # Update or add the image to the multi-image list
                     if existing_img:
                         existing_img.update(img_resource_data)
@@ -2338,6 +2504,57 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                             img_res_dict["changed"] = True
                         img_res_dict["sub_surface_rect"] = rect
 
+                # Handle position if present (new parameter)
+                if "position" in image_data:
+                    position_data = image_data["position"]
+                    position = None
+
+                    if isinstance(position_data, str):
+                        try:
+                            pos_parts = position_data.strip().split(",")
+                            if len(pos_parts) == 2:
+                                position = (
+                                    float(pos_parts[0].strip()),
+                                    float(pos_parts[1].strip()),
+                                )
+                        except (ValueError, TypeError):
+                            warnings.warn(
+                                f"Unable to parse position from string: {position_data}"
+                            )
+                    elif (
+                        isinstance(position_data, (list, tuple))
+                        and len(position_data) == 2
+                    ):
+                        try:
+                            position = (
+                                float(position_data[0]),
+                                float(position_data[1]),
+                            )
+                        except (ValueError, TypeError):
+                            warnings.warn(
+                                f"Unable to parse position from list/tuple: {position_data}"
+                            )
+
+                    if position is not None:
+                        # Clamp values to valid range
+                        position = (
+                            max(0.0, min(1.0, position[0])),
+                            max(0.0, min(1.0, position[1])),
+                        )
+
+                        if (
+                            "position" in img_res_dict
+                            and position != img_res_dict["position"]
+                        ):
+                            img_res_dict["changed"] = True
+                        img_res_dict["position"] = position
+                    else:
+                        # Default position (center)
+                        img_res_dict["position"] = (0.5, 0.5)
+                else:
+                    # Default position (center) if not specified
+                    img_res_dict["position"] = (0.5, 0.5)
+
     def _load_element_colour_data_from_theme(
         self, data_type: str, element_name: str, element_theming: Dict[str, Any]
     ):
@@ -2392,7 +2609,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
         if locale not in self.ui_element_fonts_info[element_name]:
             self.ui_element_fonts_info[element_name][locale] = {
                 "name": "",
-                "size": 0,
+                "size": self.font_dict.default_font.size,
                 "bold": False,
                 "italic": False,
                 "antialiased": True,
diff --git a/pygame_gui/elements/ui_button.py b/pygame_gui/elements/ui_button.py
index c71586a..214d7f3 100644
--- a/pygame_gui/elements/ui_button.py
+++ b/pygame_gui/elements/ui_button.py
@@ -146,6 +146,12 @@ class UIButton(UIElement):
         self.selected_images: List[pygame.Surface] = []
         self.disabled_images: List[pygame.Surface] = []
 
+        # Position support for images - tuples of (x, y) where 0.0-1.0 represents relative position
+        self.normal_image_positions: List[Tuple[float, float]] = []
+        self.hovered_image_positions: List[Tuple[float, float]] = []
+        self.selected_image_positions: List[Tuple[float, float]] = []
+        self.disabled_image_positions: List[Tuple[float, float]] = []
+
         self.text_horiz_alignment = "center"
         self.text_vert_alignment = "center"
         self.text_horiz_alignment_padding = 0
@@ -219,31 +225,78 @@ class UIButton(UIElement):
         """
         changed = False
 
-        # State mappings for loading images
-        state_mappings = [
-            ("normal", "normal_images"),
-            ("hovered", "hovered_images"),
-            ("selected", "selected_images"),
-            ("disabled", "disabled_images"),
+        # Process normal state first to establish baseline for fallbacks
+        normal_images = []
+        normal_positions = []
+
+        # First try to load multi-image format for normal state
+        try:
+            image_details = self.ui_theme.get_image_details(
+                "normal_images", self.combined_element_ids
+            )
+            normal_images = [detail["surface"] for detail in image_details]
+            normal_positions = [detail["position"] for detail in image_details]
+        except LookupError:
+            # Fall back to single image format for normal state
+            try:
+                image_details = self.ui_theme.get_image_details(
+                    "normal_image", self.combined_element_ids
+                )
+                if image_details:
+                    normal_images = [detail["surface"] for detail in image_details]
+                    normal_positions = [detail["position"] for detail in image_details]
+            except LookupError:
+                # No normal image found
+                pass
+
+        # Apply auto-scaling to normal images if enabled
+        if normal_images and self.auto_scale_images:
+            scaled_images = []
+            for img in normal_images:
+                scaled_img = self._scale_image_to_fit(img, self.rect.size)
+                scaled_images.append(scaled_img)
+            normal_images = scaled_images
+
+        # Ensure we have positions for all normal images (default to center if missing)
+        while len(normal_positions) < len(normal_images):
+            normal_positions.append((0.5, 0.5))
+
+        # Check if normal images have changed
+        if (
+            normal_images != self.normal_images
+            or normal_positions != self.normal_image_positions
+        ):
+            self.normal_images = normal_images
+            self.normal_image_positions = normal_positions
+            changed = True
+
+        # Now process other states with fallback to normal
+        other_state_mappings = [
+            ("hovered", "hovered_images", "hovered_image_positions"),
+            ("selected", "selected_images", "selected_image_positions"),
+            ("disabled", "disabled_images", "disabled_image_positions"),
         ]
 
-        for state_name, attr_name in state_mappings:
+        for state_name, attr_name, position_attr_name in other_state_mappings:
             new_images = []
+            new_positions = []
 
-            # First try to load multi-image format (e.g., "normal_images")
+            # First try to load multi-image format (e.g., "hovered_images")
             try:
-                multi_images = self.ui_theme.get_images(
+                image_details = self.ui_theme.get_image_details(
                     f"{state_name}_images", self.combined_element_ids
                 )
-                new_images = multi_images
+                new_images = [detail["surface"] for detail in image_details]
+                new_positions = [detail["position"] for detail in image_details]
             except LookupError:
-                # Fall back to single image format (e.g., "normal_image")
+                # Fall back to single image format (e.g., "hovered_image")
                 try:
-                    single_image = self.ui_theme.get_image(
+                    image_details = self.ui_theme.get_image_details(
                         f"{state_name}_image", self.combined_element_ids
                     )
-                    if single_image is not None:
-                        new_images = [single_image]  # Convert to list
+                    if image_details:
+                        new_images = [detail["surface"] for detail in image_details]
+                        new_positions = [detail["position"] for detail in image_details]
                 except LookupError:
                     # No image found for this state
                     pass
@@ -256,15 +309,22 @@ class UIButton(UIElement):
                     scaled_images.append(scaled_img)
                 new_images = scaled_images
 
-            # Handle fallbacks for non-normal states
-            if not new_images and state_name != "normal":
-                # Fall back to normal_images
-                new_images = self.normal_images.copy()
+            # Handle fallbacks to normal state
+            if not new_images:
+                # Fall back to normal_images and normal_image_positions
+                new_images = normal_images.copy()
+                new_positions = normal_positions.copy()
+
+            # Ensure we have positions for all images (default to center if missing)
+            while len(new_positions) < len(new_images):
+                new_positions.append((0.5, 0.5))
 
             # Check if images have changed
             current_images = getattr(self, attr_name)
-            if new_images != current_images:
+            current_positions = getattr(self, position_attr_name, [])
+            if new_images != current_images or new_positions != current_positions:
                 setattr(self, attr_name, new_images)
+                setattr(self, position_attr_name, new_positions)
                 changed = True
 
         return changed
@@ -880,6 +940,12 @@ class UIButton(UIElement):
             "disabled_images": self.disabled_images,
             "selected_images": self.selected_images,
             "active_images": self.selected_images,
+            # Position support for images
+            "normal_image_positions": self.normal_image_positions,
+            "hovered_image_positions": self.hovered_image_positions,
+            "disabled_image_positions": self.disabled_image_positions,
+            "selected_image_positions": self.selected_image_positions,
+            "active_image_positions": self.selected_image_positions,
             "border_width": self.border_width,
             "shadow_width": self.shadow_width,
             "font": self.font,
@@ -1003,6 +1069,22 @@ class UIButton(UIElement):
         else:
             return self.normal_images
 
+    def get_current_image_positions(self) -> List[Tuple[float, float]]:
+        """
+        Get the current image positions for the button's current state.
+        Returns a list of position tuples.
+
+        :return: List of (x, y) position tuples for the current state
+        """
+        if not self.is_enabled:
+            return self.disabled_image_positions
+        elif self.is_selected:
+            return self.selected_image_positions
+        elif self.hovered:
+            return self.hovered_image_positions
+        else:
+            return self.normal_image_positions
+
     def is_multi_image_mode(self) -> bool:
         """
         Check if the button is currently using multi-image mode.
diff --git a/pygame_gui/elements/ui_check_box.py b/pygame_gui/elements/ui_check_box.py
index d10953d..3415642 100644
--- a/pygame_gui/elements/ui_check_box.py
+++ b/pygame_gui/elements/ui_check_box.py
@@ -1,4 +1,4 @@
-from typing import Optional, Union, Dict, Set, Any, List
+from typing import Optional, Union, Dict, Set, Any, List, Tuple
 
 import pygame
 
@@ -120,6 +120,12 @@ class UICheckBox(UIElement):
         self.hovered_images: List[pygame.Surface] = []
         self.disabled_images: List[pygame.Surface] = []
 
+        # Position support for images - tuples of (x, y) where 0.0-1.0 represents relative position
+        self.normal_image_positions: List[Tuple[float, float]] = []
+        self.selected_image_positions: List[Tuple[float, float]] = []
+        self.hovered_image_positions: List[Tuple[float, float]] = []
+        self.disabled_image_positions: List[Tuple[float, float]] = []
+
         # Get text offset from theme
         self.text_offset = 5
         try:
@@ -188,6 +194,11 @@ class UICheckBox(UIElement):
             "text": self._get_display_symbol(),
             "text_horiz_alignment": "center",
             "text_vert_alignment": "center",
+            # Position support for images
+            "normal_image_positions": self.normal_image_positions,
+            "selected_image_positions": self.selected_image_positions,
+            "hovered_image_positions": self.hovered_image_positions,
+            "disabled_image_positions": self.disabled_image_positions,
         }
 
         self.drawable_shape = RectDrawableShape(
@@ -380,44 +391,90 @@ class UICheckBox(UIElement):
         """
         changed = False
 
-        # State mappings for loading images
-        state_mappings = [
-            ("normal", "normal_images"),
-            ("hovered", "hovered_images"),
-            ("selected", "selected_images"),
-            ("disabled", "disabled_images"),
+        # Process normal state first to establish baseline for fallbacks
+        normal_images = []
+        normal_positions = []
+
+        # First try to load multi-image format for normal state
+        try:
+            image_details = self.ui_theme.get_image_details(
+                "normal_images", self.combined_element_ids
+            )
+            normal_images = [detail["surface"] for detail in image_details]
+            normal_positions = [detail["position"] for detail in image_details]
+        except LookupError:
+            # Fall back to single image format for normal state
+            try:
+                image_details = self.ui_theme.get_image_details(
+                    "normal_image", self.combined_element_ids
+                )
+                if image_details:
+                    normal_images = [detail["surface"] for detail in image_details]
+                    normal_positions = [detail["position"] for detail in image_details]
+            except LookupError:
+                # No normal image found
+                pass
+
+        # Ensure we have positions for all normal images (default to center if missing)
+        while len(normal_positions) < len(normal_images):
+            normal_positions.append((0.5, 0.5))
+
+        # Check if normal images have changed
+        if (
+            normal_images != self.normal_images
+            or normal_positions != self.normal_image_positions
+        ):
+            self.normal_images = normal_images
+            self.normal_image_positions = normal_positions
+            changed = True
+
+        # Now process other states with fallback to normal
+        other_state_mappings = [
+            ("hovered", "hovered_images", "hovered_image_positions"),
+            ("selected", "selected_images", "selected_image_positions"),
+            ("disabled", "disabled_images", "disabled_image_positions"),
         ]
 
-        for state_name, attr_name in state_mappings:
+        for state_name, attr_name, position_attr_name in other_state_mappings:
             new_images = []
+            new_positions = []
 
-            # First try to load multi-image format (e.g., "normal_images")
+            # First try to load multi-image format (e.g., "hovered_images")
             try:
-                multi_images = self.ui_theme.get_images(
+                image_details = self.ui_theme.get_image_details(
                     f"{state_name}_images", self.combined_element_ids
                 )
-                new_images = multi_images
+                new_images = [detail["surface"] for detail in image_details]
+                new_positions = [detail["position"] for detail in image_details]
             except LookupError:
-                # Fall back to single image format (e.g., "normal_image")
+                # Fall back to single image format (e.g., "hovered_image")
                 try:
-                    single_image = self.ui_theme.get_image(
+                    image_details = self.ui_theme.get_image_details(
                         f"{state_name}_image", self.combined_element_ids
                     )
-                    if single_image is not None:
-                        new_images = [single_image]  # Convert to list
+                    if image_details:
+                        new_images = [detail["surface"] for detail in image_details]
+                        new_positions = [detail["position"] for detail in image_details]
                 except LookupError:
                     # No image found for this state
                     pass
 
-            # Handle fallbacks for non-normal states
-            if not new_images and state_name != "normal":
-                # Fall back to normal_images
-                new_images = self.normal_images.copy()
+            # Handle fallbacks to normal state
+            if not new_images:
+                # Fall back to normal_images and normal_image_positions
+                new_images = normal_images.copy()
+                new_positions = normal_positions.copy()
+
+            # Ensure we have positions for all images (default to center if missing)
+            while len(new_positions) < len(new_images):
+                new_positions.append((0.5, 0.5))
 
             # Check if images have changed
             current_images = getattr(self, attr_name)
-            if new_images != current_images:
+            current_positions = getattr(self, position_attr_name, [])
+            if new_images != current_images or new_positions != current_positions:
                 setattr(self, attr_name, new_images)
+                setattr(self, position_attr_name, new_positions)
                 changed = True
 
         return changed
@@ -994,6 +1051,11 @@ class UICheckBox(UIElement):
             "selected_images": self.selected_images,
             "hovered_images": self.hovered_images,
             "disabled_images": self.disabled_images,
+            # Position support for images
+            "normal_image_positions": self.normal_image_positions,
+            "selected_image_positions": self.selected_image_positions,
+            "hovered_image_positions": self.hovered_image_positions,
+            "disabled_image_positions": self.disabled_image_positions,
             "border_width": getattr(self, "border_width", 1),
             "shadow_width": getattr(self, "shadow_width", 2),
             "shape_corner_radius": getattr(self, "shape_corner_radius", [2, 2, 2, 2]),
diff --git a/pygame_gui/elements/ui_panel.py b/pygame_gui/elements/ui_panel.py
index 5ab5172..4f69d0d 100644
--- a/pygame_gui/elements/ui_panel.py
+++ b/pygame_gui/elements/ui_panel.py
@@ -103,9 +103,12 @@ class UIPanel(UIElement, IContainerLikeInterface):
             0, 0, 0, 0
         )
 
-        # Enhanced multi-image support - always use lists internally for consistency
+        # Image support - always use lists internally for consistency
         self.background_images: List[pygame.Surface] = []
 
+        # Position support for images - tuples of (x, y) where 0.0-1.0 represents relative position
+        self.background_image_positions: List[Tuple[float, float]] = []
+
         self.border_width = 1
         self.shadow_width = 2
         self.shape = "rectangle"
@@ -182,21 +185,28 @@ class UIPanel(UIElement, IContainerLikeInterface):
         """
         changed = False
         new_images = []
+        new_positions = []
 
         # First try to load multi-image format (background_images)
         try:
-            multi_images = self.ui_theme.get_images(
+            image_details = self.ui_theme.get_image_details(
                 "background_images", self.combined_element_ids
             )
-            new_images = multi_images
+            new_images = [detail["surface"] for detail in image_details]
+            new_positions = [
+                detail.get("position", (0.5, 0.5)) for detail in image_details
+            ]
         except LookupError:
             # Fall back to single image format (background_image)
             try:
-                single_image = self.ui_theme.get_image(
+                image_details = self.ui_theme.get_image_details(
                     "background_image", self.combined_element_ids
                 )
-                if single_image is not None:
-                    new_images = [single_image]  # Convert to list
+                if image_details:
+                    new_images = [detail["surface"] for detail in image_details]
+                    new_positions = [
+                        detail.get("position", (0.5, 0.5)) for detail in image_details
+                    ]
             except LookupError:
                 # No image found for this state
                 pass
@@ -209,9 +219,17 @@ class UIPanel(UIElement, IContainerLikeInterface):
                 scaled_images.append(scaled_img)
             new_images = scaled_images
 
-        # Check if images have changed
-        if new_images != self.background_images:
+        # Ensure we have positions for all images (default to center if missing)
+        while len(new_positions) < len(new_images):
+            new_positions.append((0.5, 0.5))
+
+        # Check if images or positions have changed
+        if (
+            new_images != self.background_images
+            or new_positions != self.background_image_positions
+        ):
             self.background_images = new_images
+            self.background_image_positions = new_positions
             changed = True
 
         return changed
@@ -404,12 +422,12 @@ class UIPanel(UIElement, IContainerLikeInterface):
     def rebuild(self):
         """
         A complete rebuild of the drawable shape used by this panel.
-
         """
         theming_parameters = {
             "normal_bg": self.background_colour,
             "normal_border": self.border_colour,
             "normal_images": self.background_images,
+            "normal_image_positions": self.background_image_positions,  # Add positions
             "border_width": self.border_width,
             "shadow_width": self.shadow_width,
             "shape_corner_radius": self.shape_corner_radius,
@@ -625,3 +643,44 @@ class UIPanel(UIElement, IContainerLikeInterface):
         :return: True if auto-scaling is enabled, False otherwise
         """
         return self.auto_scale_images
+
+    def get_current_image_positions(self) -> List[Tuple[float, float]]:
+        """
+        Get the current image positions for the panel.
+        Returns a list of position tuples.
+
+        :return: List of (x, y) position tuples for the background images
+        """
+        return self.background_image_positions
+
+    def set_image_positions(self, positions: List[Tuple[float, float]]):
+        """
+        Set the positions for all background images.
+
+        :param positions: List of (x, y) tuples where x and y are between 0.0 and 1.0
+        """
+        # Only keep positions up to the number of images we have
+        if len(positions) > len(self.background_images):
+            positions = positions[: len(self.background_images)]
+
+        self.background_image_positions = positions.copy()
+        # Ensure we have positions for all images
+        while len(self.background_image_positions) < len(self.background_images):
+            self.background_image_positions.append((0.5, 0.5))
+        self.rebuild()
+
+    def set_image_position(self, index: int, position: Tuple[float, float]) -> bool:
+        """
+        Set the position of a specific background image.
+
+        :param index: Index of the image to position
+        :param position: (x, y) tuple where x and y are between 0.0 and 1.0
+        :return: True if position was set, False if index was invalid
+        """
+        if 0 <= index < len(self.background_images):
+            while len(self.background_image_positions) <= index:
+                self.background_image_positions.append((0.5, 0.5))
+            self.background_image_positions[index] = position
+            self.rebuild()
+            return True
+        return False


Test patch:
diff --git a/tests/test_elements/test_ui_button.py b/tests/test_elements/test_ui_button.py
index 38f4235..ff4c52e 100644
--- a/tests/test_elements/test_ui_button.py
+++ b/tests/test_elements/test_ui_button.py
@@ -1891,6 +1891,854 @@ class TestUIButton:
             os.unlink(single_theme_file)
             os.unlink(multi_theme_file)
 
+    def test_image_positioning_single_image_mode(self, _init_pygame, _display_surface_return_none):
+        """Test image positioning in single-image mode."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with positioned single images
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.0, 0.0]  # Top-left
+                    },
+                    "hovered_image": {
+                        "path": "tests/data/images/splat.png", 
+                        "position": [1.0, 1.0]  # Bottom-right
+                    },
+                    "selected_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.5, 0.5]  # Center
+                    },
+                    "disabled_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.2, 0.8]  # Custom position
+                    }
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            button = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                            text="Position Test",
+                            manager=manager)
+            
+            # Test normal state position
+            assert button.normal_image_positions == [(0.0, 0.0)]
+            assert button.get_current_image_positions() == [(0.0, 0.0)]
+            
+            # Test hovered state position
+            assert button.hovered_image_positions == [(1.0, 1.0)]
+            button.hovered = True
+            assert button.get_current_image_positions() == [(1.0, 1.0)]
+            button.hovered = False
+            
+            # Test selected state position
+            assert button.selected_image_positions == [(0.5, 0.5)]
+            button.select()
+            assert button.get_current_image_positions() == [(0.5, 0.5)]
+            button.unselect()
+            
+            # Test disabled state position
+            assert button.disabled_image_positions == [(0.2, 0.8)]
+            button.disable()
+            assert button.get_current_image_positions() == [(0.2, 0.8)]
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_multi_image_mode(self, _init_pygame, _display_surface_return_none):
+        """Test image positioning in multi-image mode."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with positioned multi-images
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_images": [
+                        {
+                            "id": "background",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.5, 0.5]  # Center
+                        },
+                        {
+                            "id": "icon",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.1, 0.1]  # Top-left area
+                        },
+                        {
+                            "id": "decoration",
+                            "layer": 2,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.9, 0.9]  # Bottom-right area
+                        }
+                    ],
+                    "hovered_images": [
+                        {
+                            "id": "background_hover",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.5, 0.5]  # Center
+                        },
+                        {
+                            "id": "glow",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.0, 0.0]  # Top-left corner
+                        }
+                    ]
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            button = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                            text="Multi Position Test",
+                            manager=manager)
+            
+            # Test normal state positions
+            expected_normal_positions = [(0.5, 0.5), (0.1, 0.1), (0.9, 0.9)]
+            assert button.normal_image_positions == expected_normal_positions
+            assert button.get_current_image_positions() == expected_normal_positions
+            assert len(button.normal_images) == 3
+            
+            # Test hovered state positions
+            expected_hovered_positions = [(0.5, 0.5), (0.0, 0.0)]
+            assert button.hovered_image_positions == expected_hovered_positions
+            button.hovered = True
+            assert button.get_current_image_positions() == expected_hovered_positions
+            assert len(button.get_current_images()) == 2
+            button.hovered = False
+            
+            # Test fallback to normal positions for states without specific images
+            button.select()
+            assert button.get_current_image_positions() == expected_normal_positions  # Falls back to normal
+            button.unselect()
+            
+            button.disable()
+            assert button.get_current_image_positions() == expected_normal_positions  # Falls back to normal
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_default_values(self, _init_pygame, _display_surface_return_none):
+        """Test that images without position parameters default to center (0.5, 0.5)."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme without position parameters
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_images": [
+                        {
+                            "id": "no_position",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png"
+                            # No position parameter - should default to (0.5, 0.5)
+                        },
+                        {
+                            "id": "with_position",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.2, 0.8]
+                        }
+                    ]
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            button = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                            text="Default Position Test",
+                            manager=manager)
+            
+            # First image should default to center, second should use specified position
+            expected_positions = [(0.5, 0.5), (0.2, 0.8)]
+            assert button.normal_image_positions == expected_positions
+            assert button.get_current_image_positions() == expected_positions
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_validation_valid_values(self, _init_pygame, _display_surface_return_none):
+        """Test that valid position values are accepted."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with various valid position values
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_images": [
+                        {
+                            "id": "corner_tl",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.0, 0.0]  # Top-left corner
+                        },
+                        {
+                            "id": "corner_br",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [1.0, 1.0]  # Bottom-right corner
+                        },
+                        {
+                            "id": "center",
+                            "layer": 2,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.5, 0.5]  # Center
+                        },
+                        {
+                            "id": "custom",
+                            "layer": 3,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.25, 0.75]  # Custom position
+                        }
+                    ]
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            # Should load without validation errors
+            manager = UIManager((800, 600), theme_file)
+            button = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                            text="Valid Position Test",
+                            manager=manager)
+            
+            expected_positions = [(0.0, 0.0), (1.0, 1.0), (0.5, 0.5), (0.25, 0.75)]
+            assert button.normal_image_positions == expected_positions
+            
+        finally:
+            os.unlink(theme_file)
+
+    @pytest.mark.filterwarnings("ignore:Theme validation found")
+    def test_image_positioning_validation_invalid_values(self, _init_pygame, _display_surface_return_none):
+        """Test that invalid position values are properly validated."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with invalid position values
+        invalid_themes = [
+            # Position values out of range
+            {
+                "button": {
+                    "images": {
+                        "normal_image": {
+                            "path": "tests/data/images/splat.png",
+                            "position": [2.0, 0.5]  # x > 1.0
+                        }
+                    }
+                }
+            },
+            {
+                "button": {
+                    "images": {
+                        "normal_image": {
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.5, -0.1]  # y < 0.0
+                        }
+                    }
+                }
+            },
+            # Wrong number of values
+            {
+                "button": {
+                    "images": {
+                        "normal_image": {
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.5]  # Only one value
+                        }
+                    }
+                }
+            },
+            {
+                "button": {
+                    "images": {
+                        "normal_image": {
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.5, 0.5, 0.5]  # Three values
+                        }
+                    }
+                }
+            },
+            # Non-numeric values
+            {
+                "button": {
+                    "images": {
+                        "normal_image": {
+                            "path": "tests/data/images/splat.png",
+                            "position": ["left", "top"]  # String values
+                        }
+                    }
+                }
+            }
+        ]
+        
+        for i, theme_data in enumerate(invalid_themes):
+            with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+                json.dump(theme_data, f)
+                theme_file = f.name
+            
+            try:
+                manager = UIManager((800, 600))
+                
+                # Validation should catch the errors
+                errors = manager.get_theme().validate_theme_file_path(theme_file)
+                assert len(errors) > 0, f"Theme {i} should have validation errors"
+                
+                # At least one error should mention position
+                position_errors = [error for error in errors if "position" in error.lower()]
+                assert len(position_errors) > 0, f"Theme {i} should have position-related errors"
+                
+            finally:
+                os.unlink(theme_file)
+
+    def test_image_positioning_state_fallbacks(self, _init_pygame, _display_surface_return_none):
+        """Test that position fallbacks work correctly when states don't have specific images."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with only normal images defined
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_images": [
+                        {
+                            "id": "bg",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.3, 0.3]
+                        },
+                        {
+                            "id": "icon",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.7, 0.7]
+                        }
+                    ]
+                    # No hovered, selected, or disabled images defined
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            button = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                            text="Fallback Test",
+                            manager=manager)
+            
+            expected_positions = [(0.3, 0.3), (0.7, 0.7)]
+            
+            # Normal state should have the defined positions
+            assert button.normal_image_positions == expected_positions
+            assert button.get_current_image_positions() == expected_positions
+            
+            # Other states should fall back to normal positions
+            assert button.hovered_image_positions == expected_positions
+            assert button.selected_image_positions == expected_positions
+            assert button.disabled_image_positions == expected_positions
+            
+            # Test state changes
+            button.hovered = True
+            assert button.get_current_image_positions() == expected_positions
+            button.hovered = False
+            
+            button.select()
+            assert button.get_current_image_positions() == expected_positions
+            button.unselect()
+            
+            button.disable()
+            assert button.get_current_image_positions() == expected_positions
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_mixed_with_without_positions(self, _init_pygame, _display_surface_return_none):
+        """Test handling of mixed images where some have positions and others don't."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with mixed position specifications
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_images": [
+                        {
+                            "id": "positioned",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.1, 0.1]
+                        },
+                        {
+                            "id": "not_positioned",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png"
+                            # No position - should default to (0.5, 0.5)
+                        },
+                        {
+                            "id": "also_positioned",
+                            "layer": 2,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.9, 0.9]
+                        }
+                    ]
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            button = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                            text="Mixed Position Test",
+                            manager=manager)
+            
+            # Should have positions: specified, default, specified
+            expected_positions = [(0.1, 0.1), (0.5, 0.5), (0.9, 0.9)]
+            assert button.normal_image_positions == expected_positions
+            assert button.get_current_image_positions() == expected_positions
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_api_methods(self, _init_pygame, _display_surface_return_none):
+        """Test the new API methods for accessing image positions."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with different positions for different states
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_images": [
+                        {
+                            "id": "normal_bg",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.2, 0.2]
+                        }
+                    ],
+                    "hovered_images": [
+                        {
+                            "id": "hovered_bg",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.4, 0.4]
+                        }
+                    ],
+                    "selected_images": [
+                        {
+                            "id": "selected_bg",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.6, 0.6]
+                        }
+                    ],
+                    "disabled_images": [
+                        {
+                            "id": "disabled_bg",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.8, 0.8]
+                        }
+                    ]
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            button = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                            text="API Test",
+                            manager=manager)
+            
+            # Test get_current_image_positions() for different states
+            
+            # Normal state
+            assert button.get_current_image_positions() == [(0.2, 0.2)]
+            
+            # Hovered state
+            button.hovered = True
+            assert button.get_current_image_positions() == [(0.4, 0.4)]
+            button.hovered = False
+            
+            # Selected state
+            button.select()
+            assert button.get_current_image_positions() == [(0.6, 0.6)]
+            button.unselect()
+            
+            # Disabled state
+            button.disable()
+            assert button.get_current_image_positions() == [(0.8, 0.8)]
+            button.enable()
+            
+            # Test that positions correspond to images
+            assert len(button.get_current_image_positions()) == len(button.get_current_images())
+            
+            # Test state-specific position attributes
+            assert button.normal_image_positions == [(0.2, 0.2)]
+            assert button.hovered_image_positions == [(0.4, 0.4)]
+            assert button.selected_image_positions == [(0.6, 0.6)]
+            assert button.disabled_image_positions == [(0.8, 0.8)]
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_theme_loading_and_storage(self, _init_pygame, _display_surface_return_none):
+        """Test that position data is correctly loaded and stored from theme files."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with positioned single images
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.0, 0.0]  # Top-left
+                    },
+                    "hovered_image": {
+                        "path": "tests/data/images/splat.png", 
+                        "position": [1.0, 1.0]  # Bottom-right
+                    },
+                    "selected_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.5, 0.5]  # Center
+                    },
+                    "disabled_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.2, 0.8]  # Custom position
+                    }
+                }
+            }
+        }
+
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+
+        try:
+            manager = pygame_gui.UIManager((800, 600), theme_file)
+            
+            # Test that position data is correctly loaded in theme
+            normal_details = manager.ui_theme.get_image_details("normal_image", ["button"])
+            assert normal_details[0]["position"] == (0.0, 0.0)
+            
+            hovered_details = manager.ui_theme.get_image_details("hovered_image", ["button"])
+            assert hovered_details[0]["position"] == (1.0, 1.0)
+            
+            selected_details = manager.ui_theme.get_image_details("selected_image", ["button"])
+            assert selected_details[0]["position"] == (0.5, 0.5)
+            
+            disabled_details = manager.ui_theme.get_image_details("disabled_image", ["button"])
+            assert disabled_details[0]["position"] == (0.2, 0.8)
+            
+            # Test button creation and position loading
+            button = pygame_gui.elements.UIButton(
+                relative_rect=pygame.Rect(100, 100, 150, 30),
+                text="Position Test",
+                manager=manager
+            )
+            
+            # Verify positions are correctly loaded into button
+            assert button.normal_image_positions == [(0.0, 0.0)]
+            assert button.hovered_image_positions == [(1.0, 1.0)]
+            assert button.selected_image_positions == [(0.5, 0.5)]
+            assert button.disabled_image_positions == [(0.2, 0.8)]
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_state_transitions(self, _init_pygame, _display_surface_return_none):
+        """Test that position data changes correctly with button state transitions."""
+        import tempfile
+        import json
+        import os
+        
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.1, 0.1]
+                    },
+                    "hovered_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.2, 0.2]
+                    },
+                    "selected_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.3, 0.3]
+                    },
+                    "disabled_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.4, 0.4]
+                    }
+                }
+            }
+        }
+
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+
+        try:
+            manager = pygame_gui.UIManager((800, 600), theme_file)
+            button = pygame_gui.elements.UIButton(
+                relative_rect=pygame.Rect(100, 100, 150, 30),
+                text="State Test",
+                manager=manager
+            )
+            
+            # Test normal state
+            assert button.get_current_image_positions() == [(0.1, 0.1)]
+            
+            # Test hovered state
+            button.hovered = True
+            assert button.get_current_image_positions() == [(0.2, 0.2)]
+            
+            # Test selected state (takes priority over hovered)
+            button.is_selected = True
+            assert button.get_current_image_positions() == [(0.3, 0.3)]
+            
+            # Test disabled state (takes priority over all)
+            button.is_enabled = False
+            assert button.get_current_image_positions() == [(0.4, 0.4)]
+            
+            # Reset to normal
+            button.is_enabled = True
+            button.is_selected = False
+            button.hovered = False
+            assert button.get_current_image_positions() == [(0.1, 0.1)]
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_multi_image_format(self, _init_pygame, _display_surface_return_none):
+        """Test image positioning with multi-image format."""
+        import tempfile
+        import json
+        import os
+        
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_images": [
+                        {
+                            "id": "bg",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.0, 0.0]
+                        },
+                        {
+                            "id": "icon",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [1.0, 1.0]
+                        }
+                    ]
+                }
+            }
+        }
+
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+
+        try:
+            manager = pygame_gui.UIManager((800, 600), theme_file)
+            button = pygame_gui.elements.UIButton(
+                relative_rect=pygame.Rect(100, 100, 150, 30),
+                text="Multi Test",
+                manager=manager
+            )
+            
+            # Test multi-image positions
+            positions = button.get_current_image_positions()
+            assert len(positions) == 2
+            assert positions == [(0.0, 0.0), (1.0, 1.0)]
+            
+            # Test that we have multiple images
+            assert button.is_multi_image_mode() == True
+            assert button.get_image_count() == 2
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_fallback_behavior(self, _init_pygame, _display_surface_return_none):
+        """Test fallback behavior when some states don't have specific images."""
+        import tempfile
+        import json
+        import os
+        
+        # Only define normal image, others should fall back
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.25, 0.75]
+                    }
+                }
+            }
+        }
+
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+
+        try:
+            manager = pygame_gui.UIManager((800, 600), theme_file)
+            button = pygame_gui.elements.UIButton(
+                relative_rect=pygame.Rect(100, 100, 150, 30),
+                text="Fallback Test",
+                manager=manager
+            )
+            
+            # All states should fall back to normal image position
+            assert button.normal_image_positions == [(0.25, 0.75)]
+            assert button.hovered_image_positions == [(0.25, 0.75)]
+            assert button.selected_image_positions == [(0.25, 0.75)]
+            assert button.disabled_image_positions == [(0.25, 0.75)]
+            
+            # Current positions should match normal regardless of state
+            assert button.get_current_image_positions() == [(0.25, 0.75)]
+            
+            button.hovered = True
+            assert button.get_current_image_positions() == [(0.25, 0.75)]
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_default_center_position(self, _init_pygame, _display_surface_return_none):
+        """Test that images without position specified default to center (0.5, 0.5)."""
+        import tempfile
+        import json
+        import os
+        
+        # Image without position specified
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_image": {
+                        "path": "tests/data/images/splat.png"
+                        # No position specified
+                    }
+                }
+            }
+        }
+
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+
+        try:
+            manager = pygame_gui.UIManager((800, 600), theme_file)
+            button = pygame_gui.elements.UIButton(
+                relative_rect=pygame.Rect(100, 100, 150, 30),
+                text="Default Test",
+                manager=manager
+            )
+            
+            # Should default to center position
+            assert button.normal_image_positions == [(0.5, 0.5)]
+            assert button.get_current_image_positions() == [(0.5, 0.5)]
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_edge_case_values(self, _init_pygame, _display_surface_return_none):
+        """Test edge case position values (0.0, 1.0, boundaries)."""
+        import tempfile
+        import json
+        import os
+        
+        theme_data = {
+            "button": {
+                "images": {
+                    "normal_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.0, 1.0]  # Top-left x, bottom y
+                    },
+                    "hovered_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [1.0, 0.0]  # Bottom-right x, top y
+                    }
+                }
+            }
+        }
+
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+
+        try:
+            manager = pygame_gui.UIManager((800, 600), theme_file)
+            button = pygame_gui.elements.UIButton(
+                relative_rect=pygame.Rect(100, 100, 150, 30),
+                text="Edge Test",
+                manager=manager
+            )
+            
+            # Test boundary values
+            assert button.normal_image_positions == [(0.0, 1.0)]
+            assert button.hovered_image_positions == [(1.0, 0.0)]
+            
+            # Test state transitions with edge values
+            assert button.get_current_image_positions() == [(0.0, 1.0)]
+            
+            button.hovered = True
+            assert button.get_current_image_positions() == [(1.0, 0.0)]
+            
+        finally:
+            os.unlink(theme_file)
+
 
 if __name__ == '__main__':
     pytest.console_main()
diff --git a/tests/test_elements/test_ui_panel.py b/tests/test_elements/test_ui_panel.py
index afe4b66..7dc42fe 100644
--- a/tests/test_elements/test_ui_panel.py
+++ b/tests/test_elements/test_ui_panel.py
@@ -943,6 +943,322 @@ class TestUIPanel:
             except:
                 pass
 
+    def test_image_positioning_single_image_mode(self, _init_pygame, _display_surface_return_none):
+        """Test image positioning in single-image mode."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with positioned single image
+        theme_data = {
+            "panel": {
+                "images": {
+                    "background_image": {
+                        "path": "tests/data/images/splat.png",
+                        "position": [0.0, 0.0]  # Top-left corner
+                    }
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            panel = UIPanel(relative_rect=pygame.Rect(100, 100, 200, 200), manager=manager)
+            
+            # Test position
+            assert panel.background_image_positions == [(0.0, 0.0)]
+            assert panel.get_current_image_positions() == [(0.0, 0.0)]
+            
+            # Test API methods
+            assert not panel.is_multi_image_mode()
+            assert panel.get_image_count() == 1
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_multi_image_mode(self, _init_pygame, _display_surface_return_none):
+        """Test image positioning in multi-image mode."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme with positioned multi-images
+        theme_data = {
+            "panel": {
+                "images": {
+                    "background_images": [
+                        {
+                            "id": "background",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.5, 0.5]  # Center
+                        },
+                        {
+                            "id": "top_left",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.0, 0.0]  # Top-left corner
+                        },
+                        {
+                            "id": "bottom_right",
+                            "layer": 2,
+                            "path": "tests/data/images/splat.png",
+                            "position": [1.0, 1.0]  # Bottom-right corner
+                        }
+                    ]
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            panel = UIPanel(relative_rect=pygame.Rect(100, 100, 200, 200), manager=manager)
+            
+            # Test positions
+            expected_positions = [(0.5, 0.5), (0.0, 0.0), (1.0, 1.0)]
+            assert panel.background_image_positions == expected_positions
+            assert panel.get_current_image_positions() == expected_positions
+            
+            # Test API methods
+            assert panel.is_multi_image_mode()
+            assert panel.get_image_count() == 3
+            
+            # Test image access
+            current_images = panel.get_current_images()
+            assert len(current_images) == 3
+            for image in current_images:
+                assert isinstance(image, pygame.Surface)
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_default_values(self, _init_pygame, _display_surface_return_none):
+        """Test that images without position parameters default to center (0.5, 0.5)."""
+        import tempfile
+        import json
+        import os
+        
+        # Theme without position parameters
+        theme_data = {
+            "panel": {
+                "images": {
+                    "background_images": [
+                        {
+                            "id": "no_position",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png"
+                            # No position parameter - should default to (0.5, 0.5)
+                        },
+                        {
+                            "id": "with_position",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.2, 0.8]
+                        }
+                    ]
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            panel = UIPanel(relative_rect=pygame.Rect(100, 100, 200, 200), manager=manager)
+            
+            # First image should default to center, second should use specified position
+            expected_positions = [(0.5, 0.5), (0.2, 0.8)]
+            assert panel.background_image_positions == expected_positions
+            assert panel.get_current_image_positions() == expected_positions
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_api_methods(self, _init_pygame, _display_surface_return_none):
+        """Test the API methods for image positioning."""
+        manager = UIManager((800, 600))
+        panel = UIPanel(relative_rect=pygame.Rect(100, 100, 200, 200), manager=manager)
+        
+        # Create test images first
+        panel.background_images = [pygame.Surface((10, 10)) for _ in range(2)]
+        
+        # Test setting positions
+        test_positions = [(0.1, 0.1), (0.9, 0.9)]
+        panel.set_image_positions(test_positions)
+        assert panel.background_image_positions == test_positions
+        
+        # Test setting individual position
+        panel.set_image_position(0, (0.3, 0.3))
+        assert panel.background_image_positions[0] == (0.3, 0.3)
+        
+        # Test invalid index
+        assert not panel.set_image_position(99, (0.5, 0.5))
+        
+        # Test auto-extension of positions list
+        panel.background_images = [pygame.Surface((10, 10)) for _ in range(3)]
+        panel.set_image_positions([(0.1, 0.1)])
+        assert len(panel.background_image_positions) == 3
+        assert panel.background_image_positions[0] == (0.1, 0.1)
+        assert panel.background_image_positions[1] == (0.5, 0.5)  # Default center position
+        assert panel.background_image_positions[2] == (0.5, 0.5)  # Default center position
+
+    def test_image_positioning_mixed_with_without_positions(self, _init_pygame, _display_surface_return_none):
+        """Test handling of mixed images where some have positions and others don't."""
+        import tempfile
+        import json
+        import os
+        
+        theme_data = {
+            "panel": {
+                "images": {
+                    "background_images": [
+                        {
+                            "id": "with_position",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.1, 0.1]
+                        },
+                        {
+                            "id": "no_position",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png"
+                            # No position - should default to center
+                        },
+                        {
+                            "id": "with_position_2",
+                            "layer": 2,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.9, 0.9]
+                        }
+                    ]
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_data, f)
+            theme_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_file)
+            panel = UIPanel(relative_rect=pygame.Rect(100, 100, 200, 200), manager=manager)
+            
+            # Should have positions: specified, default, specified
+            expected_positions = [(0.1, 0.1), (0.5, 0.5), (0.9, 0.9)]
+            assert panel.background_image_positions == expected_positions
+            assert panel.get_current_image_positions() == expected_positions
+            
+        finally:
+            os.unlink(theme_file)
+
+    def test_image_positioning_theme_loading_and_storage(self, _init_pygame, _display_surface_return_none):
+        """Test that image positions are properly loaded and stored when switching themes."""
+        import tempfile
+        import json
+        import os
+        
+        # First theme with positions
+        theme_1 = {
+            "panel": {
+                "images": {
+                    "background_images": [
+                        {
+                            "id": "bg1",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.1, 0.1]
+                        },
+                        {
+                            "id": "bg2",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.9, 0.9]
+                        }
+                    ]
+                }
+            }
+        }
+        
+        # Second theme with different positions
+        theme_2 = {
+            "panel": {
+                "images": {
+                    "background_images": [
+                        {
+                            "id": "bg1",
+                            "layer": 0,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.2, 0.2]
+                        },
+                        {
+                            "id": "bg2",
+                            "layer": 1,
+                            "path": "tests/data/images/splat.png",
+                            "position": [0.8, 0.8]
+                        }
+                    ]
+                }
+            }
+        }
+        
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_1, f)
+            theme_1_file = f.name
+            
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(theme_2, f)
+            theme_2_file = f.name
+        
+        try:
+            manager = UIManager((800, 600), theme_1_file)
+            panel = UIPanel(relative_rect=pygame.Rect(100, 100, 200, 200), manager=manager)
+            
+            # Check first theme positions
+            assert panel.background_image_positions == [(0.1, 0.1), (0.9, 0.9)]
+            
+            # Switch to second theme
+            manager.ui_theme.load_theme(theme_2_file)
+            panel.rebuild_from_changed_theme_data()
+            
+            # Check second theme positions
+            assert panel.background_image_positions == [(0.2, 0.2), (0.8, 0.8)]
+            
+        finally:
+            os.unlink(theme_1_file)
+            os.unlink(theme_2_file)
+
+    def test_image_positioning_edge_case_values(self, _init_pygame, _display_surface_return_none):
+        """Test image positioning with edge case values."""
+        manager = UIManager((800, 600))
+        panel = UIPanel(relative_rect=pygame.Rect(100, 100, 200, 200), manager=manager)
+        
+        # Test with empty positions list
+        panel.set_image_positions([])
+        assert panel.background_image_positions == []
+        
+        # Test with single image but multiple positions
+        panel.background_images = [pygame.Surface((10, 10))]
+        panel.set_image_positions([(0.1, 0.1), (0.2, 0.2)])
+        assert panel.background_image_positions == [(0.1, 0.1)]
+        
+        # Test with multiple images but single position
+        panel.background_images = [pygame.Surface((10, 10)) for _ in range(3)]
+        panel.set_image_positions([(0.3, 0.3)])
+        assert len(panel.background_image_positions) == 3
+        assert panel.background_image_positions[0] == (0.3, 0.3)
+        assert panel.background_image_positions[1] == (0.5, 0.5)  # Default center
+        assert panel.background_image_positions[2] == (0.5, 0.5)  # Default center
+
 
 if __name__ == '__main__':
     pytest.console_main()