Source code for gui_image_studio.cli.commands

#!/usr/bin/env python3
"""
Enhanced command-line interface for gui_image_studio package.

This module provides CLI commands that use the unified image processing core,
ensuring consistency between CLI and GUI operations.
"""

import argparse
import sys
from pathlib import Path
from typing import Optional, Tuple

from .. import __version__
from ..core.code_generation import embed_images_from_folder
from ..core.io_utils import load_image, save_image
from ..core.sample_creation import (
    create_sample_image_set,
    create_sample_images_legacy_compatible,
)
from ..image_studio.toolkit import effects  # This triggers effect registration

# Use the modern toolkit effects system
from ..image_studio.toolkit.effects.base_effect import EffectRegistry


def apply_toolkit_transformations(image, **transforms):
    """Apply transformations using the toolkit effects system."""
    from PIL import Image

    result = image

    # Map CLI parameters to toolkit effects
    effect_mappings = [
        ("grayscale", "grayscale", {}),
        ("rotate", "rotate", {"angle": transforms.get("rotate", 0)}),
        ("contrast", "contrast", {"factor": transforms.get("contrast", 1.0)}),
        ("saturation", "saturation", {"factor": transforms.get("saturation", 1.0)}),
        ("brightness", "brightness", {"factor": transforms.get("brightness", 1.0)}),
        ("blur_radius", "blur", {"radius": transforms.get("blur_radius", 0.0)}),
    ]

    # Apply each transformation if specified
    for param_name, effect_name, effect_params in effect_mappings:
        if param_name in transforms and transforms[param_name] != (
            1.0 if "factor" in effect_params else 0
        ):
            effect = EffectRegistry.get_effect(effect_name)
            if effect:
                # Update effect params with actual values
                for param_key, param_value in effect_params.items():
                    if param_value == transforms.get(param_name):
                        effect_params[param_key] = transforms[param_name]

                result = effect.apply_with_validation(result, **effect_params)

    # Handle special cases that don't map directly to toolkit effects

    # Handle resize (not a toolkit effect, use PIL directly)
    if "size" in transforms:
        result = result.resize(transforms["size"], Image.Resampling.LANCZOS)

    # Handle transparency (not a toolkit effect, use PIL directly)
    if "transparency" in transforms and transforms["transparency"] != 1.0:
        from PIL import ImageEnhance

        enhancer = ImageEnhance.Brightness(result)
        result = enhancer.enhance(transforms["transparency"])

    # Handle tint (not a toolkit effect, use PIL directly)
    if "tint_color" in transforms and "tint_intensity" in transforms:
        tint_color = transforms["tint_color"]
        intensity = transforms["tint_intensity"]
        if intensity > 0.0:
            if result.mode != "RGBA":
                result = result.convert("RGBA")
            overlay = Image.new("RGBA", result.size, tint_color + (255,))
            result = Image.blend(result, overlay, intensity)

    # Handle format conversion (not a toolkit effect, use PIL directly)
    if "format_override" in transforms and transforms["format_override"]:
        from io import BytesIO

        target_format = transforms["format_override"]
        buffer = BytesIO()

        if target_format.upper() == "JPEG":
            if result.mode in ("RGBA", "LA"):
                background = Image.new("RGB", result.size, (255, 255, 255))
                background.paste(
                    result, mask=result.split()[-1] if result.mode == "RGBA" else None
                )
                result = background

        result.save(buffer, format=target_format)
        buffer.seek(0)
        result = Image.open(buffer)

    return result


