"""
Unified code generation and image embedding core.
This module provides the core functionality for generating embedded Python code
and processing image folders, used by both CLI and GUI interfaces.
"""
import base64
import os
from io import BytesIO
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
from PIL import Image
from .io_utils import image_to_base64
def process_image_folder(
folder_path: Union[str, Path],
compression_quality: int = 85,
valid_extensions: Optional[Tuple[str, ...]] = None,
) -> Dict[str, Dict[str, str]]:
"""
Process all valid images in a folder and organize them by theme.
Args:
folder_path: Path to the folder containing images
compression_quality: JPEG/WebP quality (1-100)
valid_extensions: Tuple of valid file extensions
Returns:
Dictionary mapping theme names to {image_key: base64_data}
Raises:
FileNotFoundError: If folder doesn't exist
ValueError: If compression_quality is invalid
"""
if not 1 <= compression_quality <= 100:
raise ValueError("Compression quality must be between 1 and 100")
if valid_extensions is None:
valid_extensions = (
".gif",
".png",
".ico",
".jpg",
".jpeg",
".bmp",
".tiff",
".webp",
)
folder_path = Path(folder_path)
if not folder_path.exists():
raise FileNotFoundError(f"Folder not found: {folder_path}")
images_dict: Dict[str, Dict[str, str]] = {}
def store_image(theme: str, key: str, encoded: str) -> None:
"""Store an image under the given theme and key."""
if theme not in images_dict:
images_dict[theme] = {}
images_dict[theme][key] = encoded
# Process all files in the folder
for file_path in folder_path.iterdir():
if not file_path.is_file():
continue
file_ext = file_path.suffix.lower()
if file_ext not in valid_extensions:
continue
try:
# Load and process the image
image = Image.open(file_path)
# Determine format for compression
if file_ext in (".jpg", ".jpeg"):
format_name = "JPEG"
# Convert RGBA to RGB for JPEG
if image.mode == "RGBA":
background = Image.new("RGB", image.size, (255, 255, 255))
background.paste(
image, mask=image.split()[-1] if image.mode == "RGBA" else None
)
image = background
elif file_ext == ".webp":
format_name = "WebP"
else:
format_name = "PNG"
compression_quality = None # PNG doesn't use quality parameter
# Convert to base64
encoded = image_to_base64(image, format_name, quality=compression_quality)
# Determine theme and key from filename
filename_stem = file_path.stem
if "_" in filename_stem:
theme, key = filename_stem.split("_", 1)
else:
theme = "default"
key = filename_stem
store_image(theme, key, encoded)
except Exception as e:
print(f"Warning: Failed to process {file_path}: {e}")
continue
return images_dict
def generate_embedded_code(
images_dict: Dict[str, Dict[str, str]],
framework: str = "tkinter",
usage: str = "general",
include_examples: bool = True,
) -> str:
"""
Generate Python code with embedded images.
Args:
images_dict: Dictionary of {theme: {key: base64_data}}
framework: Target framework ("tkinter" or "customtkinter")
usage: Usage type ("icons", "buttons", "backgrounds", "general")
include_examples: Whether to include usage examples
Returns:
Generated Python code as string
"""
code_lines = [
f"# Generated {framework} code for {usage}",
"# Generated by GUI Image Studio",
"",
"import base64",
"from io import BytesIO",
"from PIL import Image, ImageTk",
"",
]
# Add framework-specific imports
if framework == "tkinter":
code_lines.append("import tkinter as tk")
elif framework == "customtkinter":
code_lines.extend(
[
"import customtkinter as ctk",
"import tkinter as tk",
]
)
code_lines.extend(
[
"",
"# Embedded image data",
"EMBEDDED_IMAGES = {",
]
)
# Add the images dictionary
for theme, images in images_dict.items():
code_lines.append(f' "{theme}": {{')
for key, data in images.items():
# Split long base64 strings for readability
if len(data) > 80:
code_lines.append(f' "{key}": (')
for i in range(0, len(data), 80):
chunk = data[i : i + 80]
is_last = i + 80 >= len(data)
code_lines.append(f' "{chunk}"{"" if is_last else " +"}')
code_lines.append(" ),")
else:
code_lines.append(f' "{key}": "{data}",')
code_lines.append(" },")
code_lines.extend(
[
"}",
"",
"def load_image(name, theme='default', size=None):",
' """Load an embedded image by name and optional theme."""',
" if theme not in EMBEDDED_IMAGES:",
" raise ValueError(f'Theme {theme} not found')",
" if name not in EMBEDDED_IMAGES[theme]:",
" raise ValueError(f'Image {name} not found in theme {theme}')",
" ",
" # Decode base64 data",
" image_data = EMBEDDED_IMAGES[theme][name]",
" if isinstance(image_data, tuple):",
" image_data = ''.join(image_data)",
" ",
" decoded = base64.b64decode(image_data)",
" image = Image.open(BytesIO(decoded))",
" ",
" # Resize if requested",
" if size:",
" image = image.resize(size, Image.Resampling.LANCZOS)",
" ",
" return ImageTk.PhotoImage(image)",
"",
]
)
# Add usage examples if requested
if include_examples:
code_lines.extend(_generate_usage_examples(framework, usage, images_dict))
return "\n".join(code_lines)
def _generate_usage_examples(
framework: str, usage: str, images_dict: Dict[str, Dict[str, str]]
) -> List[str]:
"""Generate usage examples based on framework and usage type."""
examples = [
"# Usage Examples",
"if __name__ == '__main__':",
]
# Get a sample image name for examples
sample_image = None
for theme, images in images_dict.items():
if images:
sample_image = list(images.keys())[0]
sample_theme = theme
break
if not sample_image:
return examples + [" # No images available for examples", ""]
if framework == "tkinter":
examples.extend(
[
" root = tk.Tk()",
" root.title('Image Demo')",
"",
]
)
if usage == "buttons":
examples.extend(
[
" # Button with image",
f" image = load_image('{sample_image}', '{sample_theme}', size=(32, 32))",
" button = tk.Button(root, image=image, text='Click me')",
" button.pack(pady=10)",
]
)
elif usage == "icons":
examples.extend(
[
" # Icon usage",
f" icon = load_image('{sample_image}', '{sample_theme}', size=(16, 16))",
" label = tk.Label(root, image=icon, text=' Icon Label', compound='left')",
" label.pack(pady=10)",
]
)
elif usage == "backgrounds":
examples.extend(
[
" # Background image",
f" bg_image = load_image('{sample_image}', '{sample_theme}')",
" canvas = tk.Canvas(root, width=400, height=300)",
" canvas.create_image(200, 150, image=bg_image)",
" canvas.pack()",
]
)
else: # general
examples.extend(
[
" # General image usage",
f" image = load_image('{sample_image}', '{sample_theme}', size=(100, 100))",
" label = tk.Label(root, image=image)",
" label.pack(pady=20)",
]
)
elif framework == "customtkinter":
examples.extend(
[
" ctk.set_appearance_mode('dark')",
" root = ctk.CTk()",
" root.title('Image Demo')",
"",
]
)
if usage == "buttons":
examples.extend(
[
" # CustomTkinter button with image",
f" image = load_image('{sample_image}', '{sample_theme}', size=(32, 32))",
" button = ctk.CTkButton(root, image=image, text='Click me')",
" button.pack(pady=10)",
]
)
else:
examples.extend(
[
" # CustomTkinter label with image",
f" image = load_image('{sample_image}', '{sample_theme}', size=(100, 100))",
" label = ctk.CTkLabel(root, image=image, text='')",
" label.pack(pady=20)",
]
)
examples.extend(
[
"",
" root.mainloop()",
"",
]
)
return examples
[docs]
def embed_images_from_folder(
folder_path: Union[str, Path],
output_file: Union[str, Path] = "embedded_images.py",
compression_quality: int = 85,
framework: str = "tkinter",
usage: str = "general",
include_examples: bool = True,
) -> None:
"""
Complete pipeline: process folder and generate embedded code file.
Args:
folder_path: Path to folder containing images
output_file: Output Python file path
compression_quality: Image compression quality (1-100)
framework: Target framework ("tkinter" or "customtkinter")
usage: Usage type ("icons", "buttons", "backgrounds", "general")
include_examples: Whether to include usage examples
Raises:
FileNotFoundError: If input folder doesn't exist
IOError: If output file cannot be written
ValueError: If parameters are invalid
"""
# Process the image folder
images_dict = process_image_folder(folder_path, compression_quality)
if not images_dict:
raise ValueError(f"No valid images found in {folder_path}")
# Generate the code
code = generate_embedded_code(images_dict, framework, usage, include_examples)
# Write to output file
output_path = Path(output_file)
try:
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write(code)
except Exception as e:
raise IOError(f"Failed to write output file {output_path}: {e}")
def get_available_themes(images_dict: Dict[str, Dict[str, str]]) -> List[str]:
"""Get list of available themes from images dictionary."""
return list(images_dict.keys())
def get_images_in_theme(
images_dict: Dict[str, Dict[str, str]], theme: str
) -> List[str]:
"""Get list of image names in a specific theme."""
return list(images_dict.get(theme, {}).keys())
def validate_compression_quality(quality: int) -> int:
"""Validate and clamp compression quality to valid range."""
if not isinstance(quality, int):
raise ValueError("Compression quality must be an integer")
return max(1, min(100, quality))