import base64
from dataclasses import dataclass
from io import BytesIO
from typing import Optional, Tuple
from PIL import Image, ImageEnhance, ImageOps, ImageSequence
# Try to import embedded_images, create empty dict if not found
try:
from . import embedded_images
except ImportError:
# Create a fallback embedded_images module
class EmbeddedImages:
embedded_images = {"default": {}}
embedded_images = EmbeddedImages()
[docs]
@dataclass
class ImageConfig:
"""Configuration class for image processing parameters."""
image_name: str
framework: str = "tkinter"
size: Tuple[int, int] = (32, 32)
theme: str = "default"
grayscale: bool = False
rotate: int = 0
transparency: float = 1.0
format_override: Optional[str] = None
animated: bool = False
frame_delay: int = 100
tint_color: Optional[Tuple[int, int, int]] = None
tint_intensity: float = 0.0
contrast: float = 1.0
saturation: float = 1.0
def _get_image_data(image_name, theme):
"""
Retrieve and decode image data from embedded images.
Args:
image_name (str): Name of the image file.
theme (str): Theme name.
Returns:
PIL.Image: Opened PIL Image object.
Raises:
ValueError: If image is not found in the specified theme.
"""
theme_dict = embedded_images.embedded_images.get(
theme
) or embedded_images.embedded_images.get("default")
if not theme_dict or image_name not in theme_dict:
raise ValueError(f"Image '{image_name}' not found in theme '{theme}'.")
image_data = base64.b64decode(theme_dict[image_name])
stream = BytesIO(image_data)
return Image.open(stream)
def _apply_image_transformations(img, **transforms):
"""
Apply various transformations to a PIL Image.
Args:
img (PIL.Image): The image to transform.
**transforms: Transformation parameters.
Returns:
PIL.Image: The transformed image.
"""
# Extract transformation parameters
grayscale = transforms.get("grayscale", False)
rotate = transforms.get("rotate", 0)
transparency = transforms.get("transparency", 1.0)
size = transforms.get("size")
contrast = transforms.get("contrast", 1.0)
saturation = transforms.get("saturation", 1.0)
tint_color = transforms.get("tint_color")
tint_intensity = transforms.get("tint_intensity", 0.0)
format_override = transforms.get("format_override")
# Apply transformations in sequence
if grayscale:
img = ImageOps.grayscale(img).convert("RGBA")
if rotate:
img = img.rotate(rotate, expand=True)
if transparency < 1.0:
img = ImageEnhance.Brightness(img).enhance(transparency)
if size:
img = img.resize(size, Image.LANCZOS)
if contrast != 1.0:
img = ImageEnhance.Contrast(img).enhance(contrast)
if saturation != 1.0:
img = ImageEnhance.Color(img).enhance(saturation)
if tint_color is not None and tint_intensity > 0.0:
if img.mode != "RGBA":
img = img.convert("RGBA")
overlay = Image.new("RGBA", img.size, tint_color + (255,))
img = Image.blend(img, overlay, tint_intensity)
if format_override:
buffer = BytesIO()
img.save(buffer, format=format_override)
img = Image.open(BytesIO(buffer.getvalue()))
return img
def _create_framework_image(img, framework, size):
"""
Convert PIL Image to framework-specific image object.
Args:
img (PIL.Image): The PIL image to convert.
framework (str): Target framework ("tkinter" or "customtkinter").
size (tuple): Image size for CustomTkinter.
Returns:
Framework-specific image object.
"""
if framework.lower() == "customtkinter":
import customtkinter as ctk
return ctk.CTkImage(light_image=img, size=size)
else:
from PIL import ImageTk
return ImageTk.PhotoImage(img)
def _is_animated_gif(img, animated):
"""
Check if image is an animated GIF that should be processed as animated.
Args:
img (PIL.Image): The image to check.
animated (bool): Whether animated processing is requested.
Returns:
bool: True if image should be processed as animated GIF.
"""
return (
animated
and img.format == "GIF"
and getattr(img, "is_animated", False)
and getattr(img, "n_frames", 1) > 1
)
def _process_animated_gif(img, framework, size, frame_delay, **transforms):
"""
Process an animated GIF by applying transformations to each frame.
Args:
img (PIL.Image): The animated GIF image.
framework (str): Target framework.
size (tuple): Frame size.
frame_delay (int): Delay between frames.
**transforms: Transformation parameters.
Returns:
dict: Dictionary with "animated_frames" and "frame_delay" keys.
Raises:
RuntimeError: If frame processing fails.
"""
frames = []
try:
for frame in ImageSequence.Iterator(img):
frame = frame.convert("RGBA")
frame = _apply_image_transformations(frame, size=size, **transforms)
frame_obj = _create_framework_image(frame, framework, size)
frames.append(frame_obj)
except Exception as e:
raise RuntimeError(f"Error processing animated GIF frames: {e}")
return {"animated_frames": frames, "frame_delay": frame_delay}
def _process_image_with_config(config: ImageConfig):
"""
Process an image based on the provided configuration.
Args:
config (ImageConfig): Image processing configuration.
Returns:
Framework-specific image object or animated frames dictionary.
"""
# Get the base image
img = _get_image_data(config.image_name, config.theme)
# Get transformation parameters
transforms = config.to_transforms_dict()
# Handle animated GIFs
if _is_animated_gif(img, config.animated):
return _process_animated_gif(
img, config.framework, config.size, config.frame_delay, **transforms
)
# Process static image
img = _apply_image_transformations(img, **transforms)
return _create_framework_image(img, config.framework, config.size)
[docs]
def get_image(
image_name,
framework="tkinter",
size=(32, 32),
theme="default",
grayscale=False,
rotate=0,
transparency=1.0,
format_override=None,
animated=False,
frame_delay=100,
tint_color=None,
tint_intensity=0.0,
contrast=1.0,
saturation=1.0,
):
"""
Retrieve an embedded image with dynamic transformations and optional animated
GIF support.
Args:
image_name (str): Name of the image file (e.g., 'icon.png').
framework (str): "tkinter" or "customtkinter".
size (tuple): Desired dimensions; used for resizing. For animated GIFs,
each frame is resized.
theme (str): Theme name (e.g., "dark", "light"); falls back to "default"
if not matched.
grayscale (bool): Convert image to grayscale.
rotate (int): Rotate image (or each frame) by the given degrees.
transparency (float): Adjust brightness/opacity (0.0 to 1.0).
format_override (str): Convert image to this format on the fly ("PNG",
"JPEG", etc.).
animated (bool): If True and image is an animated GIF, process all its frames.
frame_delay (int): Delay (milliseconds) between frames for animated GIFs.
tint_color (tuple or None): A tuple (R, G, B) for a tint overlay.
tint_intensity (float): Blending factor (0.0 to 1.0) for tint; 0 means no tint.
contrast (float): Contrast adjustment factor (1.0 means no change).
saturation (float): Saturation adjustment factor (1.0 means no change).
Returns:
For static images:
- A Tkinter PhotoImage or a CustomTkinter CTkImage.
For animated GIFs:
- A dictionary with keys "animated_frames" (a list of image objects) and
"frame_delay".
Use these frames in an animation loop.
"""
config = ImageConfig(
image_name=image_name,
framework=framework,
size=size,
theme=theme,
grayscale=grayscale,
rotate=rotate,
transparency=transparency,
format_override=format_override,
animated=animated,
frame_delay=frame_delay,
tint_color=tint_color,
tint_intensity=tint_intensity,
contrast=contrast,
saturation=saturation,
)
return _process_image_with_config(config)
[docs]
def get_image_from_config(config: ImageConfig):
"""
Retrieve an embedded image using an ImageConfig object.
This is a convenience function for when you have complex configurations
or want to reuse the same configuration multiple times.
Args:
config (ImageConfig): Complete image processing configuration.
Returns:
Same as get_image() - framework-specific image object or animated frames dictionary.
"""
return _process_image_with_config(config)