File size: 14,562 Bytes
0f31229
278a64b
0f31229
 
 
 
 
 
 
 
 
 
 
 
 
 
278a64b
 
0f31229
 
 
 
 
 
 
 
 
278a64b
0f31229
278a64b
0f31229
 
 
 
 
 
 
 
278a64b
 
 
 
0f31229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278a64b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f31229
278a64b
 
 
 
 
0f31229
278a64b
0f31229
 
278a64b
 
0f31229
278a64b
0f31229
 
278a64b
0f31229
 
 
 
 
 
 
 
278a64b
 
 
0f31229
 
 
 
 
 
 
278a64b
 
 
0f31229
 
 
 
 
 
 
 
 
278a64b
 
0f31229
 
 
 
 
 
 
 
 
278a64b
0f31229
278a64b
0f31229
 
 
 
 
 
 
278a64b
 
 
 
 
 
 
 
 
 
 
 
0f31229
 
278a64b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f31229
 
 
 
278a64b
0f31229
 
 
 
 
 
 
 
 
 
 
 
 
 
278a64b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f31229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278a64b
0f31229
 
278a64b
 
 
0f31229
 
278a64b
0f31229
 
278a64b
 
 
0f31229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
import io
from PIL import Image, ImageOps
from typing import Tuple, Optional
import base64
from ..storage import upload_fileobj, get_object_url