[docs] def image_processor() -> None: """ Console script entry point for processing images with transformations. This is a new CLI command that demonstrates the unified image processing core. """ parser = argparse.ArgumentParser( description="Process images with various transformations using the unified core", prog="gui-image-studio-process", ) # Input/Output arguments parser.add_argument("--input", "-i", required=True, help="Input image file path") parser.add_argument("--output", "-o", required=True, help="Output image file path") # Transformation arguments parser.add_argument( "--resize", nargs=2, type=int, metavar=("WIDTH", "HEIGHT"), help="Resize image to WIDTH HEIGHT", ) parser.add_argument( "--rotate", type=float, default=0.0, help="Rotate image by degrees (positive = clockwise)", ) parser.add_argument( "--grayscale", action="store_true", help="Convert image to grayscale" ) parser.add_argument( "--blur", type=float, default=0.0, help="Apply Gaussian blur with given radius" ) parser.add_argument( "--contrast", type=float, default=1.0, help="Adjust contrast (1.0 = no change, >1.0 = more contrast)", ) parser.add_argument( "--saturation", type=float, default=1.0, help="Adjust saturation (1.0 = no change, 0.0 = grayscale)", ) parser.add_argument( "--brightness", type=float, default=1.0, help="Adjust brightness (1.0 = no change, >1.0 = brighter)", ) parser.add_argument( "--transparency", type=float, default=1.0, help="Adjust transparency (1.0 = opaque, 0.0 = transparent)", ) parser.add_argument( "--tint-color", nargs=3, type=int, metavar=("R", "G", "B"), help="Apply color tint with RGB values (0-255)", ) parser.add_argument( "--tint-intensity", type=float, default=0.0, help="Tint intensity (0.0 = no tint, 1.0 = full tint)", ) parser.add_argument( "--format", help="Convert to specified format (PNG, JPEG, etc.)" ) parser.add_argument( "--quality", type=int, default=95, help="JPEG quality (1-100, default: 95)" ) # Utility arguments parser.add_argument( "--preserve-aspect", action="store_true", help="Preserve aspect ratio when resizing", ) parser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) args = parser.parse_args() # Validate input file input_path = Path(args.input) if not input_path.exists(): print(f"Error: Input file not found: {input_path}", file=sys.stderr) sys.exit(1) # Validate parameters if args.quality < 1 or args.quality > 100: print("Error: Quality must be between 1 and 100", file=sys.stderr) sys.exit(1) if args.tint_color: for val in args.tint_color: if val < 0 or val > 255: print( "Error: Tint color values must be between 0 and 255", file=sys.stderr, ) sys.exit(1) try: # Load the image using unified core print(f"Loading image: {input_path}") image = load_image(input_path) # Prepare transformation parameters transforms = { "grayscale": args.grayscale, "rotate": args.rotate, "transparency": args.transparency, "contrast": args.contrast, "saturation": args.saturation, "brightness": args.brightness, "blur_radius": args.blur, "format_override": args.format, } # Add resize if specified if args.resize: transforms["size"] = tuple(args.resize) # Add tint if specified if args.tint_color and args.tint_intensity > 0.0: transforms["tint_color"] = tuple(args.tint_color) transforms["tint_intensity"] = args.tint_intensity # Apply transformations using toolkit effects system print("Applying transformations...") processed_image = apply_toolkit_transformations(image, **transforms) # Save the result using unified core output_path = Path(args.output) print(f"Saving processed image: {output_path}") save_image(processed_image, output_path, quality=args.quality) print(f"Successfully processed image: {input_path} -> {output_path}") except Exception as e: print(f"Error processing image: {e}", file=sys.stderr) sys.exit(1)
[docs] def generate_embedded_images() -> None: """Console script entry point for generating embedded images.""" parser = argparse.ArgumentParser( description="Generate embedded images from a folder", prog="gui-image-studio-generate", ) parser.add_argument( "--folder", "-f", default="sample_images", help="Folder containing images (default: sample_images)", ) parser.add_argument( "--output", "-o", default="embedded_images.py", help="Output file name (default: embedded_images.py)", ) parser.add_argument( "--quality", "-q", type=int, default=85, help="Compression quality 1-100 (default: 85)", ) parser.add_argument( "--framework", choices=["tkinter", "customtkinter"], default="tkinter", help="Target framework (default: tkinter)", ) parser.add_argument( "--usage", choices=["icons", "buttons", "backgrounds", "general"], default="general", help="Usage type for examples (default: general)", ) parser.add_argument( "--no-examples", action="store_true", help="Don't include usage examples in generated code", ) parser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) args = parser.parse_args() # Validate quality parameter if not 1 <= args.quality <= 100: print("Error: Quality must be between 1 and 100", file=sys.stderr) sys.exit(1) try: print(f"Processing images from: {args.folder}") print(f"Target framework: {args.framework}") print(f"Usage type: {args.usage}") print(f"Quality: {args.quality}") embed_images_from_folder( folder_path=args.folder, output_file=args.output, compression_quality=args.quality, framework=args.framework, usage=args.usage, include_examples=not args.no_examples, ) print(f"Successfully generated {args.output}") except FileNotFoundError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) except ValueError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Unexpected error: {e}", file=sys.stderr) sys.exit(1)
[docs] def create_sample_images() -> None: """Console script entry point for creating sample images.""" parser = argparse.ArgumentParser( description="Create sample images for testing gui_image_studio functionality", prog="gui-image-studio-create-samples", ) parser.add_argument( "--output-dir", "-o", default="sample_images", help="Output directory for sample images (default: sample_images)", ) parser.add_argument( "--themes", nargs="+", choices=["default", "dark", "light", "colorful"], default=["default", "dark", "light"], help="Themes to generate (default: default dark light)", ) parser.add_argument( "--types", nargs="+", choices=["icons", "buttons", "shapes", "patterns", "gradients"], default=["icons", "buttons", "shapes"], help="Types of images to generate (default: icons buttons shapes)", ) parser.add_argument( "--include-animations", action="store_true", help="Include animated GIF samples", ) parser.add_argument( "--legacy-mode", action="store_true", help="Create images compatible with original sample_creator", ) parser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) args = parser.parse_args() try: if args.legacy_mode: # Use legacy-compatible creation print("Creating legacy-compatible sample images...") create_sample_images_legacy_compatible(args.output_dir) else: # Use new unified creation print(f"Creating sample images in: {args.output_dir}") print(f"Themes: {', '.join(args.themes)}") print(f"Types: {', '.join(args.types)}") print(f"Include animations: {args.include_animations}") created_files = create_sample_image_set( output_dir=args.output_dir, themes=args.themes, image_types=args.types, include_animations=args.include_animations, ) # Report results total_files = sum(len(files) for files in created_files.values()) print(f"\nSuccessfully created {total_files} sample images:") for theme, files in created_files.items(): print(f" {theme}: {len(files)} images") print(f"\nSample images saved to: {args.output_dir}") except Exception as e: print(f"Error: {e}", file=sys.stderr) import traceback traceback.print_exc() sys.exit(1)
[docs] def launch_designer() -> None: """Console script entry point for launching the image studio GUI.""" parser = argparse.ArgumentParser( description="Launch the GUI Image Studio", prog="gui-image-studio-designer", ) parser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) args = parser.parse_args() try: from ..image_studio.main_app import main main() except ImportError as e: print(f"Error importing GUI components: {e}", file=sys.stderr) print( "Make sure tkinter is available (usually built-in with Python)", file=sys.stderr, ) sys.exit(1) except Exception as e: print(f"Error launching studio: {e}", file=sys.stderr) sys.exit(1)
if __name__ == "__main__": # This allows the module to be run directly for testing if len(sys.argv) > 1 and sys.argv[1] == "process": image_processor() elif len(sys.argv) > 1 and sys.argv[1] == "generate": generate_embedded_images() elif len(sys.argv) > 1 and sys.argv[1] == "samples": create_sample_images() elif len(sys.argv) > 1 and sys.argv[1] == "designer": launch_designer() else: print( "Usage: python -m gui_image_studio.cli.commands [process|generate|samples|designer]" ) sys.exit(1) def list_effects() -> None: """Console script entry point for listing available image effects.""" parser = argparse.ArgumentParser( description="List available image effects and their parameters", prog="gui-image-studio-list-effects", ) parser.add_argument( "--category", "-c", help="Filter effects by category", ) parser.add_argument( "--detailed", "-d", action="store_true", help="Show detailed parameter information", ) parser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) args = parser.parse_args() try: effects = EffectRegistry.get_all_effects() categories = EffectRegistry.get_categories() if args.category: if args.category not in categories: print(f"Error: Unknown category '{args.category}'", file=sys.stderr) print(f"Available categories: {', '.join(categories.keys())}") sys.exit(1) effects_to_show = { name: effects[name] for name in categories[args.category] } print(f"Effects in category '{args.category}':") else: effects_to_show = effects print("All available image effects:") print("=" * 60) if args.detailed: # Detailed view with parameters for category, effect_names in categories.items(): if args.category and category != args.category: continue print(f"\n{category.upper()} EFFECTS:") print("-" * 40) for effect_name in effect_names: if effect_name in effects_to_show: effect = effects[effect_name] print(f"\n{effect.display_name} ({effect.name})") print(f" Description: {effect.description}") if effect.parameters: print(" Parameters:") for param in effect.parameters: param_info = ( f" - {param.name} ({param.param_type.__name__})" ) if param.default is not None: param_info += f", default: {param.default}" if ( param.min_value is not None or param.max_value is not None ): param_info += ( f", range: {param.min_value}-{param.max_value}" ) if param.choices: param_info += f", choices: {param.choices}" print(param_info) if param.description: print(f" {param.description}") else: print(" Parameters: None") else: # Simple list view for category, effect_names in categories.items(): if args.category and category != args.category: continue print(f"\n{category.upper()}:") for effect_name in effect_names: if effect_name in effects_to_show: effect = effects[effect_name] print(f" {effect.name:15} - {effect.display_name}") print(f"\nTotal effects: {len(effects_to_show)}") print(f"Categories: {', '.join(categories.keys())}") print("\nUse --detailed for parameter information") print("Use gui-image-studio-apply-effect to apply effects to images") except Exception as e: print(f"Error: {e}", file=sys.stderr) import traceback traceback.print_exc() sys.exit(1) def apply_effect() -> None: """Console script entry point for applying image effects.""" parser = argparse.ArgumentParser( description="Apply image effects to files", prog="gui-image-studio-apply-effect", ) parser.add_argument( "input_file", help="Input image file", ) parser.add_argument( "output_file", help="Output image file", ) parser.add_argument( "--effect", "-e", required=True, help="Effect name to apply", ) parser.add_argument( "--params", "-p", action="append", help="Effect parameters in format key=value (can be used multiple times)", ) parser.add_argument( "--list-effects", action="store_true", help="List available effects and exit", ) parser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) args = parser.parse_args() if args.list_effects: effects = EffectRegistry.get_all_effects() print("Available effects:") for name, effect in effects.items(): print(f" {name:15} - {effect.display_name}") return try: # Check if effect exists effect = EffectRegistry.get_effect(args.effect) if effect is None: print(f"Error: Unknown effect '{args.effect}'", file=sys.stderr) print("Use --list-effects to see available effects") sys.exit(1) # Parse parameters params = {} if args.params: for param_str in args.params: if "=" not in param_str: print( f"Error: Invalid parameter format '{param_str}'. Use key=value", file=sys.stderr, ) sys.exit(1) key, value = param_str.split("=", 1) # Try to convert value to appropriate type try: # Try int first if value.isdigit() or ( value.startswith("-") and value[1:].isdigit() ): params[key] = int(value) # Try float elif "." in value: params[key] = float(value) # Try boolean elif value.lower() in ("true", "false"): params[key] = value.lower() == "true" # Keep as string else: params[key] = value except ValueError: params[key] = value print(f"Loading image: {args.input_file}") image = load_image(args.input_file) print(f"Applying effect: {effect.display_name}") if params: print(f"Parameters: {params}") result = effect.apply(image, **params) print(f"Saving result: {args.output_file}") save_image(result, args.output_file) print("✅ Effect applied successfully!") except FileNotFoundError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) except ValueError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Unexpected error: {e}", file=sys.stderr) import traceback traceback.print_exc() sys.exit(1)