Back to skills
SkillHub ClubAnalyze Data & AIFull StackData / AI

image-utils

Classic image manipulation with Python Pillow - resize, crop, composite, format conversion, watermarks, brightness/contrast adjustments, and web optimization. Use this skill when post-processing AI-generated images, preparing images for web delivery, batch processing image directories, creating responsive image variants, or performing any deterministic pixel-level image operation. Works standalone or alongside bria-ai for post-processing generated images.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,126
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install openclaw-skills-image-utils

Repository

openclaw/skills

Skill path: skills/galbria/image-utils

Classic image manipulation with Python Pillow - resize, crop, composite, format conversion, watermarks, brightness/contrast adjustments, and web optimization. Use this skill when post-processing AI-generated images, preparing images for web delivery, batch processing image directories, creating responsive image variants, or performing any deterministic pixel-level image operation. Works standalone or alongside bria-ai for post-processing generated images.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Data / AI.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install image-utils into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding image-utils to shared team environments
  • Use image-utils for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: image-utils
description: Classic image manipulation with Python Pillow - resize, crop, composite, format conversion, watermarks, brightness/contrast adjustments, and web optimization. Use this skill when post-processing AI-generated images, preparing images for web delivery, batch processing image directories, creating responsive image variants, or performing any deterministic pixel-level image operation. Works standalone or alongside bria-ai for post-processing generated images.
license: MIT
metadata:
  author: Bria AI
  version: "1.2.5"
---

# Image Utilities

Pillow-based utilities for deterministic pixel-level image operations. Use for resize, crop, composite, format conversion, watermarks, and other standard image processing tasks.

## When to Use This Skill

- **Post-processing AI-generated images**: Resize, crop, optimize for web after generation
- **Format conversion**: PNG ↔ JPEG ↔ WEBP with quality control
- **Compositing**: Overlay images, paste subjects onto backgrounds
- **Batch processing**: Resize to multiple sizes, add watermarks
- **Web optimization**: Compress and resize for fast delivery
- **Social media preparation**: Crop to platform-specific aspect ratios

## Quick Reference

| Operation | Method | Description |
|-----------|--------|-------------|
| **Loading** | `load(source)` | Load from URL, path, bytes, or base64 |
| | `load_from_url(url)` | Download image from URL |
| **Saving** | `save(image, path)` | Save with format auto-detection |
| | `to_bytes(image, format)` | Convert to bytes |
| | `to_base64(image, format)` | Convert to base64 string |
| **Resizing** | `resize(image, width, height)` | Resize to exact dimensions |
| | `scale(image, factor)` | Scale by factor (0.5 = half) |
| | `thumbnail(image, size)` | Fit within size, maintain aspect |
| **Cropping** | `crop(image, left, top, right, bottom)` | Crop to region |
| | `crop_center(image, width, height)` | Crop from center |
| | `crop_to_aspect(image, ratio)` | Crop to aspect ratio |
| **Compositing** | `paste(bg, fg, position)` | Overlay at coordinates |
| | `composite(bg, fg, mask)` | Alpha composite |
| | `fit_to_canvas(image, w, h)` | Fit onto canvas size |
| **Borders** | `add_border(image, width, color)` | Add solid border |
| | `add_padding(image, padding)` | Add whitespace padding |
| **Transforms** | `rotate(image, angle)` | Rotate by degrees |
| | `flip_horizontal(image)` | Mirror horizontally |
| | `flip_vertical(image)` | Flip vertically |
| **Watermarks** | `add_text_watermark(image, text)` | Add text overlay |
| | `add_image_watermark(image, logo)` | Add logo watermark |
| **Adjustments** | `adjust_brightness(image, factor)` | Lighten/darken |
| | `adjust_contrast(image, factor)` | Adjust contrast |
| | `adjust_saturation(image, factor)` | Adjust color saturation |
| | `blur(image, radius)` | Apply Gaussian blur |
| **Web** | `optimize_for_web(image, max_size)` | Optimize for delivery |
| **Info** | `get_info(image)` | Get dimensions, format, mode |

## Requirements

```bash
pip install Pillow requests
```

## Basic Usage

```python
from image_utils import ImageUtils

# Load from URL
image = ImageUtils.load_from_url("https://example.com/image.jpg")

# Or load from various sources
image = ImageUtils.load("/path/to/image.png")         # File path
image = ImageUtils.load(image_bytes)                  # Bytes
image = ImageUtils.load("data:image/png;base64,...")  # Base64

# Resize and save
resized = ImageUtils.resize(image, width=800, height=600)
ImageUtils.save(resized, "output.webp", quality=90)

# Get image info
info = ImageUtils.get_info(image)
print(f"{info['width']}x{info['height']} {info['mode']}")
```

