Production-grade async image preprocessing for LLM APIs
Project description
ImageOps: Production-Grade Image Preprocessing for LLMs
Simple, fast, and production-ready async image preprocessing for LLM APIs.
PyPI Package:
llm-imageops(import asimageops)
ImageOps handles all the complexity of preparing images for LLM providers like Anthropic Claude, automatically handling resizing, compression, slicing, and encoding based on each provider's limits.
🚀 Features
- Zero Configuration - Works out of the box with sensible defaults
- 100% Async - Built for modern async Python applications
- Production Ready - Comprehensive error handling, security, resource limits
- Provider Agnostic - Easy to add support for new LLM providers
- Type Safe - Full type hints with Pydantic validation
- Edge Case Handling - Animated GIFs, panoramas, EXIF orientation, alpha channels
- Smart Optimization - Only processes when necessary based on provider limits
- Resource Safe - Automatic cleanup with async context managers
📦 Installation
pip install llm-imageops
Import as:
import imageops # Module name stays 'imageops'
Or install from source:
git clone https://github.com/yourusername/imageops.git
cd imageops
pip install -e .
🎯 Quick Start
Simple One-Liner
import imageops
import asyncio
async def main():
# Process image for Anthropic Claude
result = await imageops.process("image.jpg", provider="anthropic")
# Use in your LLM API call
messages = [
{
"role": "user",
"content": result.to_anthropic_format() + [
{"type": "text", "text": "What's in this image?"}
]
}
]
asyncio.run(main())
Process from URL
result = await imageops.process(
"https://example.com/image.jpg",
provider="anthropic"
)
Get File Paths Instead of Base64
result = await imageops.process(
"large_image.jpg",
provider="anthropic",
output_format="file" # Returns processed file paths
)
With Progress Callback
async def progress_callback(step: str, progress: float):
print(f"{step}: {progress:.1%}")
result = await imageops.process(
"huge_image.jpg",
provider="anthropic",
progress_callback=progress_callback
)
Batch Processing
results = await imageops.process_batch(
["img1.jpg", "img2.jpg", "img3.jpg"],
provider="anthropic"
)
for result in results:
print(f"Processed {result.slice_count} image(s)")
🔧 Advanced Usage
Custom Configuration
import imageops
config = imageops.ImageOpsConfig(
max_image_size_mb=50, # Reject images > 50MB
max_batch_size=20, # Limit batch processing
enable_logging=True,
log_level="DEBUG",
default_quality=85 # JPEG quality
)
async with imageops.ImageProcessor(provider="anthropic", config=config) as processor:
result1 = await processor.process("img1.jpg")
result2 = await processor.process("img2.jpg", timeout=60.0)
# Automatic cleanup
Custom Provider
from imageops.providers import BaseProvider, ProviderConfig, register_provider
class MyCustomProvider(BaseProvider):
name = "my_llm"
config = ProviderConfig(
max_width=4096,
max_height=4096,
max_size_mb=5.0,
max_base64_size_mb=10.0,
max_images_per_call=10,
supported_formats=["jpeg", "png"]
)
def needs_processing(self, metadata) -> bool:
return metadata.width > 4096 or metadata.size_mb > 5.0
def format_for_api(self, images):
# Return your provider's format
pass
# Register it
register_provider("my_llm", MyCustomProvider())
# Use it
result = await imageops.process("image.jpg", provider="my_llm")
📊 What It Does
ImageOps automatically handles:
Resizing
- Reduces width/height to fit provider limits
- Maintains aspect ratio
- Uses high-quality INTER_AREA interpolation
Slicing
- Splits very tall images into vertical segments
- Splits very wide panoramas into horizontal slices
- Processes each segment independently
Compression
- Iterative quality reduction (95 → 10)
- Falls back to dimension reduction if needed
- Ensures file size stays under limits
Edge Cases
- Animated GIFs - Extracts first frame
- EXIF Orientation - Auto-rotates images
- Alpha Channels - Converts RGBA → RGB on white background
- Wide Panoramas - Horizontal slicing for 50000x1000 images
- Corrupted Images - Clear error messages
🔒 Security
- Path Traversal Protection - Validates file paths
- File Size Limits - Rejects images > 100MB by default
- Dimension Limits - Maximum 100,000px per side
- Format Whitelist - Only allows safe formats
- URL Validation - Proper URL format checking
📈 Result Format
result = await imageops.process("image.jpg")
# Result attributes
result.success # bool
result.images # List[ImageOutput]
result.output_format # "base64" or "file"
result.was_sliced # bool
result.slice_count # int
result.processing_time_ms # float
result.provider # str
result.original_metadata # Dict
# Each image has THREE ways to access base64 + media type:
# 1. Separate fields (most flexible)
for img in result.images:
base64_data = img.data # Just the base64 string
media_type = img.media_type # e.g., "image/jpeg"
# Combine as needed: f"data:{media_type};base64,{base64_data}"
# 2. Data URI with prefix (convenience method)
for img in result.images:
data_uri = img.to_data_uri() # "data:image/jpeg;base64,<data>"
# 3. Anthropic API format (ready-to-use)
content = result.to_anthropic_format()
# Returns Anthropic-formatted content blocks with media_type included
# Other attributes
for img in result.images:
img.format # "base64" or "file"
img.width # int
img.height # int
img.size_bytes # int
img.was_compressed # bool
img.compression_quality # Optional[int]
# Convenience methods
result.to_anthropic_format() # Ready for Anthropic API
await result.cleanup() # Manual cleanup if needed
⚙️ Configuration Options
ImageOpsConfig(
# Resource Limits
max_image_size_mb=100.0, # Max input image size
max_dimension=100000, # Max width/height
max_batch_size=50, # Max images per batch
# Performance
max_concurrent_ops=5, # Concurrent operations
download_timeout=10.0, # Download timeout (seconds)
processing_timeout=300.0, # Processing timeout (seconds)
# Retry Logic
max_download_retries=3, # Download retry attempts
retry_backoff_factor=2.0, # Exponential backoff
# Compression
default_quality=90, # JPEG quality (1-100)
quality_step=5, # Quality reduction step
min_quality=10, # Minimum quality
# Logging
enable_logging=True,
log_level="INFO", # DEBUG, INFO, WARNING, ERROR
log_file=None, # Optional log file
# Security
validate_paths=True, # Path traversal protection
allowed_formats=[".jpg", ".jpeg", ".png", ".gif", ".webp"]
)
🧪 Testing
The package includes comprehensive tests:
# Install dev dependencies
pip install -e ".[dev]"
# Run all tests
pytest
# Run with coverage
pytest --cov=imageops --cov-report=html
# Run specific test categories
pytest tests/test_operations.py
pytest tests/test_edge_cases.py
🐛 Troubleshooting
Image Download Fails
from imageops.exceptions import ImageDownloadError
try:
result = await imageops.process("https://example.com/image.jpg")
except ImageDownloadError as e:
print(f"Download failed: {e}")
# Handle fallback
Image Too Large
from imageops.exceptions import ImageTooLargeError
try:
result = await imageops.process("huge.jpg")
except ImageTooLargeError as e:
print(f"Image exceeds limit: {e}")
Processing Timeout
from imageops.exceptions import TimeoutError
try:
result = await imageops.process("image.jpg", timeout=30.0)
except TimeoutError as e:
print(f"Processing took too long: {e}")
Enable Debug Logging
config = imageops.ImageOpsConfig(
enable_logging=True,
log_level="DEBUG",
log_file="imageops.log"
)
processor = imageops.ImageProcessor(config=config)
🏗️ Architecture
imageops/
├── core/ # Main processor and result types
├── providers/ # Provider implementations
├── operations/ # Image operations (resize, slice, compress, encode)
├── strategies/ # Processing strategies
├── utils/ # Utilities (validation, cleanup, logging, metadata)
├── config.py # Configuration
└── exceptions.py # Exception hierarchy
🤝 Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📝 License
MIT License - see LICENSE file for details.
🙏 Acknowledgments
Built with best practices from:
- httpx - Async HTTP patterns
- aiofiles - Async file I/O
- pydantic - Type validation
- opencv-python - Image processing
📧 Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with ❤️ for the LLM community
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llm_imageops-1.0.3.tar.gz.
File metadata
- Download URL: llm_imageops-1.0.3.tar.gz
- Upload date:
- Size: 26.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c633dc2690efdacde4988346a1aac4f7eabb3705e20faa4612d2a0ca76649a4
|
|
| MD5 |
dfc36883a4b276dc6567cad8c20bc67f
|
|
| BLAKE2b-256 |
dfcf6aa7363fb7ea80c18dd0e0d062eacae01864645ceb532e88896447bf90ec
|
File details
Details for the file llm_imageops-1.0.3-py3-none-any.whl.
File metadata
- Download URL: llm_imageops-1.0.3-py3-none-any.whl
- Upload date:
- Size: 29.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee3d03d7b669dd16c79c2b71d817056685a8102af400ea1fb971a980bd2ceb13
|
|
| MD5 |
dd5ff6b31a14314198a589500f3a6995
|
|
| BLAKE2b-256 |
549ead471c3eefaac50d48f92536b3d1ba779ea19bf956f483e66aad9db1c6f2
|