File size: 2,630 Bytes
df4a21a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""
Image processing utilities.
"""

from io import BytesIO
from typing import Optional, Tuple

from PIL import Image

from app.core.errors import ImageProcessingError
from app.core.logging import get_logger

logger = get_logger(__name__)


def load_image_from_bytes(image_bytes: bytes) -> Image.Image:
    """
    Load a PIL Image from raw bytes.
    
    Args:
        image_bytes: Raw image bytes
        
    Returns:
        PIL Image object
        
    Raises:
        ImageProcessingError: If image cannot be decoded
    """
    try:
        image = Image.open(BytesIO(image_bytes))
        # Convert to RGB if necessary (handles RGBA, grayscale, etc.)
        if image.mode != "RGB":
            image = image.convert("RGB")
        return image
    except Exception as e:
        logger.error(f"Failed to decode image: {e}")
        raise ImageProcessingError(
            message="Failed to decode image",
            details={"error": str(e)}
        )


def validate_image_bytes(image_bytes: bytes) -> bool:
    """
    Validate that bytes represent a valid image.
    
    Args:
        image_bytes: Raw image bytes
        
    Returns:
        True if valid image, False otherwise
    """
    try:
        image = Image.open(BytesIO(image_bytes))
        image.verify()
        return True
    except Exception:
        return False


def get_image_info(image: Image.Image) -> dict:
    """
    Get basic information about an image.
    
    Args:
        image: PIL Image object
        
    Returns:
        Dictionary with image info
    """
    return {
        "width": image.width,
        "height": image.height,
        "mode": image.mode,
        "format": image.format
    }


def resize_image(
    image: Image.Image,
    size: Tuple[int, int],
    resample: int = Image.Resampling.LANCZOS
) -> Image.Image:
    """
    Resize image to specified size.
    
    Args:
        image: PIL Image object
        size: Target (width, height)
        resample: Resampling filter
        
    Returns:
        Resized PIL Image
    """
    return image.resize(size, resample=resample)


def image_to_bytes(
    image: Image.Image,
    format: str = "PNG",
    quality: int = 95
) -> bytes:
    """
    Convert PIL Image to bytes.
    
    Args:
        image: PIL Image object
        format: Output format (PNG, JPEG, etc.)
        quality: JPEG quality (1-95)
        
    Returns:
        Image as bytes
    """
    buffer = BytesIO()
    if format.upper() == "JPEG":
        image.save(buffer, format=format, quality=quality)
    else:
        image.save(buffer, format=format)
    return buffer.getvalue()