## Resizing & Scaling

```python
# Resize to exact dimensions
resized = ImageUtils.resize(image, width=800, height=600)

# Resize maintaining aspect ratio (fit within bounds)
fitted = ImageUtils.resize(image, width=800, height=600, maintain_aspect=True)

# Resize by width only (height auto-calculated)
resized = ImageUtils.resize(image, width=800)

# Scale by factor
half = ImageUtils.scale(image, 0.5)    # 50% size
double = ImageUtils.scale(image, 2.0)  # 200% size

# Create thumbnail
thumb = ImageUtils.thumbnail(image, (150, 150))
```

## Cropping

```python
# Crop to specific region
cropped = ImageUtils.crop(image, left=100, top=50, right=500, bottom=350)

# Crop from center
center = ImageUtils.crop_center(image, width=400, height=400)

# Crop to aspect ratio (for social media)
square = ImageUtils.crop_to_aspect(image, "1:1")      # Instagram
wide = ImageUtils.crop_to_aspect(image, "16:9")       # YouTube thumbnail
story = ImageUtils.crop_to_aspect(image, "9:16")      # Stories/Reels

# Control crop anchor
top_crop = ImageUtils.crop_to_aspect(image, "16:9", anchor="top")
bottom_crop = ImageUtils.crop_to_aspect(image, "16:9", anchor="bottom")
```

## Compositing

```python
# Paste foreground onto background
result = ImageUtils.paste(background, foreground, position=(100, 50))

# Alpha composite (foreground must have transparency)
result = ImageUtils.composite(background, foreground)

# Fit image onto canvas with letterboxing
canvas = ImageUtils.fit_to_canvas(
    image,
    width=1200,
    height=800,
    background_color=(255, 255, 255, 255),  # White
    position="center"  # or "top", "bottom"
)
```

## Format Conversion

```python
# Convert to different formats
png_bytes = ImageUtils.to_bytes(image, "PNG")
jpeg_bytes = ImageUtils.to_bytes(image, "JPEG", quality=85)
webp_bytes = ImageUtils.to_bytes(image, "WEBP", quality=90)

# Get base64 for data URLs
base64_str = ImageUtils.to_base64(image, "PNG")
data_url = ImageUtils.to_base64(image, "PNG", include_data_url=True)
# Returns: "data:image/png;base64,..."

# Save with format auto-detected from extension
ImageUtils.save(image, "output.png")
ImageUtils.save(image, "output.jpg", quality=85)
ImageUtils.save(image, "output.webp", quality=90)
```

## Watermarks

```python
# Text watermark
watermarked = ImageUtils.add_text_watermark(
    image,
    text="© 2024 My Company",
    position="bottom-right",  # bottom-left, top-right, top-left, center
    font_size=24,
    color=(255, 255, 255, 128),  # Semi-transparent white
    margin=20
)

# Logo/image watermark
logo = ImageUtils.load("logo.png")
watermarked = ImageUtils.add_image_watermark(
    image,
    watermark=logo,
    position="bottom-right",
    opacity=0.5,
    scale=0.15,  # 15% of image width
    margin=20
)
```

## Adjustments

```python
# Brightness (1.0 = original, <1 darker, >1 lighter)
bright = ImageUtils.adjust_brightness(image, 1.3)
dark = ImageUtils.adjust_brightness(image, 0.7)

# Contrast (1.0 = original)
high_contrast = ImageUtils.adjust_contrast(image, 1.5)

# Saturation (0 = grayscale, 1.0 = original, >1 more vivid)
vivid = ImageUtils.adjust_saturation(image, 1.3)
grayscale = ImageUtils.adjust_saturation(image, 0)

# Sharpness
sharp = ImageUtils.adjust_sharpness(image, 2.0)

# Blur
blurred = ImageUtils.blur(image, radius=5)
```

## Transforms

```python
# Rotate (counter-clockwise, degrees)
rotated = ImageUtils.rotate(image, 45)
rotated = ImageUtils.rotate(image, 90, expand=False)  # Don't expand canvas

# Flip
mirrored = ImageUtils.flip_horizontal(image)
flipped = ImageUtils.flip_vertical(image)
```

## Borders & Padding

```python
# Add solid border
bordered = ImageUtils.add_border(image, width=5, color=(0, 0, 0))

# Add padding (whitespace)
padded = ImageUtils.add_padding(image, padding=20)  # Uniform
padded = ImageUtils.add_padding(image, padding=(10, 20, 10, 20))  # left, top, right, bottom
```