class ImageProcessingService:
    """Service for creating and managing multiple image resolutions"""
    
    @staticmethod
    def create_resized_image(
        image_content: bytes,
        filename: str,
        max_width: int,
        max_height: int,
        quality: int = 85,
        suffix: str = "",
        format: str = "JPEG"
    ) -> Tuple[bytes, str]:
        """
        Create a resized image from original content
        
        Args:
            image_content: Raw image bytes
            filename: Original filename (for extension detection)
            max_width: Maximum width for resized image
            max_height: Maximum height for resized image
            quality: Quality (1-100)
            suffix: Suffix to add to filename (e.g., "_thumb", "_detail")
            format: Output format ("JPEG" or "WEBP")
            
        Returns:
            Tuple of (resized_bytes, resized_filename)
        """
        try:
            # Open image from bytes
            image = Image.open(io.BytesIO(image_content))
            
            # Honor EXIF orientation
            image = ImageOps.exif_transpose(image)
            
            # Convert to RGB if necessary (for JPEG/WebP output)
            if image.mode in ('RGBA', 'LA', 'P'):
                # Create white background for transparent images
                background = Image.new('RGB', image.size, (255, 255, 255))
                if image.mode == 'P':
                    image = image.convert('RGBA')
                background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
                image = background
            elif image.mode != 'RGB':
                image = image.convert('RGB')
            
            # Calculate new dimensions maintaining aspect ratio
            width, height = image.size
            ratio = min(max_width / width, max_height / height)
            new_width = int(width * ratio)
            new_height = int(height * ratio)
            
            # Resize image
            image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
            
            # Save to bytes with optimized compression
            output = io.BytesIO()
            if format == "WEBP":
                image.save(output, format='WEBP', quality=quality, method=6, optimize=True)
            else:
                image.save(output, format='JPEG', quality=quality, progressive=True, optimize=True, subsampling=0)
            resized_bytes = output.getvalue()
            
            # Generate resized filename with correct extension
            name_parts = filename.rsplit('.', 1)
            base_name = name_parts[0] if len(name_parts) > 1 else filename
            extension = "webp" if format == "WEBP" else "jpg"
            resized_filename = f"{base_name}{suffix}.{extension}"
            
            return resized_bytes, resized_filename
            
        except Exception as e:
            print(f"Error creating resized image: {str(e)}")
            # Return original content as fallback
            return image_content, filename
    
    @staticmethod
    def create_resized_image_max_width(
        image_content: bytes,
        filename: str,
        max_width: int,
        quality: int = 85,
        suffix: str = "",
        format: str = "JPEG"
    ) -> Tuple[bytes, str]:
        """
        Create a resized image using max width only while keeping aspect ratio
        
        Args:
            image_content: Raw image bytes
            filename: Original filename (for extension detection)
            max_width: Maximum width for resized image
            quality: Quality (1-100)
            suffix: Suffix to add to filename (e.g., "_thumb", "_detail")
            format: Output format ("JPEG" or "WEBP")
            
        Returns:
            Tuple of (resized_bytes, resized_filename)
        """
        try:
            # Open image from bytes
            image = Image.open(io.BytesIO(image_content))
            
            # Honor EXIF orientation
            image = ImageOps.exif_transpose(image)
            
            # Convert to RGB if necessary (for JPEG/WebP output)
            if image.mode in ('RGBA', 'LA', 'P'):
                # Create white background for transparent images
                background = Image.new('RGB', image.size, (255, 255, 255))
                if image.mode == 'P':
                    image = image.convert('RGBA')
                background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
                image = background
            elif image.mode != 'RGB':
                image = image.convert('RGB')
            
            # Calculate new dimensions using max width only
            width, height = image.size
            if width <= max_width:
                # Image is already smaller than max_width, no need to resize
                new_width, new_height = width, height
            else:
                # Resize to max_width while keeping aspect ratio
                ratio = max_width / width
                new_width = int(width * ratio)
                new_height = int(height * ratio)
            
            # Resize image if needed
            if new_width != width or new_height != height:
                image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
            
            # Save to bytes with optimized compression
            output = io.BytesIO()
            if format == "WEBP":
                image.save(output, format='WEBP', quality=quality, method=6, optimize=True)
            else:
                image.save(output, format='JPEG', quality=quality, progressive=True, optimize=True, subsampling=0)
            resized_bytes = output.getvalue()
            
            # Generate resized filename with correct extension
            name_parts = filename.rsplit('.', 1)
            base_name = name_parts[0] if len(name_parts) > 1 else filename
            extension = "webp" if format == "WEBP" else "jpg"
            resized_filename = f"{base_name}{suffix}.{extension}"
            
            return resized_bytes, resized_filename
            
        except Exception as e:
            print(f"Error creating resized image: {str(e)}")
            # Return original content as fallback
            return image_content, filename
    
    @staticmethod
    def create_thumbnail(
        image_content: bytes,
        filename: str
    ) -> Optional[Tuple[bytes, str]]:
        """Create thumbnail (smallest version) - max width 300px, WebP format, 80% quality"""
        return ImageProcessingService.create_resized_image_max_width(
            image_content, filename, 300, 80, "_thumb", "WEBP"
        )
    
    @staticmethod
    def create_detail_image(
        image_content: bytes,
        filename: str
    ) -> Optional[Tuple[bytes, str]]:
        """Create detail version (medium quality) - max width 800px, WebP format, 85% quality"""
        return ImageProcessingService.create_resized_image_max_width(
            image_content, filename, 800, 85, "_detail", "WEBP"
        )
    
    @staticmethod
    def upload_resized_image(
        image_content: bytes,
        filename: str,
        max_width: int,
        max_height: int,
        quality: int = 85,
        suffix: str = "",
        format: str = "JPEG"
    ) -> Optional[Tuple[str, str]]:
        """
        Create and upload a resized image, returning the key and SHA256
        
        Args:
            image_content: Raw image bytes
            filename: Original filename
            max_width: Maximum width for resized image
            max_height: Maximum height for resized image
            quality: Quality (1-100)
            suffix: Suffix to add to filename
            format: Output format ("JPEG" or "WEBP")
            
        Returns:
            Tuple of (resized_key, resized_sha256) or None if failed
        """
        try:
            # Create resized image
            resized_bytes, resized_filename = ImageProcessingService.create_resized_image(
                image_content, filename, max_width, max_height, quality, suffix, format
            )
            
            # Determine content type based on format
            content_type = 'image/webp' if format == 'WEBP' else 'image/jpeg'
            
            # Upload resized image
            resized_key = upload_fileobj(
                io.BytesIO(resized_bytes),
                resized_filename,
                content_type=content_type,
                cache_control='public, max-age=31536000, immutable'
            )
            
            # Calculate SHA256 hash of resized image
            import hashlib
            resized_sha256 = hashlib.sha256(resized_bytes).hexdigest()
            
            # Return key and SHA256
            return resized_key, resized_sha256
            
        except Exception as e:
            print(f"Error uploading resized image: {str(e)}")
            return None
    
    @staticmethod
    def upload_resized_image_max_width(
        image_content: bytes,
        filename: str,
        max_width: int,
        quality: int = 85,
        suffix: str = "",
        format: str = "JPEG"
    ) -> Optional[Tuple[str, str]]:
        """
        Create and upload a resized image using max width only, returning the key and SHA256
        
        Args:
            image_content: Raw image bytes
            filename: Original filename
            max_width: Maximum width for resized image
            quality: Quality (1-100)
            suffix: Suffix to add to filename
            format: Output format ("JPEG" or "WEBP")
            
        Returns:
            Tuple of (resized_key, resized_sha256) or None if failed
        """
        try:
            # Create resized image
            resized_bytes, resized_filename = ImageProcessingService.create_resized_image_max_width(
                image_content, filename, max_width, quality, suffix, format
            )
            
            # Determine content type based on format
            content_type = 'image/webp' if format == 'WEBP' else 'image/jpeg'
            
            # Upload resized image
            resized_key = upload_fileobj(
                io.BytesIO(resized_bytes),
                resized_filename,
                content_type=content_type,
                cache_control='public, max-age=31536000, immutable'
            )
            
            # Calculate SHA256 hash of resized image
            import hashlib
            resized_sha256 = hashlib.sha256(resized_bytes).hexdigest()
            
            # Return key and SHA256
            return resized_key, resized_sha256
            
        except Exception as e:
            print(f"Error uploading resized image: {str(e)}")
            return None
    
    @staticmethod
    def upload_image_bytes(
        image_bytes: bytes,
        filename: str,
        format: str = "WEBP"
    ) -> Optional[Tuple[str, str]]:
        """
        Upload pre-created image bytes without re-processing
        
        Args:
            image_bytes: Pre-created image bytes
            filename: Filename for the image
            format: Image format ("JPEG" or "WEBP")
            
        Returns:
            Tuple of (uploaded_key, sha256) or None if failed
        """
        try:
            # Determine content type based on format
            content_type = 'image/webp' if format == 'WEBP' else 'image/jpeg'
            
            # Upload image bytes
            uploaded_key = upload_fileobj(
                io.BytesIO(image_bytes),
                filename,
                content_type=content_type,
                cache_control='public, max-age=31536000, immutable'
            )
            
            # Calculate SHA256 hash
            import hashlib
            sha256 = hashlib.sha256(image_bytes).hexdigest()
            
            # Return key and SHA256
            return uploaded_key, sha256
            
        except Exception as e:
            print(f"Error uploading image bytes: {str(e)}")
            return None
    
    @staticmethod
    def process_all_resolutions(
        image_content: bytes,
        filename: str
    ) -> Tuple[Optional[Tuple[str, str]], Optional[Tuple[str, str]]]:
        """
        Create and upload both thumbnail and detail versions
        
        Args:
            image_content: Raw image bytes
            filename: Original filename
            
        Returns:
            Tuple of (thumbnail_result, detail_result) where each result is (key, sha256) or None
        """
        thumbnail_result = None
        detail_result = None
        
        try:
            # Create thumbnail (WebP format, max width 300px, 80% quality)
            thumbnail_bytes, thumbnail_filename = ImageProcessingService.create_thumbnail(image_content, filename)
            if thumbnail_bytes and thumbnail_filename:
                # Upload the pre-created thumbnail bytes without re-processing
                thumbnail_result = ImageProcessingService.upload_image_bytes(
                    thumbnail_bytes, thumbnail_filename, "WEBP"
                )
            
            # Create detail version (WebP format, max width 800px, 85% quality)
            detail_bytes, detail_filename = ImageProcessingService.create_detail_image(image_content, filename)
            if detail_bytes and detail_filename:
                # Upload the pre-created detail bytes without re-processing
                detail_result = ImageProcessingService.upload_image_bytes(
                    detail_bytes, detail_filename, "WEBP"
                )
                
        except Exception as e:
            print(f"Error processing image resolutions: {str(e)}")
        
        return thumbnail_result, detail_result
    
    @staticmethod
    def get_thumbnail_url(image_url: str, fallback_url: Optional[str] = None) -> str:
        """
        Get thumbnail URL from image URL or return fallback
        
        Args:
            image_url: Original image URL
            fallback_url: Fallback URL if thumbnail not available
            
        Returns:
            Thumbnail URL or fallback
        """
        # For now, return the original URL as fallback
        # In a real implementation, you might have a mapping or pattern
        return fallback_url or image_url