llm-imageops 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_imageops-1.0.0/PKG-INFO +421 -0
- llm_imageops-1.0.0/README.md +382 -0
- llm_imageops-1.0.0/imageops/__init__.py +98 -0
- llm_imageops-1.0.0/imageops/config.py +135 -0
- llm_imageops-1.0.0/imageops/core/__init__.py +11 -0
- llm_imageops-1.0.0/imageops/core/processor.py +179 -0
- llm_imageops-1.0.0/imageops/core/result.py +93 -0
- llm_imageops-1.0.0/imageops/exceptions.py +57 -0
- llm_imageops-1.0.0/imageops/operations/__init__.py +20 -0
- llm_imageops-1.0.0/imageops/operations/compress.py +126 -0
- llm_imageops-1.0.0/imageops/operations/encode.py +76 -0
- llm_imageops-1.0.0/imageops/operations/resize.py +227 -0
- llm_imageops-1.0.0/imageops/operations/slice.py +158 -0
- llm_imageops-1.0.0/imageops/providers/__init__.py +14 -0
- llm_imageops-1.0.0/imageops/providers/anthropic.py +53 -0
- llm_imageops-1.0.0/imageops/providers/base.py +34 -0
- llm_imageops-1.0.0/imageops/providers/registry.py +27 -0
- llm_imageops-1.0.0/imageops/strategies/__init__.py +9 -0
- llm_imageops-1.0.0/imageops/strategies/sequential.py +295 -0
- llm_imageops-1.0.0/imageops/utils/__init__.py +19 -0
- llm_imageops-1.0.0/imageops/utils/cleanup.py +122 -0
- llm_imageops-1.0.0/imageops/utils/download.py +116 -0
- llm_imageops-1.0.0/imageops/utils/logging.py +55 -0
- llm_imageops-1.0.0/imageops/utils/metadata.py +141 -0
- llm_imageops-1.0.0/imageops/utils/validation.py +119 -0
- llm_imageops-1.0.0/llm_imageops.egg-info/PKG-INFO +421 -0
- llm_imageops-1.0.0/llm_imageops.egg-info/SOURCES.txt +31 -0
- llm_imageops-1.0.0/llm_imageops.egg-info/dependency_links.txt +1 -0
- llm_imageops-1.0.0/llm_imageops.egg-info/requires.txt +15 -0
- llm_imageops-1.0.0/llm_imageops.egg-info/top_level.txt +1 -0
- llm_imageops-1.0.0/pyproject.toml +74 -0
- llm_imageops-1.0.0/setup.cfg +4 -0
- llm_imageops-1.0.0/setup.py +18 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-imageops
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Production-grade async image preprocessing for LLM APIs
|
|
5
|
+
Author: ImageOps Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/yourusername/imageops
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/yourusername/imageops
|
|
9
|
+
Project-URL: Issues, https://gitlab.com/yourusername/imageops/-/issues
|
|
10
|
+
Keywords: llm,image-processing,anthropic,async,ai,agents,computer-vision
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Framework :: AsyncIO
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: httpx>=0.28.0
|
|
25
|
+
Requires-Dist: aiofiles>=25.0.0
|
|
26
|
+
Requires-Dist: pydantic>=2.12.0
|
|
27
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
28
|
+
Requires-Dist: opencv-python-headless>=4.12.0
|
|
29
|
+
Requires-Dist: pillow>=12.0.0
|
|
30
|
+
Requires-Dist: numpy>=2.2.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
37
|
+
Requires-Dist: mypy>=1.5.0; extra == "dev"
|
|
38
|
+
Dynamic: requires-python
|
|
39
|
+
|
|
40
|
+
# ImageOps: Production-Grade Image Preprocessing for LLMs
|
|
41
|
+
|
|
42
|
+
**Simple, fast, and production-ready async image preprocessing for LLM APIs.**
|
|
43
|
+
|
|
44
|
+
> **PyPI Package**: `llm-imageops` (import as `imageops`)
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
## ๐ Features
|
|
49
|
+
|
|
50
|
+
- **Zero Configuration** - Works out of the box with sensible defaults
|
|
51
|
+
- **100% Async** - Built for modern async Python applications
|
|
52
|
+
- **Production Ready** - Comprehensive error handling, security, resource limits
|
|
53
|
+
- **Provider Agnostic** - Easy to add support for new LLM providers
|
|
54
|
+
- **Type Safe** - Full type hints with Pydantic validation
|
|
55
|
+
- **Edge Case Handling** - Animated GIFs, panoramas, EXIF orientation, alpha channels
|
|
56
|
+
- **Smart Optimization** - Only processes when necessary based on provider limits
|
|
57
|
+
- **Resource Safe** - Automatic cleanup with async context managers
|
|
58
|
+
|
|
59
|
+
## ๐ฆ Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install llm-imageops
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Import as:
|
|
66
|
+
```python
|
|
67
|
+
import imageops # Module name stays 'imageops'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or install from source:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/yourusername/imageops.git
|
|
74
|
+
cd imageops
|
|
75
|
+
pip install -e .
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## ๐ฏ Quick Start
|
|
79
|
+
|
|
80
|
+
### Simple One-Liner
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import imageops
|
|
84
|
+
import asyncio
|
|
85
|
+
|
|
86
|
+
async def main():
|
|
87
|
+
# Process image for Anthropic Claude
|
|
88
|
+
result = await imageops.process("image.jpg", provider="anthropic")
|
|
89
|
+
|
|
90
|
+
# Use in your LLM API call
|
|
91
|
+
messages = [
|
|
92
|
+
{
|
|
93
|
+
"role": "user",
|
|
94
|
+
"content": result.to_anthropic_format() + [
|
|
95
|
+
{"type": "text", "text": "What's in this image?"}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
asyncio.run(main())
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Process from URL
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
result = await imageops.process(
|
|
107
|
+
"https://example.com/image.jpg",
|
|
108
|
+
provider="anthropic"
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Get File Paths Instead of Base64
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
result = await imageops.process(
|
|
116
|
+
"large_image.jpg",
|
|
117
|
+
provider="anthropic",
|
|
118
|
+
output_format="file" # Returns processed file paths
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### With Progress Callback
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
async def progress_callback(step: str, progress: float):
|
|
126
|
+
print(f"{step}: {progress:.1%}")
|
|
127
|
+
|
|
128
|
+
result = await imageops.process(
|
|
129
|
+
"huge_image.jpg",
|
|
130
|
+
provider="anthropic",
|
|
131
|
+
progress_callback=progress_callback
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Batch Processing
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
results = await imageops.process_batch(
|
|
139
|
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
|
140
|
+
provider="anthropic"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for result in results:
|
|
144
|
+
print(f"Processed {result.slice_count} image(s)")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## ๐ง Advanced Usage
|
|
148
|
+
|
|
149
|
+
### Custom Configuration
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import imageops
|
|
153
|
+
|
|
154
|
+
config = imageops.ImageOpsConfig(
|
|
155
|
+
max_image_size_mb=50, # Reject images > 50MB
|
|
156
|
+
max_batch_size=20, # Limit batch processing
|
|
157
|
+
enable_logging=True,
|
|
158
|
+
log_level="DEBUG",
|
|
159
|
+
default_quality=85 # JPEG quality
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
async with imageops.ImageProcessor(provider="anthropic", config=config) as processor:
|
|
163
|
+
result1 = await processor.process("img1.jpg")
|
|
164
|
+
result2 = await processor.process("img2.jpg", timeout=60.0)
|
|
165
|
+
# Automatic cleanup
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Custom Provider
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from imageops.providers import BaseProvider, ProviderConfig, register_provider
|
|
172
|
+
|
|
173
|
+
class MyCustomProvider(BaseProvider):
|
|
174
|
+
name = "my_llm"
|
|
175
|
+
|
|
176
|
+
config = ProviderConfig(
|
|
177
|
+
max_width=4096,
|
|
178
|
+
max_height=4096,
|
|
179
|
+
max_size_mb=5.0,
|
|
180
|
+
max_base64_size_mb=10.0,
|
|
181
|
+
max_images_per_call=10,
|
|
182
|
+
supported_formats=["jpeg", "png"]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def needs_processing(self, metadata) -> bool:
|
|
186
|
+
return metadata.width > 4096 or metadata.size_mb > 5.0
|
|
187
|
+
|
|
188
|
+
def format_for_api(self, images):
|
|
189
|
+
# Return your provider's format
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
# Register it
|
|
193
|
+
register_provider("my_llm", MyCustomProvider())
|
|
194
|
+
|
|
195
|
+
# Use it
|
|
196
|
+
result = await imageops.process("image.jpg", provider="my_llm")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## ๐ What It Does
|
|
200
|
+
|
|
201
|
+
ImageOps automatically handles:
|
|
202
|
+
|
|
203
|
+
### Resizing
|
|
204
|
+
- Reduces width/height to fit provider limits
|
|
205
|
+
- Maintains aspect ratio
|
|
206
|
+
- Uses high-quality INTER_AREA interpolation
|
|
207
|
+
|
|
208
|
+
### Slicing
|
|
209
|
+
- Splits very tall images into vertical segments
|
|
210
|
+
- Splits very wide panoramas into horizontal slices
|
|
211
|
+
- Processes each segment independently
|
|
212
|
+
|
|
213
|
+
### Compression
|
|
214
|
+
- Iterative quality reduction (95 โ 10)
|
|
215
|
+
- Falls back to dimension reduction if needed
|
|
216
|
+
- Ensures file size stays under limits
|
|
217
|
+
|
|
218
|
+
### Edge Cases
|
|
219
|
+
- **Animated GIFs** - Extracts first frame
|
|
220
|
+
- **EXIF Orientation** - Auto-rotates images
|
|
221
|
+
- **Alpha Channels** - Converts RGBA โ RGB on white background
|
|
222
|
+
- **Wide Panoramas** - Horizontal slicing for 50000x1000 images
|
|
223
|
+
- **Corrupted Images** - Clear error messages
|
|
224
|
+
|
|
225
|
+
## ๐ Security
|
|
226
|
+
|
|
227
|
+
- **Path Traversal Protection** - Validates file paths
|
|
228
|
+
- **File Size Limits** - Rejects images > 100MB by default
|
|
229
|
+
- **Dimension Limits** - Maximum 100,000px per side
|
|
230
|
+
- **Format Whitelist** - Only allows safe formats
|
|
231
|
+
- **URL Validation** - Proper URL format checking
|
|
232
|
+
|
|
233
|
+
## ๐ Result Format
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
result = await imageops.process("image.jpg")
|
|
237
|
+
|
|
238
|
+
# Result attributes
|
|
239
|
+
result.success # bool
|
|
240
|
+
result.images # List[ImageOutput]
|
|
241
|
+
result.output_format # "base64" or "file"
|
|
242
|
+
result.was_sliced # bool
|
|
243
|
+
result.slice_count # int
|
|
244
|
+
result.processing_time_ms # float
|
|
245
|
+
result.provider # str
|
|
246
|
+
result.original_metadata # Dict
|
|
247
|
+
|
|
248
|
+
# Each image has THREE ways to access base64 + media type:
|
|
249
|
+
|
|
250
|
+
# 1. Separate fields (most flexible)
|
|
251
|
+
for img in result.images:
|
|
252
|
+
base64_data = img.data # Just the base64 string
|
|
253
|
+
media_type = img.media_type # e.g., "image/jpeg"
|
|
254
|
+
# Combine as needed: f"data:{media_type};base64,{base64_data}"
|
|
255
|
+
|
|
256
|
+
# 2. Data URI with prefix (convenience method)
|
|
257
|
+
for img in result.images:
|
|
258
|
+
data_uri = img.to_data_uri() # "data:image/jpeg;base64,<data>"
|
|
259
|
+
|
|
260
|
+
# 3. Anthropic API format (ready-to-use)
|
|
261
|
+
content = result.to_anthropic_format()
|
|
262
|
+
# Returns Anthropic-formatted content blocks with media_type included
|
|
263
|
+
|
|
264
|
+
# Other attributes
|
|
265
|
+
for img in result.images:
|
|
266
|
+
img.format # "base64" or "file"
|
|
267
|
+
img.width # int
|
|
268
|
+
img.height # int
|
|
269
|
+
img.size_bytes # int
|
|
270
|
+
img.was_compressed # bool
|
|
271
|
+
img.compression_quality # Optional[int]
|
|
272
|
+
|
|
273
|
+
# Convenience methods
|
|
274
|
+
result.to_anthropic_format() # Ready for Anthropic API
|
|
275
|
+
await result.cleanup() # Manual cleanup if needed
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## โ๏ธ Configuration Options
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
ImageOpsConfig(
|
|
282
|
+
# Resource Limits
|
|
283
|
+
max_image_size_mb=100.0, # Max input image size
|
|
284
|
+
max_dimension=100000, # Max width/height
|
|
285
|
+
max_batch_size=50, # Max images per batch
|
|
286
|
+
|
|
287
|
+
# Performance
|
|
288
|
+
max_concurrent_ops=5, # Concurrent operations
|
|
289
|
+
download_timeout=10.0, # Download timeout (seconds)
|
|
290
|
+
processing_timeout=300.0, # Processing timeout (seconds)
|
|
291
|
+
|
|
292
|
+
# Retry Logic
|
|
293
|
+
max_download_retries=3, # Download retry attempts
|
|
294
|
+
retry_backoff_factor=2.0, # Exponential backoff
|
|
295
|
+
|
|
296
|
+
# Compression
|
|
297
|
+
default_quality=90, # JPEG quality (1-100)
|
|
298
|
+
quality_step=5, # Quality reduction step
|
|
299
|
+
min_quality=10, # Minimum quality
|
|
300
|
+
|
|
301
|
+
# Logging
|
|
302
|
+
enable_logging=True,
|
|
303
|
+
log_level="INFO", # DEBUG, INFO, WARNING, ERROR
|
|
304
|
+
log_file=None, # Optional log file
|
|
305
|
+
|
|
306
|
+
# Security
|
|
307
|
+
validate_paths=True, # Path traversal protection
|
|
308
|
+
allowed_formats=[".jpg", ".jpeg", ".png", ".gif", ".webp"]
|
|
309
|
+
)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## ๐งช Testing
|
|
313
|
+
|
|
314
|
+
The package includes comprehensive tests:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Install dev dependencies
|
|
318
|
+
pip install -e ".[dev]"
|
|
319
|
+
|
|
320
|
+
# Run all tests
|
|
321
|
+
pytest
|
|
322
|
+
|
|
323
|
+
# Run with coverage
|
|
324
|
+
pytest --cov=imageops --cov-report=html
|
|
325
|
+
|
|
326
|
+
# Run specific test categories
|
|
327
|
+
pytest tests/test_operations.py
|
|
328
|
+
pytest tests/test_edge_cases.py
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## ๐ Troubleshooting
|
|
332
|
+
|
|
333
|
+
### Image Download Fails
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from imageops.exceptions import ImageDownloadError
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
result = await imageops.process("https://example.com/image.jpg")
|
|
340
|
+
except ImageDownloadError as e:
|
|
341
|
+
print(f"Download failed: {e}")
|
|
342
|
+
# Handle fallback
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Image Too Large
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
from imageops.exceptions import ImageTooLargeError
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
result = await imageops.process("huge.jpg")
|
|
352
|
+
except ImageTooLargeError as e:
|
|
353
|
+
print(f"Image exceeds limit: {e}")
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Processing Timeout
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
from imageops.exceptions import TimeoutError
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
result = await imageops.process("image.jpg", timeout=30.0)
|
|
363
|
+
except TimeoutError as e:
|
|
364
|
+
print(f"Processing took too long: {e}")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Enable Debug Logging
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
config = imageops.ImageOpsConfig(
|
|
371
|
+
enable_logging=True,
|
|
372
|
+
log_level="DEBUG",
|
|
373
|
+
log_file="imageops.log"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
processor = imageops.ImageProcessor(config=config)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## ๐๏ธ Architecture
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
imageops/
|
|
383
|
+
โโโ core/ # Main processor and result types
|
|
384
|
+
โโโ providers/ # Provider implementations
|
|
385
|
+
โโโ operations/ # Image operations (resize, slice, compress, encode)
|
|
386
|
+
โโโ strategies/ # Processing strategies
|
|
387
|
+
โโโ utils/ # Utilities (validation, cleanup, logging, metadata)
|
|
388
|
+
โโโ config.py # Configuration
|
|
389
|
+
โโโ exceptions.py # Exception hierarchy
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## ๐ค Contributing
|
|
393
|
+
|
|
394
|
+
Contributions are welcome! Please:
|
|
395
|
+
|
|
396
|
+
1. Fork the repository
|
|
397
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
398
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
399
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
400
|
+
5. Open a Pull Request
|
|
401
|
+
|
|
402
|
+
## ๐ License
|
|
403
|
+
|
|
404
|
+
MIT License - see LICENSE file for details.
|
|
405
|
+
|
|
406
|
+
## ๐ Acknowledgments
|
|
407
|
+
|
|
408
|
+
Built with best practices from:
|
|
409
|
+
- [httpx](https://www.python-httpx.org/) - Async HTTP patterns
|
|
410
|
+
- [aiofiles](https://github.com/Tinche/aiofiles) - Async file I/O
|
|
411
|
+
- [pydantic](https://pydantic-docs.helpmanual.io/) - Type validation
|
|
412
|
+
- [opencv-python](https://github.com/opencv/opencv-python) - Image processing
|
|
413
|
+
|
|
414
|
+
## ๐ง Support
|
|
415
|
+
|
|
416
|
+
- **Issues**: [GitHub Issues](https://github.com/yourusername/imageops/issues)
|
|
417
|
+
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/imageops/discussions)
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
Made with โค๏ธ for the LLM community
|