| """
|
| Image Editing
|
| Part of SOVEREIGN PYTHON LLM ENGINE
|
|
|
| Edit images using PIL/Pillow.
|
| """
|
|
|
| from typing import Any
|
| from pathlib import Path
|
| from dataclasses import dataclass
|
| from io import BytesIO
|
|
|
|
|
| @dataclass
|
| class EditedImage:
|
| """Edited image result"""
|
| image_bytes: bytes
|
| format: str
|
| size: tuple[int, int]
|
| operations: list[str]
|
|
|
|
|
| class ImageEditor:
|
| """
|
| Image editing operations using Pillow.
|
|
|
| Supports:
|
| - Resize, crop, rotate
|
| - Format conversion
|
| - Filters and adjustments
|
| - Watermarking
|
| """
|
|
|
| def __init__(self):
|
| try:
|
| from PIL import Image
|
| except ImportError:
|
| raise ImportError("Pillow is required. Install: pip install Pillow")
|
|
|
| async def resize(
|
| self,
|
| image_path: Path,
|
| width: int,
|
| height: int,
|
| maintain_aspect: bool = True
|
| ) -> EditedImage:
|
| """
|
| Resize image.
|
|
|
| Args:
|
| image_path: Input image
|
| width: Target width
|
| height: Target height
|
| maintain_aspect: Maintain aspect ratio
|
|
|
| Returns:
|
| EditedImage
|
| """
|
| from PIL import Image
|
| import asyncio
|
|
|
| def _sync_resize():
|
| img = Image.open(image_path)
|
|
|
| if maintain_aspect:
|
| img.thumbnail((width, height), Image.Resampling.LANCZOS)
|
| else:
|
| img = img.resize((width, height), Image.Resampling.LANCZOS)
|
|
|
|
|
| buffer = BytesIO()
|
| img.save(buffer, format=img.format or "PNG")
|
| buffer.seek(0)
|
|
|
| return EditedImage(
|
| image_bytes=buffer.read(),
|
| format=img.format or "PNG",
|
| size=img.size,
|
| operations=["resize"]
|
| )
|
|
|
| return await asyncio.to_thread(_sync_resize)
|
|
|
| async def crop(
|
| self,
|
| image_path: Path,
|
| left: int,
|
| top: int,
|
| right: int,
|
| bottom: int
|
| ) -> EditedImage:
|
| """
|
| Crop image.
|
|
|
| Args:
|
| image_path: Input image
|
| left, top, right, bottom: Crop box
|
|
|
| Returns:
|
| EditedImage
|
| """
|
| from PIL import Image
|
| import asyncio
|
|
|
| def _sync_crop():
|
| img = Image.open(image_path)
|
| img = img.crop((left, top, right, bottom))
|
|
|
| buffer = BytesIO()
|
| img.save(buffer, format=img.format or "PNG")
|
| buffer.seek(0)
|
|
|
| return EditedImage(
|
| image_bytes=buffer.read(),
|
| format=img.format or "PNG",
|
| size=img.size,
|
| operations=["crop"]
|
| )
|
|
|
| return await asyncio.to_thread(_sync_crop)
|
|
|
| async def rotate(
|
| self,
|
| image_path: Path,
|
| angle: float,
|
| expand: bool = True
|
| ) -> EditedImage:
|
| """
|
| Rotate image.
|
|
|
| Args:
|
| image_path: Input image
|
| angle: Rotation angle in degrees
|
| expand: Expand image to fit rotated content
|
|
|
| Returns:
|
| EditedImage
|
| """
|
| from PIL import Image
|
| import asyncio
|
|
|
| def _sync_rotate():
|
| img = Image.open(image_path)
|
| img = img.rotate(angle, expand=expand)
|
|
|
| buffer = BytesIO()
|
| img.save(buffer, format=img.format or "PNG")
|
| buffer.seek(0)
|
|
|
| return EditedImage(
|
| image_bytes=buffer.read(),
|
| format=img.format or "PNG",
|
| size=img.size,
|
| operations=["rotate"]
|
| )
|
|
|
| return await asyncio.to_thread(_sync_rotate)
|
|
|
| async def convert_format(
|
| self,
|
| image_path: Path,
|
| target_format: str
|
| ) -> EditedImage:
|
| """
|
| Convert image format.
|
|
|
| Args:
|
| image_path: Input image
|
| target_format: Target format ("PNG", "JPEG", "WEBP", etc.)
|
|
|
| Returns:
|
| EditedImage
|
| """
|
| from PIL import Image
|
| import asyncio
|
|
|
| def _sync_convert():
|
| img = Image.open(image_path)
|
|
|
|
|
| if target_format.upper() == "JPEG" and img.mode == "RGBA":
|
| img = img.convert("RGB")
|
|
|
| buffer = BytesIO()
|
| img.save(buffer, format=target_format)
|
| buffer.seek(0)
|
|
|
| return EditedImage(
|
| image_bytes=buffer.read(),
|
| format=target_format,
|
| size=img.size,
|
| operations=["convert"]
|
| )
|
|
|
| return await asyncio.to_thread(_sync_convert)
|
|
|
| async def apply_filter(
|
| self,
|
| image_path: Path,
|
| filter_name: str
|
| ) -> EditedImage:
|
| """
|
| Apply filter to image.
|
|
|
| Args:
|
| image_path: Input image
|
| filter_name: Filter name ("blur", "sharpen", "contour", etc.)
|
|
|
| Returns:
|
| EditedImage
|
| """
|
| from PIL import Image, ImageFilter
|
| import asyncio
|
|
|
| def _sync_filter():
|
| img = Image.open(image_path)
|
|
|
|
|
| filters = {
|
| "blur": ImageFilter.BLUR,
|
| "sharpen": ImageFilter.SHARPEN,
|
| "contour": ImageFilter.CONTOUR,
|
| "detail": ImageFilter.DETAIL,
|
| "edge_enhance": ImageFilter.EDGE_ENHANCE,
|
| "smooth": ImageFilter.SMOOTH,
|
| }
|
|
|
| filter_obj = filters.get(filter_name.lower())
|
| if not filter_obj:
|
| raise ValueError(f"Unknown filter: {filter_name}")
|
|
|
| img = img.filter(filter_obj)
|
|
|
| buffer = BytesIO()
|
| img.save(buffer, format=img.format or "PNG")
|
| buffer.seek(0)
|
|
|
| return EditedImage(
|
| image_bytes=buffer.read(),
|
| format=img.format or "PNG",
|
| size=img.size,
|
| operations=[f"filter:{filter_name}"]
|
| )
|
|
|
| return await asyncio.to_thread(_sync_filter)
|
|
|
|
|
|
|
| async def resize_image_tool(
|
| image_path: str,
|
| width: int,
|
| height: int,
|
| output_path: str | None = None
|
| ) -> dict:
|
| """Resize image tool"""
|
| editor = ImageEditor()
|
| result = await editor.resize(Path(image_path), width, height)
|
|
|
| if output_path:
|
| Path(output_path).write_bytes(result.image_bytes)
|
|
|
| return {
|
| "size": result.size,
|
| "format": result.format,
|
| "operations": result.operations
|
| }
|
|
|
|
|
| async def convert_image_tool(
|
| image_path: str,
|
| target_format: str,
|
| output_path: str | None = None
|
| ) -> dict:
|
| """Convert image format tool"""
|
| editor = ImageEditor()
|
| result = await editor.convert_format(Path(image_path), target_format)
|
|
|
| if output_path:
|
| Path(output_path).write_bytes(result.image_bytes)
|
|
|
| return {
|
| "format": result.format,
|
| "size": result.size
|
| }
|
|
|