deckbuilder 1.0.0b1__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.
@@ -0,0 +1,280 @@
1
+ """
2
+ PlaceKittenIntegration - Bridge between PlaceKitten library and Deckbuilder engine.
3
+
4
+ This module provides the PlaceKittenIntegration class that generates professional
5
+ fallback images using PlaceKitten when user-provided images are missing or invalid.
6
+ """
7
+
8
+ from typing import Dict, Optional, Tuple
9
+
10
+ from .image_handler import ImageHandler
11
+
12
+ try:
13
+ from placekitten import PlaceKitten
14
+
15
+ PLACEKITTEN_AVAILABLE = True
16
+ except ImportError:
17
+ PLACEKITTEN_AVAILABLE = False
18
+ print("Warning: PlaceKitten library not available. Image fallbacks will be disabled.")
19
+
20
+
21
+ class PlaceKittenIntegration:
22
+ """
23
+ Bridge between PlaceKitten library and Deckbuilder engine.
24
+
25
+ Generates professional fallback images with business-appropriate styling
26
+ when user-provided images are missing or invalid.
27
+ """
28
+
29
+ def __init__(self, image_handler: ImageHandler):
30
+ """
31
+ Initialize PlaceKittenIntegration with image handler.
32
+
33
+ Args:
34
+ image_handler: ImageHandler instance for caching and processing
35
+ """
36
+ self.image_handler = image_handler
37
+
38
+ # Initialize PlaceKitten if available
39
+ if PLACEKITTEN_AVAILABLE:
40
+ self.pk = PlaceKitten()
41
+ else:
42
+ self.pk = None
43
+
44
+ # Professional styling configuration
45
+ self.professional_config = self._get_professional_styling()
46
+
47
+ def is_available(self) -> bool:
48
+ """
49
+ Check if PlaceKitten integration is available.
50
+
51
+ Returns:
52
+ bool: True if PlaceKitten library is available and ready
53
+ """
54
+ return PLACEKITTEN_AVAILABLE and self.pk is not None
55
+
56
+ def generate_fallback(
57
+ self, dimensions: Tuple[int, int], context: Optional[Dict] = None
58
+ ) -> Optional[str]:
59
+ """
60
+ Generate professional fallback image with business-appropriate styling.
61
+
62
+ Args:
63
+ dimensions: Target (width, height) for the image
64
+ context: Optional context information for consistent generation
65
+
66
+ Returns:
67
+ str: Path to generated fallback image, or None if generation failed
68
+ """
69
+ if not self.is_available():
70
+ return None
71
+
72
+ try:
73
+ width, height = dimensions
74
+
75
+ # Generate cache key for fallback image
76
+ cache_key = self._generate_fallback_cache_key(dimensions, context)
77
+ cached_path = self.image_handler._get_cached_image(cache_key)
78
+
79
+ if cached_path:
80
+ return cached_path
81
+
82
+ # Generate new fallback image
83
+ return self._create_fallback_image(width, height, cache_key, context)
84
+
85
+ except Exception as e:
86
+ print(f"Warning: PlaceKitten fallback generation failed: {e}")
87
+ return None
88
+
89
+ def _create_fallback_image(
90
+ self, width: int, height: int, cache_key: str, context: Optional[Dict] = None
91
+ ) -> Optional[str]:
92
+ """
93
+ Create new fallback image with professional styling.
94
+
95
+ Args:
96
+ width: Target width
97
+ height: Target height
98
+ cache_key: Cache key for storing result
99
+ context: Optional context for generation
100
+
101
+ Returns:
102
+ str: Path to generated image, or None if failed
103
+ """
104
+ try:
105
+ # Select consistent image based on context
106
+ image_id = self._select_image_id(context)
107
+
108
+ # Generate base image
109
+ processor = self.pk.generate(image_id=image_id)
110
+
111
+ # Apply professional styling pipeline
112
+ styled_processor = self._apply_professional_styling(processor, width, height)
113
+
114
+ # Save to cache with high quality
115
+ output_path = self.image_handler.cache_dir / f"{cache_key}.jpg"
116
+ final_path = styled_processor.save(str(output_path))
117
+
118
+ return final_path
119
+
120
+ except Exception as e:
121
+ print(f"Warning: Failed to create fallback image: {e}")
122
+ return None
123
+
124
+ def _apply_professional_styling(self, processor, width: int, height: int):
125
+ """
126
+ Apply professional styling pipeline to PlaceKitten image.
127
+
128
+ Args:
129
+ processor: PlaceKitten ImageProcessor instance
130
+ width: Target width
131
+ height: Target height
132
+
133
+ Returns:
134
+ Styled ImageProcessor ready for saving
135
+ """
136
+ config = self.professional_config
137
+
138
+ # Smart crop to exact dimensions
139
+ styled = processor.smart_crop(
140
+ width=width, height=height, strategy=config["smart_crop_strategy"]
141
+ )
142
+
143
+ # Apply business-appropriate grayscale filter
144
+ styled = styled.apply_filter(config["base_filter"])
145
+
146
+ # Enhance contrast for professional appearance
147
+ if config["contrast_adjustment"] != 100:
148
+ styled = styled.apply_filter("contrast", value=config["contrast_adjustment"])
149
+
150
+ # Subtle brightness adjustment if needed
151
+ if config["brightness_adjustment"] != 100:
152
+ styled = styled.apply_filter("brightness", value=config["brightness_adjustment"])
153
+
154
+ return styled
155
+
156
+ def _select_image_id(self, context: Optional[Dict] = None) -> int:
157
+ """
158
+ Select consistent image ID based on context.
159
+
160
+ Args:
161
+ context: Optional context with slide information
162
+
163
+ Returns:
164
+ int: Image ID (1-based) for consistent selection
165
+ """
166
+ if not context:
167
+ return 1 # Default to first image
168
+
169
+ # Use slide index for consistency within presentations
170
+ if "slide_index" in context:
171
+ slide_index = context["slide_index"]
172
+ # Cycle through available images based on slide index
173
+ available_images = self.pk.get_image_count()
174
+ return (slide_index % available_images) + 1
175
+
176
+ # Use layout type for consistency
177
+ if "layout" in context:
178
+ layout = context["layout"]
179
+ # Simple hash-based selection for consistent results
180
+ layout_hash = hash(layout) % self.pk.get_image_count()
181
+ return layout_hash + 1
182
+
183
+ return 1 # Default fallback
184
+
185
+ def _generate_fallback_cache_key(
186
+ self, dimensions: Tuple[int, int], context: Optional[Dict] = None
187
+ ) -> str:
188
+ """
189
+ Generate cache key for fallback image.
190
+
191
+ Args:
192
+ dimensions: Target dimensions
193
+ context: Optional context information
194
+
195
+ Returns:
196
+ str: Cache key for consistent fallback generation
197
+ """
198
+ width, height = dimensions
199
+
200
+ # Base key with dimensions and styling
201
+ key_parts = [
202
+ "placekitten_fallback",
203
+ f"{width}x{height}",
204
+ self.professional_config["base_filter"],
205
+ f'contrast{self.professional_config["contrast_adjustment"]}',
206
+ f'brightness{self.professional_config["brightness_adjustment"]}',
207
+ ]
208
+
209
+ # Add context-based components for consistency
210
+ if context:
211
+ if "slide_index" in context:
212
+ key_parts.append(f'slide{context["slide_index"]}')
213
+ elif "layout" in context:
214
+ key_parts.append(f'layout{hash(context["layout"])}')
215
+
216
+ return "_".join(str(part) for part in key_parts)
217
+
218
+ def _get_professional_styling(self) -> Dict:
219
+ """
220
+ Get professional styling configuration for business presentations.
221
+
222
+ Returns:
223
+ dict: Configuration for professional image styling
224
+ """
225
+ return {
226
+ "base_filter": "grayscale", # Business-appropriate monochrome
227
+ "contrast_adjustment": 95, # Subtle contrast reduction
228
+ "brightness_adjustment": 105, # Slight brightness boost
229
+ "smart_crop_strategy": "haar-face", # Face-priority cropping
230
+ }
231
+
232
+ def get_fallback_info(
233
+ self, dimensions: Tuple[int, int], context: Optional[Dict] = None
234
+ ) -> Dict:
235
+ """
236
+ Get information about fallback image that would be generated.
237
+
238
+ Args:
239
+ dimensions: Target dimensions
240
+ context: Optional context information
241
+
242
+ Returns:
243
+ dict: Information about the fallback image
244
+ """
245
+ if not self.is_available():
246
+ return {"available": False, "reason": "PlaceKitten library not available"}
247
+
248
+ width, height = dimensions
249
+ image_id = self._select_image_id(context)
250
+ cache_key = self._generate_fallback_cache_key(dimensions, context)
251
+ cached_path = self.image_handler._get_cached_image(cache_key)
252
+
253
+ return {
254
+ "available": True,
255
+ "dimensions": dimensions,
256
+ "image_id": image_id,
257
+ "styling": self.professional_config,
258
+ "cached": cached_path is not None,
259
+ "cache_key": cache_key,
260
+ }
261
+
262
+ def cleanup_fallback_cache(self):
263
+ """Clean up cached fallback images to free space."""
264
+ try:
265
+ # Remove all PlaceKitten fallback images from cache
266
+ fallback_files = list(self.image_handler.cache_dir.glob("placekitten_fallback_*.jpg"))
267
+
268
+ removed_count = 0
269
+ for file_path in fallback_files:
270
+ try:
271
+ file_path.unlink()
272
+ removed_count += 1
273
+ except Exception:
274
+ continue # nosec - Continue processing other files if one fails
275
+
276
+ return {"removed_files": removed_count, "total_fallback_files": len(fallback_files)}
277
+
278
+ except Exception as e:
279
+ print(f"Warning: Fallback cache cleanup failed: {e}")
280
+ return {"removed_files": 0, "total_fallback_files": 0}