## Web Optimization

```python
# Optimize for web delivery
optimized_bytes = ImageUtils.optimize_for_web(
    image,
    max_dimension=1920,  # Resize if larger
    format="WEBP",       # Best compression
    quality=85
)

# Save optimized
with open("optimized.webp", "wb") as f:
    f.write(optimized_bytes)
```

## Integration with AI Image Generation

Use with Bria AI or other image generation APIs:

```python
from bria_client import BriaClient
from image_utils import ImageUtils

client = BriaClient()

# Generate with AI
result = client.generate("product photo of headphones", aspect_ratio="1:1")
image_url = result['result']['image_url']

# Download and post-process
image = ImageUtils.load_from_url(image_url)

# Create multiple sizes for responsive images
sizes = {
    "large": ImageUtils.resize(image, width=1200),
    "medium": ImageUtils.resize(image, width=600),
    "thumb": ImageUtils.thumbnail(image, (150, 150))
}

# Save all as optimized WebP
for name, img in sizes.items():
    ImageUtils.save(img, f"product_{name}.webp", quality=85)
```

## Batch Processing Example

```python
from pathlib import Path
from image_utils import ImageUtils

def process_catalog(input_dir, output_dir):
    """Process all images in a directory."""
    output_path = Path(output_dir)
    output_path.mkdir(exist_ok=True)

    for image_file in Path(input_dir).glob("*.{jpg,png,webp}"):
        image = ImageUtils.load(image_file)

        # Crop to square
        square = ImageUtils.crop_to_aspect(image, "1:1")

        # Resize to standard size
        resized = ImageUtils.resize(square, width=800, height=800)

        # Add watermark
        final = ImageUtils.add_text_watermark(resized, "© My Brand")

        # Save optimized
        output_file = output_path / f"{image_file.stem}.webp"
        ImageUtils.save(final, output_file, quality=85)

process_catalog("./raw_images", "./processed")
```

## API Reference

See [image_utils.py](./references/code-examples/image_utils.py) for complete implementation with docstrings.


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/code-examples/image_utils.py

