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.
Install command
npx @skill-hub/cli install openclaw-skills-image-utils
Repository
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 repositoryBest 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
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"
}
]
}
```