llm-imageops 1.0.0__py3-none-any.whl
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.
- imageops/__init__.py +98 -0
- imageops/config.py +135 -0
- imageops/core/__init__.py +11 -0
- imageops/core/processor.py +179 -0
- imageops/core/result.py +93 -0
- imageops/exceptions.py +57 -0
- imageops/operations/__init__.py +20 -0
- imageops/operations/compress.py +126 -0
- imageops/operations/encode.py +76 -0
- imageops/operations/resize.py +227 -0
- imageops/operations/slice.py +158 -0
- imageops/providers/__init__.py +14 -0
- imageops/providers/anthropic.py +53 -0
- imageops/providers/base.py +34 -0
- imageops/providers/registry.py +27 -0
- imageops/strategies/__init__.py +9 -0
- imageops/strategies/sequential.py +295 -0
- imageops/utils/__init__.py +19 -0
- imageops/utils/cleanup.py +122 -0
- imageops/utils/download.py +116 -0
- imageops/utils/logging.py +55 -0
- imageops/utils/metadata.py +141 -0
- imageops/utils/validation.py +119 -0
- llm_imageops-1.0.0.dist-info/METADATA +421 -0
- llm_imageops-1.0.0.dist-info/RECORD +27 -0
- llm_imageops-1.0.0.dist-info/WHEEL +5 -0
- llm_imageops-1.0.0.dist-info/top_level.txt +1 -0
imageops/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""ImageOps: Production-grade image preprocessing for LLMs."""
|
|
2
|
+
|
|
3
|
+
from .config import ImageOpsConfig
|
|
4
|
+
from .core import ImageProcessor, ProcessingResult, ImageOutput
|
|
5
|
+
from .exceptions import *
|
|
6
|
+
from .providers import get_provider, register_provider
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
|
|
10
|
+
# Simple API
|
|
11
|
+
async def process(
|
|
12
|
+
image_source: str,
|
|
13
|
+
provider: str = "anthropic",
|
|
14
|
+
output_format: str = "base64",
|
|
15
|
+
progress_callback=None,
|
|
16
|
+
**kwargs
|
|
17
|
+
):
|
|
18
|
+
"""
|
|
19
|
+
Simple one-line API to process images.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
image_source: File path or URL
|
|
23
|
+
provider: Provider name (default: "anthropic")
|
|
24
|
+
output_format: "base64" or "file" (default: "base64")
|
|
25
|
+
progress_callback: Optional async progress callback
|
|
26
|
+
**kwargs: Additional arguments
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ProcessingResult object
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> result = await imageops.process("image.jpg", provider="anthropic")
|
|
33
|
+
>>> print(result.images[0].data[:50]) # First 50 chars of base64
|
|
34
|
+
"""
|
|
35
|
+
async with ImageProcessor(provider=provider) as processor:
|
|
36
|
+
return await processor.process(
|
|
37
|
+
image_source,
|
|
38
|
+
output_format=output_format,
|
|
39
|
+
progress_callback=progress_callback,
|
|
40
|
+
**kwargs
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def process_batch(
|
|
45
|
+
image_sources: list,
|
|
46
|
+
provider: str = "anthropic",
|
|
47
|
+
output_format: str = "base64",
|
|
48
|
+
**kwargs
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Process multiple images concurrently.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
image_sources: List of file paths or URLs
|
|
55
|
+
provider: Provider name (default: "anthropic")
|
|
56
|
+
output_format: "base64" or "file" (default: "base64")
|
|
57
|
+
**kwargs: Additional arguments
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of ProcessingResult objects
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> results = await imageops.process_batch(
|
|
64
|
+
... ["img1.jpg", "img2.jpg"],
|
|
65
|
+
... provider="anthropic"
|
|
66
|
+
... )
|
|
67
|
+
"""
|
|
68
|
+
async with ImageProcessor(provider=provider) as processor:
|
|
69
|
+
return await processor.process_batch(
|
|
70
|
+
image_sources,
|
|
71
|
+
output_format=output_format,
|
|
72
|
+
**kwargs
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"ImageOpsConfig",
|
|
78
|
+
"ImageProcessor",
|
|
79
|
+
"ProcessingResult",
|
|
80
|
+
"ImageOutput",
|
|
81
|
+
"process",
|
|
82
|
+
"process_batch",
|
|
83
|
+
"get_provider",
|
|
84
|
+
"register_provider",
|
|
85
|
+
# Exceptions
|
|
86
|
+
"ImageOpsError",
|
|
87
|
+
"ImageDownloadError",
|
|
88
|
+
"ImageValidationError",
|
|
89
|
+
"ImageProcessingError",
|
|
90
|
+
"ImageCorruptedError",
|
|
91
|
+
"ImageTooLargeError",
|
|
92
|
+
"UnsupportedFormatError",
|
|
93
|
+
"CompressionFailedError",
|
|
94
|
+
"ProviderNotFoundError",
|
|
95
|
+
"TimeoutError",
|
|
96
|
+
"ResourceLimitError",
|
|
97
|
+
]
|
|
98
|
+
|
imageops/config.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Configuration module with resource limits and security settings."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from pydantic import Field, validator
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from pydantic_settings import BaseSettings
|
|
8
|
+
except ImportError:
|
|
9
|
+
try:
|
|
10
|
+
from pydantic import BaseSettings
|
|
11
|
+
except ImportError:
|
|
12
|
+
raise ImportError(
|
|
13
|
+
"pydantic-settings is required. Install with: pip install pydantic-settings"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ImageOpsConfig(BaseSettings):
|
|
18
|
+
"""Global configuration with resource limits and security."""
|
|
19
|
+
|
|
20
|
+
# Resource Limits
|
|
21
|
+
max_image_size_mb: float = Field(
|
|
22
|
+
100.0,
|
|
23
|
+
gt=0,
|
|
24
|
+
description="Maximum input image size in MB"
|
|
25
|
+
)
|
|
26
|
+
max_dimension: int = Field(
|
|
27
|
+
100000,
|
|
28
|
+
gt=0,
|
|
29
|
+
description="Maximum width or height in pixels"
|
|
30
|
+
)
|
|
31
|
+
max_batch_size: int = Field(
|
|
32
|
+
50,
|
|
33
|
+
ge=1,
|
|
34
|
+
le=1000,
|
|
35
|
+
description="Maximum number of images in batch"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Temp Management
|
|
39
|
+
temp_dir_pattern: str = Field(
|
|
40
|
+
"imageops_{thread_id}",
|
|
41
|
+
description="Pattern for temp directory naming"
|
|
42
|
+
)
|
|
43
|
+
auto_cleanup: bool = Field(
|
|
44
|
+
True,
|
|
45
|
+
description="Auto-cleanup temp files"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Performance
|
|
49
|
+
max_concurrent_ops: int = Field(
|
|
50
|
+
5,
|
|
51
|
+
ge=1,
|
|
52
|
+
le=20,
|
|
53
|
+
description="Max concurrent operations"
|
|
54
|
+
)
|
|
55
|
+
download_timeout: float = Field(
|
|
56
|
+
10.0,
|
|
57
|
+
gt=0,
|
|
58
|
+
description="Download timeout in seconds"
|
|
59
|
+
)
|
|
60
|
+
processing_timeout: float = Field(
|
|
61
|
+
300.0,
|
|
62
|
+
gt=0,
|
|
63
|
+
description="Max processing time per image in seconds"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Retry Logic
|
|
67
|
+
max_download_retries: int = Field(
|
|
68
|
+
3,
|
|
69
|
+
ge=0,
|
|
70
|
+
le=10,
|
|
71
|
+
description="Max download retries"
|
|
72
|
+
)
|
|
73
|
+
retry_backoff_factor: float = Field(
|
|
74
|
+
2.0,
|
|
75
|
+
ge=1.0,
|
|
76
|
+
description="Exponential backoff factor"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Compression
|
|
80
|
+
default_quality: int = Field(
|
|
81
|
+
90,
|
|
82
|
+
ge=1,
|
|
83
|
+
le=100,
|
|
84
|
+
description="Default JPEG quality"
|
|
85
|
+
)
|
|
86
|
+
quality_step: int = Field(
|
|
87
|
+
5,
|
|
88
|
+
ge=1,
|
|
89
|
+
le=20,
|
|
90
|
+
description="Quality reduction step"
|
|
91
|
+
)
|
|
92
|
+
min_quality: int = Field(
|
|
93
|
+
10,
|
|
94
|
+
ge=1,
|
|
95
|
+
le=100,
|
|
96
|
+
description="Minimum JPEG quality"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Logging
|
|
100
|
+
enable_logging: bool = Field(
|
|
101
|
+
True,
|
|
102
|
+
description="Enable logging"
|
|
103
|
+
)
|
|
104
|
+
log_level: str = Field(
|
|
105
|
+
"INFO",
|
|
106
|
+
description="Logging level"
|
|
107
|
+
)
|
|
108
|
+
log_file: Optional[str] = Field(
|
|
109
|
+
None,
|
|
110
|
+
description="Log file path"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Security
|
|
114
|
+
validate_paths: bool = Field(
|
|
115
|
+
True,
|
|
116
|
+
description="Validate file paths against traversal attacks"
|
|
117
|
+
)
|
|
118
|
+
allowed_formats: List[str] = Field(
|
|
119
|
+
default_factory=lambda: [".jpg", ".jpeg", ".png", ".gif", ".webp"],
|
|
120
|
+
description="Allowed image formats"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@validator("log_level")
|
|
124
|
+
def validate_log_level(cls, v):
|
|
125
|
+
"""Validate log level."""
|
|
126
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
127
|
+
v_upper = v.upper()
|
|
128
|
+
if v_upper not in valid_levels:
|
|
129
|
+
raise ValueError(f"log_level must be one of {valid_levels}")
|
|
130
|
+
return v_upper
|
|
131
|
+
|
|
132
|
+
class Config:
|
|
133
|
+
env_prefix = "IMAGEOPS_"
|
|
134
|
+
case_sensitive = False
|
|
135
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Main ImageProcessor orchestrator."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import List, Literal, Optional
|
|
6
|
+
from ..config import ImageOpsConfig
|
|
7
|
+
from ..providers import get_provider
|
|
8
|
+
from ..utils import setup_logging, InputValidator, download_if_url, CleanupManager
|
|
9
|
+
from ..strategies import process_sequential, ProgressCallback
|
|
10
|
+
from ..exceptions import TimeoutError as ImageOpsTimeoutError
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("imageops")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ImageProcessor:
|
|
16
|
+
"""Main image processor with context manager support."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
provider: str = "anthropic",
|
|
21
|
+
config: Optional[ImageOpsConfig] = None
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize ImageProcessor.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
provider: Provider name (e.g., "anthropic")
|
|
28
|
+
config: Optional configuration
|
|
29
|
+
"""
|
|
30
|
+
self.provider_name = provider
|
|
31
|
+
self.config = config or ImageOpsConfig()
|
|
32
|
+
self.provider = get_provider(provider)
|
|
33
|
+
self.cleanup_manager = CleanupManager()
|
|
34
|
+
self._setup_logging()
|
|
35
|
+
|
|
36
|
+
def _setup_logging(self):
|
|
37
|
+
"""Setup logging based on config."""
|
|
38
|
+
if self.config.enable_logging:
|
|
39
|
+
setup_logging(
|
|
40
|
+
level=self.config.log_level,
|
|
41
|
+
log_file=self.config.log_file
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def __aenter__(self):
|
|
45
|
+
"""Enter async context."""
|
|
46
|
+
logger.info(f"ImageProcessor started (provider: {self.provider_name})")
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
50
|
+
"""Exit async context - cleanup all temp files."""
|
|
51
|
+
logger.info("ImageProcessor cleanup started")
|
|
52
|
+
await self.cleanup_manager.cleanup_all(ignore_errors=True)
|
|
53
|
+
logger.info("ImageProcessor cleanup complete")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
async def process(
|
|
57
|
+
self,
|
|
58
|
+
image_source: str,
|
|
59
|
+
output_format: Literal["base64", "file"] = "base64",
|
|
60
|
+
progress_callback: Optional[ProgressCallback] = None,
|
|
61
|
+
timeout: Optional[float] = None,
|
|
62
|
+
thread_id: Optional[str] = None,
|
|
63
|
+
quality: int = 90,
|
|
64
|
+
**kwargs
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Process single image with timeout.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
image_source: File path or URL
|
|
71
|
+
output_format: "base64" or "file"
|
|
72
|
+
progress_callback: Optional progress callback
|
|
73
|
+
timeout: Processing timeout in seconds
|
|
74
|
+
thread_id: Optional thread ID for temp directories
|
|
75
|
+
quality: JPEG quality (1-100)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ProcessingResult object
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
TimeoutError: If processing exceeds timeout
|
|
82
|
+
"""
|
|
83
|
+
timeout = timeout or self.config.processing_timeout
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
result = await asyncio.wait_for(
|
|
87
|
+
self._process_internal(
|
|
88
|
+
image_source,
|
|
89
|
+
output_format,
|
|
90
|
+
progress_callback,
|
|
91
|
+
thread_id,
|
|
92
|
+
quality
|
|
93
|
+
),
|
|
94
|
+
timeout=timeout
|
|
95
|
+
)
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
except asyncio.TimeoutError:
|
|
99
|
+
raise ImageOpsTimeoutError(
|
|
100
|
+
f"Processing timeout after {timeout}s: {image_source}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def _process_internal(
|
|
104
|
+
self,
|
|
105
|
+
image_source: str,
|
|
106
|
+
output_format: Literal["base64", "file"],
|
|
107
|
+
progress_callback: Optional[ProgressCallback],
|
|
108
|
+
thread_id: Optional[str],
|
|
109
|
+
quality: int
|
|
110
|
+
):
|
|
111
|
+
"""Internal processing logic."""
|
|
112
|
+
# Download if URL
|
|
113
|
+
local_path, is_temp = await download_if_url(
|
|
114
|
+
image_source,
|
|
115
|
+
timeout=self.config.download_timeout,
|
|
116
|
+
max_retries=self.config.max_download_retries,
|
|
117
|
+
backoff_factor=self.config.retry_backoff_factor
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if is_temp:
|
|
121
|
+
self.cleanup_manager.track_file(local_path)
|
|
122
|
+
|
|
123
|
+
# Validate file path
|
|
124
|
+
validated_path = InputValidator.validate_file_path(
|
|
125
|
+
local_path,
|
|
126
|
+
max_size_mb=self.config.max_image_size_mb
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Process using sequential strategy
|
|
130
|
+
result = await process_sequential(
|
|
131
|
+
validated_path,
|
|
132
|
+
self.provider,
|
|
133
|
+
output_format,
|
|
134
|
+
thread_id,
|
|
135
|
+
progress_callback,
|
|
136
|
+
quality
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
async def process_batch(
|
|
142
|
+
self,
|
|
143
|
+
image_sources: List[str],
|
|
144
|
+
output_format: Literal["base64", "file"] = "base64",
|
|
145
|
+
max_concurrent: Optional[int] = None,
|
|
146
|
+
**kwargs
|
|
147
|
+
):
|
|
148
|
+
"""
|
|
149
|
+
Process multiple images concurrently.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
image_sources: List of file paths or URLs
|
|
153
|
+
output_format: "base64" or "file"
|
|
154
|
+
max_concurrent: Max concurrent operations
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of ProcessingResult objects
|
|
158
|
+
"""
|
|
159
|
+
# Validate batch size
|
|
160
|
+
if len(image_sources) > self.config.max_batch_size:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"Batch size {len(image_sources)} exceeds limit "
|
|
163
|
+
f"of {self.config.max_batch_size}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
max_concurrent = max_concurrent or self.config.max_concurrent_ops
|
|
167
|
+
semaphore = asyncio.Semaphore(max_concurrent)
|
|
168
|
+
|
|
169
|
+
async def process_one(source: str):
|
|
170
|
+
async with semaphore:
|
|
171
|
+
return await self.process(source, output_format, **kwargs)
|
|
172
|
+
|
|
173
|
+
results = await asyncio.gather(
|
|
174
|
+
*[process_one(source) for source in image_sources],
|
|
175
|
+
return_exceptions=True
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return results
|
|
179
|
+
|
imageops/core/result.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Result types and data classes."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Dict, List, Literal, Optional
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ImageOutput(BaseModel):
|
|
9
|
+
"""Single processed image output."""
|
|
10
|
+
|
|
11
|
+
# Based on output_format parameter
|
|
12
|
+
data: str = Field(..., description="base64 string OR file path")
|
|
13
|
+
format: Literal["base64", "file"] = Field(..., description="Output format")
|
|
14
|
+
|
|
15
|
+
# Metadata
|
|
16
|
+
media_type: str = Field(..., description="image/jpeg, image/png, etc.")
|
|
17
|
+
width: int = Field(..., gt=0, description="Image width in pixels")
|
|
18
|
+
height: int = Field(..., gt=0, description="Image height in pixels")
|
|
19
|
+
size_bytes: int = Field(..., ge=0, description="Image size in bytes")
|
|
20
|
+
|
|
21
|
+
# Processing info
|
|
22
|
+
was_resized: bool = Field(False, description="Whether image was resized")
|
|
23
|
+
was_compressed: bool = Field(False, description="Whether image was compressed")
|
|
24
|
+
was_sliced: bool = Field(False, description="Whether image was sliced")
|
|
25
|
+
compression_quality: Optional[int] = Field(None, ge=1, le=100, description="Compression quality used")
|
|
26
|
+
|
|
27
|
+
def to_data_uri(self) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Convert to data URI format with media type prefix.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Data URI string like: data:image/jpeg;base64,<base64_data>
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> img = ImageOutput(data="abc123...", format="base64", media_type="image/jpeg", ...)
|
|
36
|
+
>>> img.to_data_uri()
|
|
37
|
+
'data:image/jpeg;base64,abc123...'
|
|
38
|
+
"""
|
|
39
|
+
if self.format != "base64":
|
|
40
|
+
raise ValueError("to_data_uri() only works with base64 format")
|
|
41
|
+
|
|
42
|
+
return f"data:{self.media_type};base64,{self.data}"
|
|
43
|
+
|
|
44
|
+
class Config:
|
|
45
|
+
frozen = True # Immutable
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ProcessingResult(BaseModel):
|
|
49
|
+
"""Complete processing result."""
|
|
50
|
+
|
|
51
|
+
success: bool = Field(..., description="Processing success status")
|
|
52
|
+
|
|
53
|
+
# Outputs
|
|
54
|
+
images: List[ImageOutput] = Field(..., description="Single image OR multiple if sliced")
|
|
55
|
+
output_format: Literal["base64", "file"] = Field(..., description="Output format")
|
|
56
|
+
|
|
57
|
+
# Original info
|
|
58
|
+
original_path: str = Field(..., description="Original input path")
|
|
59
|
+
original_metadata: Dict = Field(..., description="Original image metadata")
|
|
60
|
+
|
|
61
|
+
# Processing stats
|
|
62
|
+
provider: str = Field(..., description="Provider name")
|
|
63
|
+
strategy_used: str = Field(..., description="Processing strategy used")
|
|
64
|
+
processing_time_ms: float = Field(..., ge=0, description="Processing time in milliseconds")
|
|
65
|
+
was_sliced: bool = Field(..., description="Whether image was sliced")
|
|
66
|
+
slice_count: int = Field(..., ge=1, description="Number of slices")
|
|
67
|
+
created_at: datetime = Field(default_factory=datetime.utcnow, description="Result creation timestamp")
|
|
68
|
+
|
|
69
|
+
def to_anthropic_format(self) -> List[Dict]:
|
|
70
|
+
"""
|
|
71
|
+
Convert to Anthropic message content format.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of formatted image content dicts
|
|
75
|
+
"""
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
"type": "image",
|
|
79
|
+
"source": {
|
|
80
|
+
"type": "base64" if img.format == "base64" else "url",
|
|
81
|
+
"media_type": img.media_type,
|
|
82
|
+
"data": img.data if img.format == "base64" else None
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for img in self.images
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
async def cleanup(self):
|
|
89
|
+
"""Clean up temp files if output_format='file'."""
|
|
90
|
+
if self.output_format == "file":
|
|
91
|
+
from ..utils.cleanup import cleanup_files
|
|
92
|
+
await cleanup_files([img.data for img in self.images])
|
|
93
|
+
|
imageops/exceptions.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Comprehensive exception hierarchy for imageops."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ImageOpsError(Exception):
|
|
5
|
+
"""Base exception for all imageops errors."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ImageDownloadError(ImageOpsError):
|
|
10
|
+
"""Failed to download image from URL."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ImageValidationError(ImageOpsError):
|
|
15
|
+
"""Image validation failed (format, size, etc.)."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ImageProcessingError(ImageOpsError):
|
|
20
|
+
"""Error during image processing operations."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ImageCorruptedError(ImageOpsError):
|
|
25
|
+
"""Image file is corrupted or unreadable."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ImageTooLargeError(ImageOpsError):
|
|
30
|
+
"""Image exceeds size or dimension limits."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UnsupportedFormatError(ImageOpsError):
|
|
35
|
+
"""Image format is not supported."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CompressionFailedError(ImageProcessingError):
|
|
40
|
+
"""Cannot compress image to required size."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProviderNotFoundError(ImageOpsError):
|
|
45
|
+
"""Unknown or unsupported provider."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TimeoutError(ImageOpsError):
|
|
50
|
+
"""Operation timed out."""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ResourceLimitError(ImageOpsError):
|
|
55
|
+
"""Resource limit exceeded (memory, file handles, etc.)."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Operations module."""
|
|
2
|
+
|
|
3
|
+
from .compress import compress_to_size
|
|
4
|
+
from .encode import encode_to_base64, get_image_with_media_type
|
|
5
|
+
from .resize import resize_width, resize_height, fix_exif_orientation, handle_alpha_channel
|
|
6
|
+
from .slice import slice_by_height, handle_animated_gif, handle_wide_panorama
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"compress_to_size",
|
|
10
|
+
"encode_to_base64",
|
|
11
|
+
"get_image_with_media_type",
|
|
12
|
+
"resize_width",
|
|
13
|
+
"resize_height",
|
|
14
|
+
"fix_exif_orientation",
|
|
15
|
+
"handle_alpha_channel",
|
|
16
|
+
"slice_by_height",
|
|
17
|
+
"handle_animated_gif",
|
|
18
|
+
"handle_wide_panorama",
|
|
19
|
+
]
|
|
20
|
+
|