```python
#!/usr/bin/env python3
"""
Classic Image Manipulation Utilities

Pillow-based utilities for deterministic pixel-level operations that complement
Bria's AI-powered transformations. Use for resize, crop, composite, format
conversion, and other standard image processing tasks.

Usage:
    from image_utils import ImageUtils

    # Load from URL and resize
    image = ImageUtils.load_from_url("https://example.com/image.jpg")
    resized = ImageUtils.resize(image, width=800, height=600)
    ImageUtils.save(resized, "output.webp", quality=90)

    # Pipeline with Bria
    result = bria_client.generate("product photo", aspect_ratio="1:1")
    image = ImageUtils.load_from_url(result['result']['image_url'])
    final = ImageUtils.resize(image, width=800, height=800)
    ImageUtils.save(final, "product.webp")
"""

import io
import base64
import requests
from pathlib import Path
from typing import Union, Optional, Tuple, Dict, Any

from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance


class ImageUtils:
    """Classic image manipulation utilities using Pillow/PIL."""

    # ==================== Loading & Saving ====================

    @staticmethod
    def load(source: Union[str, bytes, Path]) -> Image.Image:
        """
        Load image from URL, file path, bytes, or base64 string.

        Args:
            source: URL string, file path, Path object, bytes, or base64 string

        Returns:
            PIL Image object

        Examples:
            image = ImageUtils.load("https://example.com/image.jpg")
            image = ImageUtils.load("/path/to/image.png")
            image = ImageUtils.load(image_bytes)
            image = ImageUtils.load("data:image/png;base64,...")
        """
        # Handle bytes directly
        if isinstance(source, bytes):
            return Image.open(io.BytesIO(source))

        # Handle Path objects
        if isinstance(source, Path):
            return Image.open(source)

        # Handle strings
        if isinstance(source, str):
            # Base64 data URL
            if source.startswith("data:image"):
                # Extract base64 portion after comma
                base64_data = source.split(",", 1)[1]
                image_bytes = base64.b64decode(base64_data)
                return Image.open(io.BytesIO(image_bytes))

            # Plain base64 string (no data URL prefix)
            if len(source) > 200 and not source.startswith(
                ("http://", "https://", "/")
            ):
                try:
                    image_bytes = base64.b64decode(source)
                    return Image.open(io.BytesIO(image_bytes))
                except Exception:
                    pass  # Not valid base64, try as path

            # URL
            if source.startswith(("http://", "https://")):
                return ImageUtils.load_from_url(source)

            # File path
            return Image.open(source)

        raise ValueError(f"Unsupported source type: {type(source)}")

    @staticmethod
    def load_from_url(url: str, timeout: int = 30) -> Image.Image:
        """
        Download and load image from URL.

        Args:
            url: Image URL
            timeout: Request timeout in seconds

        Returns:
            PIL Image object
        """
        response = requests.get(
            url, timeout=timeout, headers={"User-Agent": "BriaSkills/1.2.5"}
        )
        response.raise_for_status()
        return Image.open(io.BytesIO(response.content))

    @staticmethod
    def save(
        image: Image.Image,
        path: Union[str, Path],
        quality: int = 95,
        optimize: bool = True,
    ) -> None:
        """
        Save image to file with format auto-detection from extension.

        Args:
            image: PIL Image to save
            path: Output file path (format detected from extension)
            quality: Quality for lossy formats (1-100)
            optimize: Enable optimization for smaller file size

        Examples:
            ImageUtils.save(image, "output.png")
            ImageUtils.save(image, "output.jpg", quality=85)
            ImageUtils.save(image, "output.webp", quality=90)
        """
        path = Path(path)
        ext = path.suffix.lower()

        # Ensure directory exists
        path.parent.mkdir(parents=True, exist_ok=True)

        # Convert to appropriate mode for format
        save_image = image
        if ext in (".jpg", ".jpeg"):
            if image.mode in ("RGBA", "LA", "P"):
                save_image = image.convert("RGB")

        # Save with format-specific options
        save_kwargs = {"optimize": optimize}
        if ext in (".jpg", ".jpeg", ".webp"):
            save_kwargs["quality"] = quality

        save_image.save(path, **save_kwargs)

    @staticmethod
    def to_bytes(image: Image.Image, format: str = "PNG", quality: int = 95) -> bytes:
        """
        Convert image to bytes.

        Args:
            image: PIL Image
            format: Output format (PNG, JPEG, WEBP)
            quality: Quality for lossy formats

        Returns:
            Image as bytes
        """
        buffer = io.BytesIO()

        save_image = image
        if format.upper() == "JPEG" and image.mode in ("RGBA", "LA", "P"):
            save_image = image.convert("RGB")

        save_kwargs = {}
        if format.upper() in ("JPEG", "WEBP"):
            save_kwargs["quality"] = quality

        save_image.save(buffer, format=format, **save_kwargs)
        return buffer.getvalue()

    @staticmethod
    def to_base64(
        image: Image.Image,
        format: str = "PNG",
        quality: int = 95,
        include_data_url: bool = False,
    ) -> str:
        """
        Convert image to base64 string.

        Args:
            image: PIL Image
            format: Output format (PNG, JPEG, WEBP)
            quality: Quality for lossy formats
            include_data_url: Include data URL prefix

        Returns:
            Base64 encoded string
        """
        image_bytes = ImageUtils.to_bytes(image, format, quality)
        b64_string = base64.b64encode(image_bytes).decode("utf-8")

        if include_data_url:
            mime_types = {
                "PNG": "image/png",
                "JPEG": "image/jpeg",
                "WEBP": "image/webp",
            }
            mime = mime_types.get(format.upper(), "image/png")
            return f"data:{mime};base64,{b64_string}"

        return b64_string

    # ==================== Resizing & Scaling ====================

    @staticmethod
    def resize(
        image: Image.Image,
        width: Optional[int] = None,
        height: Optional[int] = None,
        maintain_aspect: bool = False,
        resample: int = Image.Resampling.LANCZOS,
    ) -> Image.Image:
        """
        Resize image to exact dimensions.

        Args:
            image: PIL Image
            width: Target width (if None, calculated from height)
            height: Target height (if None, calculated from width)
            maintain_aspect: If True, fit within dimensions keeping aspect ratio
            resample: Resampling filter

        Returns:
            New resized Image
        """
        if width is None and height is None:
            raise ValueError("Must specify width, height, or both")

        orig_width, orig_height = image.size

        if width is None:
            width = int(orig_width * height / orig_height)
        elif height is None:
            height = int(orig_height * width / orig_width)

        if maintain_aspect:
            # Calculate size that fits within bounds
            ratio = min(width / orig_width, height / orig_height)
            width = int(orig_width * ratio)
            height = int(orig_height * ratio)

        return image.resize((width, height), resample=resample)

    @staticmethod
    def scale(
        image: Image.Image, factor: float, resample: int = Image.Resampling.LANCZOS
    ) -> Image.Image:
        """
        Scale image by factor.

        Args:
            image: PIL Image
            factor: Scale factor (0.5 = half, 2.0 = double)
            resample: Resampling filter

        Returns:
            New scaled Image
        """
        width, height = image.size
        new_width = int(width * factor)
        new_height = int(height * factor)
        return image.resize((new_width, new_height), resample=resample)

    @staticmethod
    def thumbnail(
        image: Image.Image,
        size: Tuple[int, int],
        resample: int = Image.Resampling.LANCZOS,
    ) -> Image.Image:
        """
        Create thumbnail that fits within size, maintaining aspect ratio.

        Args:
            image: PIL Image
            size: Maximum (width, height)
            resample: Resampling filter

        Returns:
            New thumbnail Image
        """
        result = image.copy()
        result.thumbnail(size, resample=resample)
        return result

    # ==================== Cropping ====================

    @staticmethod
    def crop(
        image: Image.Image, left: int, top: int, right: int, bottom: int
    ) -> Image.Image:
        """
        Crop image to region.

        Args:
            image: PIL Image
            left: Left edge X coordinate
            top: Top edge Y coordinate
            right: Right edge X coordinate
            bottom: Bottom edge Y coordinate

        Returns:
            New cropped Image
        """
        return image.crop((left, top, right, bottom))

    @staticmethod
    def crop_center(image: Image.Image, width: int, height: int) -> Image.Image:
        """
        Crop from center of image.

        Args:
            image: PIL Image
            width: Crop width
            height: Crop height

        Returns:
            New cropped Image
        """
        img_width, img_height = image.size
        left = (img_width - width) // 2
        top = (img_height - height) // 2
        return image.crop((left, top, left + width, top + height))

    @staticmethod
    def crop_to_aspect(
        image: Image.Image, ratio: Union[str, float], anchor: str = "center"
    ) -> Image.Image:
        """
        Crop image to target aspect ratio.

        Args:
            image: PIL Image
            ratio: Aspect ratio as "16:9" string or float (width/height)
            anchor: Crop anchor - "center", "top", "bottom", "left", "right"

        Returns:
            New cropped Image
        """
        # Parse ratio
        if isinstance(ratio, str):
            w, h = map(int, ratio.split(":"))
            target_ratio = w / h
        else:
            target_ratio = ratio

        img_width, img_height = image.size
        current_ratio = img_width / img_height

        if current_ratio > target_ratio:
            # Image is wider, crop width
            new_width = int(img_height * target_ratio)
            new_height = img_height
        else:
            # Image is taller, crop height
            new_width = img_width
            new_height = int(img_width / target_ratio)

        # Calculate position based on anchor
        if anchor == "center":
            left = (img_width - new_width) // 2
            top = (img_height - new_height) // 2
        elif anchor == "top":
            left = (img_width - new_width) // 2
            top = 0
        elif anchor == "bottom":
            left = (img_width - new_width) // 2
            top = img_height - new_height
        elif anchor == "left":
            left = 0
            top = (img_height - new_height) // 2
        elif anchor == "right":
            left = img_width - new_width
            top = (img_height - new_height) // 2
        else:
            raise ValueError(f"Unknown anchor: {anchor}")

        return image.crop((left, top, left + new_width, top + new_height))

    # ==================== Compositing ====================

    @staticmethod
    def paste(
        background: Image.Image,
        foreground: Image.Image,
        position: Tuple[int, int] = (0, 0),
        use_alpha: bool = True,
    ) -> Image.Image:
        """
        Paste foreground onto background at position.

        Args:
            background: Background image
            foreground: Image to paste
            position: (x, y) position for top-left of foreground
            use_alpha: Use foreground alpha channel as mask

        Returns:
            New composited Image
        """
        result = background.copy()
        if result.mode != "RGBA":
            result = result.convert("RGBA")

        if use_alpha and foreground.mode == "RGBA":
            result.paste(foreground, position, foreground)
        else:
            result.paste(foreground, position)

        return result

    @staticmethod
    def composite(
        background: Image.Image,
        foreground: Image.Image,
        mask: Optional[Image.Image] = None,
    ) -> Image.Image:
        """
        Alpha composite foreground over background.

        Args:
            background: Background image
            foreground: Foreground image (must match background size)
            mask: Optional mask image

        Returns:
            New composited Image
        """
        bg = background.convert("RGBA") if background.mode != "RGBA" else background
        fg = foreground.convert("RGBA") if foreground.mode != "RGBA" else foreground

        if mask:
            mask = mask.convert("L")
            return Image.composite(fg, bg, mask)

        return Image.alpha_composite(bg, fg)

    @staticmethod
    def fit_to_canvas(
        image: Image.Image,
        width: int,
        height: int,
        background_color: Tuple[int, int, int, int] = (255, 255, 255, 0),
        position: str = "center",
    ) -> Image.Image:
        """
        Fit image onto canvas, letterboxing if needed.

        Args:
            image: PIL Image
            width: Canvas width
            height: Canvas height
            background_color: RGBA tuple for background
            position: Image position - "center", "top", "bottom"

        Returns:
            New Image on canvas
        """
        # Resize to fit
        resized = ImageUtils.resize(image, width, height, maintain_aspect=True)

        # Create canvas
        canvas = Image.new("RGBA", (width, height), background_color)

        # Calculate position
        res_width, res_height = resized.size
        if position == "center":
            x = (width - res_width) // 2
            y = (height - res_height) // 2
        elif position == "top":
            x = (width - res_width) // 2
            y = 0
        elif position == "bottom":
            x = (width - res_width) // 2
            y = height - res_height
        else:
            x = (width - res_width) // 2
            y = (height - res_height) // 2

        canvas.paste(resized, (x, y), resized if resized.mode == "RGBA" else None)
        return canvas

    # ==================== Format Conversion ====================

    @staticmethod
    def convert_format(image: Image.Image, format: str, quality: int = 95) -> bytes:
        """
        Convert image to different format.

        Args:
            image: PIL Image
            format: Target format (PNG, JPEG, WEBP)
            quality: Quality for lossy formats

        Returns:
            Image bytes in new format
        """
        return ImageUtils.to_bytes(image, format, quality)

    @staticmethod
    def get_info(image: Image.Image) -> Dict[str, Any]:
        """
        Get image metadata.

        Args:
            image: PIL Image

        Returns:
            Dict with width, height, mode, format info
        """
        return {
            "width": image.width,
            "height": image.height,
            "mode": image.mode,
            "format": image.format,
            "has_alpha": image.mode in ("RGBA", "LA", "PA"),
            "aspect_ratio": round(image.width / image.height, 3),
        }

    # ==================== Borders & Padding ====================

    @staticmethod
    def add_border(
        image: Image.Image, width: int, color: Tuple[int, int, int] = (0, 0, 0)
    ) -> Image.Image:
        """
        Add solid border around image.

        Args:
            image: PIL Image
            width: Border width in pixels
            color: RGB color tuple

        Returns:
            New Image with border
        """
        img_width, img_height = image.size
        new_width = img_width + 2 * width
        new_height = img_height + 2 * width

        # Create new image with border color
        result = Image.new(image.mode, (new_width, new_height), color)
        result.paste(image, (width, width))
        return result

    @staticmethod
    def add_padding(
        image: Image.Image,
        padding: Union[int, Tuple[int, int, int, int]],
        color: Tuple[int, int, int, int] = (255, 255, 255, 255),
    ) -> Image.Image:
        """
        Add whitespace padding around image.

        Args:
            image: PIL Image
            padding: Uniform padding or (left, top, right, bottom)
            color: RGBA color for padding

        Returns:
            New padded Image
        """
        if isinstance(padding, int):
            left = top = right = bottom = padding
        else:
            left, top, right, bottom = padding

        img_width, img_height = image.size
        new_width = img_width + left + right
        new_height = img_height + top + bottom

        result = Image.new("RGBA", (new_width, new_height), color)

        if image.mode == "RGBA":
            result.paste(image, (left, top), image)
        else:
            result.paste(image, (left, top))

        return result

    # ==================== Transforms ====================

    @staticmethod
    def rotate(
        image: Image.Image,
        angle: float,
        expand: bool = True,
        fill_color: Tuple[int, int, int, int] = (255, 255, 255, 0),
    ) -> Image.Image:
        """
        Rotate image by degrees (counter-clockwise).

        Args:
            image: PIL Image
            angle: Rotation angle in degrees
            expand: Expand canvas to fit rotated image
            fill_color: Color for new corners

        Returns:
            New rotated Image
        """
        return image.rotate(
            angle,
            expand=expand,
            fillcolor=fill_color,
            resample=Image.Resampling.BICUBIC,
        )

    @staticmethod
    def flip_horizontal(image: Image.Image) -> Image.Image:
        """
        Mirror image horizontally.

        Args:
            image: PIL Image

        Returns:
            New flipped Image
        """
        return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)

    @staticmethod
    def flip_vertical(image: Image.Image) -> Image.Image:
        """
        Flip image vertically.

        Args:
            image: PIL Image

        Returns:
            New flipped Image
        """
        return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)

    # ==================== Watermarks ====================

    @staticmethod
    def add_text_watermark(
        image: Image.Image,
        text: str,
        position: str = "bottom-right",
        font_size: int = 24,
        color: Tuple[int, int, int, int] = (255, 255, 255, 128),
        margin: int = 10,
    ) -> Image.Image:
        """
        Add text watermark to image.

        Args:
            image: PIL Image
            text: Watermark text
            position: "bottom-right", "bottom-left", "top-right", "top-left", "center"
            font_size: Font size
            color: RGBA color (with alpha for transparency)
            margin: Margin from edges

        Returns:
            New watermarked Image
        """
        result = image.convert("RGBA")
        overlay = Image.new("RGBA", result.size, (0, 0, 0, 0))
        draw = ImageDraw.Draw(overlay)

        # Try to load a font, fall back to default
        try:
            font = ImageFont.truetype(
                "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", font_size
            )
        except (IOError, OSError):
            try:
                font = ImageFont.truetype(
                    "/System/Library/Fonts/Helvetica.ttc", font_size
                )
            except (IOError, OSError):
                font = ImageFont.load_default()

        # Get text size
        bbox = draw.textbbox((0, 0), text, font=font)
        text_width = bbox[2] - bbox[0]
        text_height = bbox[3] - bbox[1]

        # Calculate position
        img_width, img_height = result.size
        positions = {
            "bottom-right": (
                img_width - text_width - margin,
                img_height - text_height - margin,
            ),
            "bottom-left": (margin, img_height - text_height - margin),
            "top-right": (img_width - text_width - margin, margin),
            "top-left": (margin, margin),
            "center": ((img_width - text_width) // 2, (img_height - text_height) // 2),
        }
        x, y = positions.get(position, positions["bottom-right"])

        draw.text((x, y), text, font=font, fill=color)
        return Image.alpha_composite(result, overlay)

    @staticmethod
    def add_image_watermark(
        image: Image.Image,
        watermark: Image.Image,
        position: str = "bottom-right",
        opacity: float = 0.5,
        scale: float = 0.2,
        margin: int = 10,
    ) -> Image.Image:
        """
        Add image/logo watermark.

        Args:
            image: PIL Image
            watermark: Watermark image
            position: "bottom-right", "bottom-left", "top-right", "top-left", "center"
            opacity: Watermark opacity (0-1)
            scale: Scale watermark relative to image width
            margin: Margin from edges

        Returns:
            New watermarked Image
        """
        result = image.convert("RGBA")

        # Scale watermark
        wm_width = int(result.width * scale)
        wm = ImageUtils.resize(watermark, width=wm_width)

        # Apply opacity
        if wm.mode == "RGBA":
            r, g, b, a = wm.split()
            a = a.point(lambda x: int(x * opacity))
            wm = Image.merge("RGBA", (r, g, b, a))
        else:
            wm = wm.convert("RGBA")

        # Calculate position
        img_width, img_height = result.size
        wm_w, wm_h = wm.size
        positions = {
            "bottom-right": (img_width - wm_w - margin, img_height - wm_h - margin),
            "bottom-left": (margin, img_height - wm_h - margin),
            "top-right": (img_width - wm_w - margin, margin),
            "top-left": (margin, margin),
            "center": ((img_width - wm_w) // 2, (img_height - wm_h) // 2),
        }
        x, y = positions.get(position, positions["bottom-right"])

        result.paste(wm, (x, y), wm)
        return result

    # ==================== Adjustments ====================

    @staticmethod
    def adjust_brightness(image: Image.Image, factor: float) -> Image.Image:
        """
        Adjust image brightness.

        Args:
            image: PIL Image
            factor: Brightness factor (1.0 = original, < 1 darker, > 1 lighter)

        Returns:
            New adjusted Image
        """
        enhancer = ImageEnhance.Brightness(image)
        return enhancer.enhance(factor)

    @staticmethod
    def adjust_contrast(image: Image.Image, factor: float) -> Image.Image:
        """
        Adjust image contrast.

        Args:
            image: PIL Image
            factor: Contrast factor (1.0 = original, < 1 less contrast, > 1 more)

        Returns:
            New adjusted Image
        """
        enhancer = ImageEnhance.Contrast(image)
        return enhancer.enhance(factor)

    @staticmethod
    def adjust_saturation(image: Image.Image, factor: float) -> Image.Image:
        """
        Adjust color saturation.

        Args:
            image: PIL Image
            factor: Saturation factor (1.0 = original, 0 = grayscale, > 1 more vivid)

        Returns:
            New adjusted Image
        """
        enhancer = ImageEnhance.Color(image)
        return enhancer.enhance(factor)

    @staticmethod
    def adjust_sharpness(image: Image.Image, factor: float) -> Image.Image:
        """
        Adjust image sharpness.

        Args:
            image: PIL Image
            factor: Sharpness factor (1.0 = original, < 1 blur, > 1 sharper)

        Returns:
            New adjusted Image
        """
        enhancer = ImageEnhance.Sharpness(image)
        return enhancer.enhance(factor)

    @staticmethod
    def blur(image: Image.Image, radius: float = 2.0) -> Image.Image:
        """
        Apply Gaussian blur to image.

        Args:
            image: PIL Image
            radius: Blur radius

        Returns:
            New blurred Image
        """
        return image.filter(ImageFilter.GaussianBlur(radius=radius))

    # ==================== Web Optimization ====================

    @staticmethod
    def optimize_for_web(
        image: Image.Image,
        max_dimension: int = 1920,
        format: str = "WEBP",
        quality: int = 85,
    ) -> bytes:
        """
        Optimize image for web delivery.

        Args:
            image: PIL Image
            max_dimension: Maximum width or height
            format: Output format (WEBP recommended)
            quality: Output quality

        Returns:
            Optimized image bytes
        """
        # Resize if needed
        width, height = image.size
        if width > max_dimension or height > max_dimension:
            image = ImageUtils.resize(
                image, max_dimension, max_dimension, maintain_aspect=True
            )

        return ImageUtils.to_bytes(image, format, quality)


# ==================== CLI Examples ====================

if __name__ == "__main__":
    # Example: Load, resize, save
    print("=== ImageUtils Examples ===\n")

    # Create a test image
    test_image = Image.new("RGB", (800, 600), color=(100, 150, 200))

    print("1. Basic operations:")
    info = ImageUtils.get_info(test_image)
    print(f"   Original: {info['width']}x{info['height']}")

    resized = ImageUtils.resize(test_image, width=400)
    print(f"   Resized (width=400): {resized.width}x{resized.height}")

    scaled = ImageUtils.scale(test_image, 0.5)
    print(f"   Scaled (0.5x): {scaled.width}x{scaled.height}")

    print("\n2. Cropping:")
    cropped = ImageUtils.crop_to_aspect(test_image, "16:9")
    print(f"   Crop to 16:9: {cropped.width}x{cropped.height}")

    center_crop = ImageUtils.crop_center(test_image, 200, 200)
    print(f"   Center crop 200x200: {center_crop.width}x{center_crop.height}")

    print("\n3. Format conversion:")
    png_bytes = ImageUtils.to_bytes(test_image, "PNG")
    print(f"   PNG: {len(png_bytes):,} bytes")

    jpeg_bytes = ImageUtils.to_bytes(test_image, "JPEG", quality=85)
    print(f"   JPEG (q=85): {len(jpeg_bytes):,} bytes")

    webp_bytes = ImageUtils.to_bytes(test_image, "WEBP", quality=85)
    print(f"   WEBP (q=85): {len(webp_bytes):,} bytes")

    print("\n4. Adjustments:")
    bright = ImageUtils.adjust_brightness(test_image, 1.5)
    contrast = ImageUtils.adjust_contrast(test_image, 1.2)
    blurred = ImageUtils.blur(test_image, radius=5)
    print("   Brightness, contrast, blur applied")

    print("\n5. Transforms:")
    rotated = ImageUtils.rotate(test_image, 45)
    print(f"   Rotated 45deg: {rotated.width}x{rotated.height}")

    flipped = ImageUtils.flip_horizontal(test_image)
    print(f"   Flipped horizontal: {flipped.width}x{flipped.height}")

    print("\n=== Complete ===")

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "galbria",
  "slug": "image-utils",
  "displayName": "Image Utils",
  "latest": {
    "version": "1.2.3",
    "publishedAt": 1773176832484,
    "commit": "https://github.com/openclaw/skills/commit/15c69e83db0b9626720c102d171dc36222f1aace"
  },
  "history": [
    {
      "version": "1.2.2",
      "publishedAt": 1772978614996,
      "commit": "https://github.com/openclaw/skills/commit/02734e738f4f3298090315536b11ce36be32c51b"
    },
    {
      "version": "1.2.1",
      "publishedAt": 1771850932425,
      "commit": "https://github.com/openclaw/skills/commit/149b98ffe563232dc00ad24967b2aebc2920716d"
    },
    {
      "version": "0.0.1",
      "publishedAt": 1770541174067,
      "commit": "https://github.com/openclaw/skills/commit/eff1a0eaa20949460910d862f09324ee133f2d68"
    }
  ]
}

```

image-utils | SkillHub