cloudflare-images-migrator 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.
- cloudflare_images_migrator-1.0.0.dist-info/METADATA +474 -0
- cloudflare_images_migrator-1.0.0.dist-info/RECORD +17 -0
- cloudflare_images_migrator-1.0.0.dist-info/WHEEL +5 -0
- cloudflare_images_migrator-1.0.0.dist-info/entry_points.txt +3 -0
- cloudflare_images_migrator-1.0.0.dist-info/licenses/LICENSE +21 -0
- cloudflare_images_migrator-1.0.0.dist-info/top_level.txt +1 -0
- src/__init__.py +1 -0
- src/audit.py +620 -0
- src/cloudflare_client.py +746 -0
- src/config.py +161 -0
- src/image_tracker.py +405 -0
- src/logger.py +160 -0
- src/migrator.py +491 -0
- src/parsers.py +609 -0
- src/quality.py +558 -0
- src/security.py +528 -0
- src/utils.py +355 -0
src/quality.py
ADDED
@@ -0,0 +1,558 @@
|
|
1
|
+
"""
|
2
|
+
Premium quality enhancement module for Cloudflare Images Migration Tool
|
3
|
+
"""
|
4
|
+
|
5
|
+
import io
|
6
|
+
import time
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Dict, List, Optional, Tuple, Any
|
9
|
+
from PIL import Image, ImageOps, ImageFilter, ImageEnhance
|
10
|
+
import requests
|
11
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
12
|
+
|
13
|
+
|
14
|
+
class QualityOptimizer:
|
15
|
+
"""Premium quality optimizer for images before upload."""
|
16
|
+
|
17
|
+
def __init__(self, config, logger=None):
|
18
|
+
self.config = config
|
19
|
+
self.logger = logger
|
20
|
+
|
21
|
+
# Quality settings
|
22
|
+
self.optimization_levels = {
|
23
|
+
'conservative': {'quality': 95, 'optimize': True, 'progressive': True},
|
24
|
+
'balanced': {'quality': 85, 'optimize': True, 'progressive': True},
|
25
|
+
'aggressive': {'quality': 75, 'optimize': True, 'progressive': True}
|
26
|
+
}
|
27
|
+
|
28
|
+
self.default_level = getattr(config, 'optimization_level', 'balanced')
|
29
|
+
|
30
|
+
# Advanced settings
|
31
|
+
self.enable_lossless_optimization = True
|
32
|
+
self.enable_format_conversion = True
|
33
|
+
self.enable_responsive_variants = True
|
34
|
+
self.max_dimension_optimization = 2048
|
35
|
+
|
36
|
+
# Quality thresholds
|
37
|
+
self.quality_thresholds = {
|
38
|
+
'file_size_mb': 5.0,
|
39
|
+
'dimensions': (4000, 4000),
|
40
|
+
'compression_ratio': 0.7
|
41
|
+
}
|
42
|
+
|
43
|
+
def analyze_image_quality(self, file_path: Path) -> Dict[str, Any]:
|
44
|
+
"""
|
45
|
+
Comprehensive image quality analysis.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
Dict with quality metrics and recommendations
|
49
|
+
"""
|
50
|
+
analysis = {
|
51
|
+
'quality_score': 0.0,
|
52
|
+
'metrics': {},
|
53
|
+
'recommendations': [],
|
54
|
+
'optimization_potential': {},
|
55
|
+
'estimated_savings': {}
|
56
|
+
}
|
57
|
+
|
58
|
+
try:
|
59
|
+
with Image.open(file_path) as img:
|
60
|
+
original_size = file_path.stat().st_size
|
61
|
+
|
62
|
+
# Basic metrics
|
63
|
+
analysis['metrics'] = {
|
64
|
+
'format': img.format,
|
65
|
+
'mode': img.mode,
|
66
|
+
'size': img.size,
|
67
|
+
'file_size_bytes': original_size,
|
68
|
+
'file_size_mb': original_size / (1024 * 1024),
|
69
|
+
'compression_ratio': self._calculate_compression_ratio(img, original_size),
|
70
|
+
'color_depth': self._analyze_color_depth(img),
|
71
|
+
'has_transparency': self._has_transparency(img),
|
72
|
+
'estimated_quality': self._estimate_jpeg_quality(img, file_path)
|
73
|
+
}
|
74
|
+
|
75
|
+
# Quality scoring
|
76
|
+
analysis['quality_score'] = self._calculate_quality_score(analysis['metrics'])
|
77
|
+
|
78
|
+
# Optimization analysis
|
79
|
+
analysis['optimization_potential'] = self._analyze_optimization_potential(img, file_path)
|
80
|
+
|
81
|
+
# Size reduction estimates
|
82
|
+
analysis['estimated_savings'] = self._estimate_savings(img, file_path)
|
83
|
+
|
84
|
+
# Recommendations
|
85
|
+
analysis['recommendations'] = self._generate_quality_recommendations(analysis)
|
86
|
+
|
87
|
+
except Exception as e:
|
88
|
+
analysis['error'] = f"Quality analysis failed: {str(e)}"
|
89
|
+
if self.logger:
|
90
|
+
self.logger.error(f"Quality analysis error for {file_path}: {str(e)}")
|
91
|
+
|
92
|
+
return analysis
|
93
|
+
|
94
|
+
def optimize_image(self, file_path: Path, output_path: Optional[Path] = None,
|
95
|
+
optimization_level: str = None) -> Dict[str, Any]:
|
96
|
+
"""
|
97
|
+
Optimize image with premium quality settings.
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
Dict with optimization results
|
101
|
+
"""
|
102
|
+
if optimization_level is None:
|
103
|
+
optimization_level = self.default_level
|
104
|
+
|
105
|
+
result = {
|
106
|
+
'success': False,
|
107
|
+
'original_size': 0,
|
108
|
+
'optimized_size': 0,
|
109
|
+
'size_reduction': 0.0,
|
110
|
+
'quality_maintained': True,
|
111
|
+
'optimizations_applied': [],
|
112
|
+
'output_path': output_path or file_path
|
113
|
+
}
|
114
|
+
|
115
|
+
try:
|
116
|
+
original_size = file_path.stat().st_size
|
117
|
+
result['original_size'] = original_size
|
118
|
+
|
119
|
+
with Image.open(file_path) as img:
|
120
|
+
optimized_img = self._apply_optimizations(img, file_path, optimization_level)
|
121
|
+
|
122
|
+
# Save optimized image
|
123
|
+
save_path = output_path or file_path
|
124
|
+
self._save_optimized_image(optimized_img, save_path, img.format, optimization_level)
|
125
|
+
|
126
|
+
# Calculate results
|
127
|
+
optimized_size = save_path.stat().st_size
|
128
|
+
result['optimized_size'] = optimized_size
|
129
|
+
result['size_reduction'] = (original_size - optimized_size) / original_size
|
130
|
+
result['success'] = True
|
131
|
+
|
132
|
+
if self.logger:
|
133
|
+
reduction_pct = result['size_reduction'] * 100
|
134
|
+
self.logger.info(f"Optimized {file_path.name}: {reduction_pct:.1f}% size reduction")
|
135
|
+
|
136
|
+
except Exception as e:
|
137
|
+
result['error'] = f"Optimization failed: {str(e)}"
|
138
|
+
if self.logger:
|
139
|
+
self.logger.error(f"Optimization error for {file_path}: {str(e)}")
|
140
|
+
|
141
|
+
return result
|
142
|
+
|
143
|
+
def create_responsive_variants(self, file_path: Path,
|
144
|
+
sizes: List[Tuple[int, int]] = None) -> List[Dict[str, Any]]:
|
145
|
+
"""
|
146
|
+
Create responsive image variants for different screen sizes.
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
List of variant creation results
|
150
|
+
"""
|
151
|
+
if sizes is None:
|
152
|
+
sizes = [(320, 240), (768, 576), (1024, 768), (1920, 1080)]
|
153
|
+
|
154
|
+
variants = []
|
155
|
+
|
156
|
+
try:
|
157
|
+
with Image.open(file_path) as img:
|
158
|
+
original_width, original_height = img.size
|
159
|
+
|
160
|
+
for target_width, target_height in sizes:
|
161
|
+
# Skip if target is larger than original
|
162
|
+
if target_width > original_width or target_height > original_height:
|
163
|
+
continue
|
164
|
+
|
165
|
+
variant_result = self._create_variant(
|
166
|
+
img, file_path, target_width, target_height
|
167
|
+
)
|
168
|
+
variants.append(variant_result)
|
169
|
+
|
170
|
+
except Exception as e:
|
171
|
+
if self.logger:
|
172
|
+
self.logger.error(f"Variant creation error for {file_path}: {str(e)}")
|
173
|
+
|
174
|
+
return variants
|
175
|
+
|
176
|
+
def enhance_image_quality(self, file_path: Path) -> Dict[str, Any]:
|
177
|
+
"""
|
178
|
+
Apply AI-powered quality enhancements.
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
Dict with enhancement results
|
182
|
+
"""
|
183
|
+
result = {
|
184
|
+
'success': False,
|
185
|
+
'enhancements_applied': [],
|
186
|
+
'quality_improvement': 0.0
|
187
|
+
}
|
188
|
+
|
189
|
+
try:
|
190
|
+
with Image.open(file_path) as img:
|
191
|
+
enhanced_img = img.copy()
|
192
|
+
|
193
|
+
# Auto-level adjustment
|
194
|
+
if self._needs_level_adjustment(img):
|
195
|
+
enhanced_img = ImageOps.autocontrast(enhanced_img)
|
196
|
+
result['enhancements_applied'].append('auto_contrast')
|
197
|
+
|
198
|
+
# Sharpening for web display
|
199
|
+
if self._needs_sharpening(img):
|
200
|
+
enhanced_img = enhanced_img.filter(ImageFilter.UnsharpMask(
|
201
|
+
radius=1.0, percent=120, threshold=1
|
202
|
+
))
|
203
|
+
result['enhancements_applied'].append('unsharp_mask')
|
204
|
+
|
205
|
+
# Color enhancement
|
206
|
+
if self._needs_color_enhancement(img):
|
207
|
+
enhancer = ImageEnhance.Color(enhanced_img)
|
208
|
+
enhanced_img = enhancer.enhance(1.1)
|
209
|
+
result['enhancements_applied'].append('color_enhancement')
|
210
|
+
|
211
|
+
# Save enhanced image
|
212
|
+
enhanced_img.save(file_path, quality=95, optimize=True)
|
213
|
+
result['success'] = True
|
214
|
+
|
215
|
+
except Exception as e:
|
216
|
+
result['error'] = f"Enhancement failed: {str(e)}"
|
217
|
+
if self.logger:
|
218
|
+
self.logger.error(f"Enhancement error for {file_path}: {str(e)}")
|
219
|
+
|
220
|
+
return result
|
221
|
+
|
222
|
+
def _apply_optimizations(self, img: Image.Image, file_path: Path,
|
223
|
+
optimization_level: str) -> Image.Image:
|
224
|
+
"""Apply comprehensive optimizations to image."""
|
225
|
+
optimized = img.copy()
|
226
|
+
settings = self.optimization_levels[optimization_level]
|
227
|
+
|
228
|
+
# Format-specific optimizations
|
229
|
+
if img.format == 'PNG':
|
230
|
+
# PNG optimizations
|
231
|
+
if not self._has_transparency(img) and self.enable_format_conversion:
|
232
|
+
# Convert to RGB if no transparency needed
|
233
|
+
if img.mode in ('RGBA', 'LA'):
|
234
|
+
background = Image.new('RGB', img.size, (255, 255, 255))
|
235
|
+
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
|
236
|
+
optimized = background
|
237
|
+
|
238
|
+
elif img.format in ('JPEG', 'JPG'):
|
239
|
+
# JPEG optimizations
|
240
|
+
if img.mode != 'RGB':
|
241
|
+
optimized = optimized.convert('RGB')
|
242
|
+
|
243
|
+
# Dimension optimization
|
244
|
+
if max(optimized.size) > self.max_dimension_optimization:
|
245
|
+
ratio = self.max_dimension_optimization / max(optimized.size)
|
246
|
+
new_size = tuple(int(dim * ratio) for dim in optimized.size)
|
247
|
+
optimized = optimized.resize(new_size, Image.Resampling.LANCZOS)
|
248
|
+
|
249
|
+
return optimized
|
250
|
+
|
251
|
+
def _save_optimized_image(self, img: Image.Image, save_path: Path,
|
252
|
+
original_format: str, optimization_level: str):
|
253
|
+
"""Save image with optimal settings."""
|
254
|
+
settings = self.optimization_levels[optimization_level]
|
255
|
+
|
256
|
+
save_kwargs = {
|
257
|
+
'optimize': settings['optimize'],
|
258
|
+
'quality': settings['quality']
|
259
|
+
}
|
260
|
+
|
261
|
+
if original_format == 'JPEG':
|
262
|
+
save_kwargs.update({
|
263
|
+
'progressive': settings['progressive'],
|
264
|
+
'subsampling': 0 # Best quality subsampling
|
265
|
+
})
|
266
|
+
elif original_format == 'PNG':
|
267
|
+
save_kwargs.update({
|
268
|
+
'compress_level': 9, # Maximum compression
|
269
|
+
'optimize': True
|
270
|
+
})
|
271
|
+
|
272
|
+
img.save(save_path, **save_kwargs)
|
273
|
+
|
274
|
+
def _calculate_compression_ratio(self, img: Image.Image, file_size: int) -> float:
|
275
|
+
"""Calculate current compression ratio."""
|
276
|
+
try:
|
277
|
+
# Estimate uncompressed size (width * height * channels * bytes_per_channel)
|
278
|
+
channels = len(img.getbands())
|
279
|
+
uncompressed_size = img.width * img.height * channels
|
280
|
+
return file_size / uncompressed_size if uncompressed_size > 0 else 1.0
|
281
|
+
except:
|
282
|
+
return 1.0
|
283
|
+
|
284
|
+
def _analyze_color_depth(self, img: Image.Image) -> Dict[str, Any]:
|
285
|
+
"""Analyze color depth and usage."""
|
286
|
+
colors = img.getcolors(maxcolors=256*256*256)
|
287
|
+
|
288
|
+
return {
|
289
|
+
'unique_colors': len(colors) if colors else 'many',
|
290
|
+
'mode': img.mode,
|
291
|
+
'has_palette': hasattr(img, 'palette') and img.palette is not None
|
292
|
+
}
|
293
|
+
|
294
|
+
def _has_transparency(self, img: Image.Image) -> bool:
|
295
|
+
"""Check if image has transparency."""
|
296
|
+
return img.mode in ('RGBA', 'LA') or 'transparency' in img.info
|
297
|
+
|
298
|
+
def _estimate_jpeg_quality(self, img: Image.Image, file_path: Path) -> Optional[int]:
|
299
|
+
"""Estimate JPEG quality level."""
|
300
|
+
if img.format != 'JPEG':
|
301
|
+
return None
|
302
|
+
|
303
|
+
try:
|
304
|
+
# This is a simplified estimation
|
305
|
+
# In practice, you'd use more sophisticated methods
|
306
|
+
file_size = file_path.stat().st_size
|
307
|
+
pixels = img.width * img.height
|
308
|
+
bytes_per_pixel = file_size / pixels
|
309
|
+
|
310
|
+
# Rough quality estimation based on bytes per pixel
|
311
|
+
if bytes_per_pixel > 2.0:
|
312
|
+
return 95
|
313
|
+
elif bytes_per_pixel > 1.5:
|
314
|
+
return 85
|
315
|
+
elif bytes_per_pixel > 1.0:
|
316
|
+
return 75
|
317
|
+
elif bytes_per_pixel > 0.5:
|
318
|
+
return 60
|
319
|
+
else:
|
320
|
+
return 40
|
321
|
+
except:
|
322
|
+
return None
|
323
|
+
|
324
|
+
def _calculate_quality_score(self, metrics: Dict) -> float:
|
325
|
+
"""Calculate overall quality score (0-100)."""
|
326
|
+
score = 100.0
|
327
|
+
|
328
|
+
# File size penalty
|
329
|
+
if metrics['file_size_mb'] > self.quality_thresholds['file_size_mb']:
|
330
|
+
score -= 20
|
331
|
+
|
332
|
+
# Dimension efficiency
|
333
|
+
max_dimension = max(metrics['size'])
|
334
|
+
if max_dimension > self.quality_thresholds['dimensions'][0]:
|
335
|
+
score -= 15
|
336
|
+
|
337
|
+
# Compression efficiency
|
338
|
+
if metrics['compression_ratio'] > self.quality_thresholds['compression_ratio']:
|
339
|
+
score -= 10
|
340
|
+
|
341
|
+
# Format efficiency
|
342
|
+
if metrics['format'] == 'BMP':
|
343
|
+
score -= 30 # Uncompressed format
|
344
|
+
elif metrics['format'] == 'PNG' and not metrics['has_transparency']:
|
345
|
+
score -= 5 # Could potentially be JPEG
|
346
|
+
|
347
|
+
return max(0.0, min(100.0, score))
|
348
|
+
|
349
|
+
def _analyze_optimization_potential(self, img: Image.Image, file_path: Path) -> Dict:
|
350
|
+
"""Analyze potential for optimization."""
|
351
|
+
potential = {
|
352
|
+
'format_conversion': False,
|
353
|
+
'dimension_reduction': False,
|
354
|
+
'quality_reduction': False,
|
355
|
+
'metadata_removal': False
|
356
|
+
}
|
357
|
+
|
358
|
+
# Format conversion potential
|
359
|
+
if img.format == 'PNG' and not self._has_transparency(img):
|
360
|
+
potential['format_conversion'] = True
|
361
|
+
|
362
|
+
# Dimension reduction potential
|
363
|
+
if max(img.size) > self.max_dimension_optimization:
|
364
|
+
potential['dimension_reduction'] = True
|
365
|
+
|
366
|
+
# Quality reduction potential
|
367
|
+
if img.format == 'JPEG':
|
368
|
+
estimated_quality = self._estimate_jpeg_quality(img, file_path)
|
369
|
+
if estimated_quality and estimated_quality > 85:
|
370
|
+
potential['quality_reduction'] = True
|
371
|
+
|
372
|
+
# Metadata removal potential
|
373
|
+
if hasattr(img, '_getexif') and img._getexif():
|
374
|
+
potential['metadata_removal'] = True
|
375
|
+
|
376
|
+
return potential
|
377
|
+
|
378
|
+
def _estimate_savings(self, img: Image.Image, file_path: Path) -> Dict:
|
379
|
+
"""Estimate potential file size savings."""
|
380
|
+
current_size = file_path.stat().st_size
|
381
|
+
|
382
|
+
savings = {
|
383
|
+
'format_conversion': 0,
|
384
|
+
'dimension_reduction': 0,
|
385
|
+
'quality_optimization': 0,
|
386
|
+
'total_estimated': 0
|
387
|
+
}
|
388
|
+
|
389
|
+
# Format conversion savings (PNG to JPEG)
|
390
|
+
if img.format == 'PNG' and not self._has_transparency(img):
|
391
|
+
savings['format_conversion'] = current_size * 0.3 # ~30% savings
|
392
|
+
|
393
|
+
# Dimension reduction savings
|
394
|
+
if max(img.size) > self.max_dimension_optimization:
|
395
|
+
ratio = self.max_dimension_optimization / max(img.size)
|
396
|
+
savings['dimension_reduction'] = current_size * (1 - ratio * ratio)
|
397
|
+
|
398
|
+
# Quality optimization savings
|
399
|
+
if img.format == 'JPEG':
|
400
|
+
estimated_quality = self._estimate_jpeg_quality(img, file_path)
|
401
|
+
if estimated_quality and estimated_quality > 85:
|
402
|
+
savings['quality_optimization'] = current_size * 0.15 # ~15% savings
|
403
|
+
|
404
|
+
savings['total_estimated'] = sum(savings.values()) - savings['total_estimated']
|
405
|
+
|
406
|
+
return savings
|
407
|
+
|
408
|
+
def _generate_quality_recommendations(self, analysis: Dict) -> List[str]:
|
409
|
+
"""Generate quality improvement recommendations."""
|
410
|
+
recommendations = []
|
411
|
+
metrics = analysis['metrics']
|
412
|
+
potential = analysis.get('optimization_potential', {})
|
413
|
+
|
414
|
+
if potential.get('format_conversion'):
|
415
|
+
recommendations.append('Convert PNG to JPEG for better compression (no transparency needed)')
|
416
|
+
|
417
|
+
if potential.get('dimension_reduction'):
|
418
|
+
recommendations.append(f'Reduce dimensions to max {self.max_dimension_optimization}px for web usage')
|
419
|
+
|
420
|
+
if potential.get('quality_reduction'):
|
421
|
+
recommendations.append('Reduce JPEG quality to 85% for optimal web performance')
|
422
|
+
|
423
|
+
if metrics['file_size_mb'] > 2.0:
|
424
|
+
recommendations.append('Consider aggressive optimization for files over 2MB')
|
425
|
+
|
426
|
+
if analysis['quality_score'] < 70:
|
427
|
+
recommendations.append('Multiple optimizations recommended for significant improvements')
|
428
|
+
|
429
|
+
return recommendations
|
430
|
+
|
431
|
+
def _create_variant(self, img: Image.Image, file_path: Path,
|
432
|
+
target_width: int, target_height: int) -> Dict:
|
433
|
+
"""Create a responsive variant."""
|
434
|
+
result = {
|
435
|
+
'size': (target_width, target_height),
|
436
|
+
'success': False,
|
437
|
+
'file_path': None
|
438
|
+
}
|
439
|
+
|
440
|
+
try:
|
441
|
+
# Calculate aspect ratio preserving resize
|
442
|
+
img_ratio = img.width / img.height
|
443
|
+
target_ratio = target_width / target_height
|
444
|
+
|
445
|
+
if img_ratio > target_ratio:
|
446
|
+
# Image is wider, fit to width
|
447
|
+
new_height = int(target_width / img_ratio)
|
448
|
+
new_size = (target_width, new_height)
|
449
|
+
else:
|
450
|
+
# Image is taller, fit to height
|
451
|
+
new_width = int(target_height * img_ratio)
|
452
|
+
new_size = (new_width, target_height)
|
453
|
+
|
454
|
+
# Resize with high quality
|
455
|
+
variant = img.resize(new_size, Image.Resampling.LANCZOS)
|
456
|
+
|
457
|
+
# Generate variant filename
|
458
|
+
stem = file_path.stem
|
459
|
+
suffix = file_path.suffix
|
460
|
+
variant_path = file_path.parent / f"{stem}_{target_width}x{target_height}{suffix}"
|
461
|
+
|
462
|
+
# Save variant
|
463
|
+
self._save_optimized_image(variant, variant_path, img.format, 'balanced')
|
464
|
+
|
465
|
+
result['success'] = True
|
466
|
+
result['file_path'] = variant_path
|
467
|
+
|
468
|
+
except Exception as e:
|
469
|
+
result['error'] = str(e)
|
470
|
+
|
471
|
+
return result
|
472
|
+
|
473
|
+
def _needs_level_adjustment(self, img: Image.Image) -> bool:
|
474
|
+
"""Check if image needs level adjustment."""
|
475
|
+
# Simple histogram analysis
|
476
|
+
if img.mode == 'RGB':
|
477
|
+
histogram = img.histogram()
|
478
|
+
# Check if histogram is heavily skewed
|
479
|
+
total_pixels = img.width * img.height
|
480
|
+
dark_pixels = sum(histogram[:64]) # First quarter of histogram
|
481
|
+
return (dark_pixels / total_pixels) > 0.7
|
482
|
+
return False
|
483
|
+
|
484
|
+
def _needs_sharpening(self, img: Image.Image) -> bool:
|
485
|
+
"""Check if image needs sharpening."""
|
486
|
+
# This is a simplified check
|
487
|
+
# In practice, you'd analyze edge detection or Laplacian variance
|
488
|
+
return max(img.size) > 500 # Apply sharpening to larger images
|
489
|
+
|
490
|
+
def _needs_color_enhancement(self, img: Image.Image) -> bool:
|
491
|
+
"""Check if image needs color enhancement."""
|
492
|
+
if img.mode != 'RGB':
|
493
|
+
return False
|
494
|
+
|
495
|
+
# Simple saturation check
|
496
|
+
try:
|
497
|
+
# Convert to HSV and check saturation
|
498
|
+
hsv = img.convert('HSV')
|
499
|
+
saturation_histogram = hsv.split()[1].histogram()
|
500
|
+
avg_saturation = sum(i * saturation_histogram[i] for i in range(256)) / sum(saturation_histogram)
|
501
|
+
return avg_saturation < 100 # Low saturation threshold
|
502
|
+
except:
|
503
|
+
return False
|
504
|
+
|
505
|
+
|
506
|
+
class QualityMonitor:
|
507
|
+
"""Monitor and track quality metrics across uploads."""
|
508
|
+
|
509
|
+
def __init__(self, logger=None):
|
510
|
+
self.logger = logger
|
511
|
+
self.quality_metrics = []
|
512
|
+
self.performance_benchmarks = {
|
513
|
+
'optimization_time': [],
|
514
|
+
'size_reductions': [],
|
515
|
+
'quality_scores': []
|
516
|
+
}
|
517
|
+
|
518
|
+
def record_quality_metrics(self, file_path: Path, analysis: Dict, optimization: Dict):
|
519
|
+
"""Record quality metrics for monitoring."""
|
520
|
+
metrics = {
|
521
|
+
'timestamp': time.time(),
|
522
|
+
'file_path': str(file_path),
|
523
|
+
'original_size': analysis['metrics']['file_size_mb'],
|
524
|
+
'quality_score': analysis['quality_score'],
|
525
|
+
'optimization_success': optimization.get('success', False),
|
526
|
+
'size_reduction': optimization.get('size_reduction', 0.0)
|
527
|
+
}
|
528
|
+
|
529
|
+
self.quality_metrics.append(metrics)
|
530
|
+
self._update_benchmarks(metrics)
|
531
|
+
|
532
|
+
def _update_benchmarks(self, metrics: Dict):
|
533
|
+
"""Update performance benchmarks."""
|
534
|
+
self.performance_benchmarks['size_reductions'].append(metrics['size_reduction'])
|
535
|
+
self.performance_benchmarks['quality_scores'].append(metrics['quality_score'])
|
536
|
+
|
537
|
+
def get_quality_report(self) -> Dict:
|
538
|
+
"""Generate quality performance report."""
|
539
|
+
if not self.quality_metrics:
|
540
|
+
return {'message': 'No quality data available'}
|
541
|
+
|
542
|
+
total_files = len(self.quality_metrics)
|
543
|
+
successful_optimizations = sum(1 for m in self.quality_metrics if m['optimization_success'])
|
544
|
+
|
545
|
+
avg_quality_score = sum(m['quality_score'] for m in self.quality_metrics) / total_files
|
546
|
+
avg_size_reduction = sum(m['size_reduction'] for m in self.quality_metrics) / total_files
|
547
|
+
|
548
|
+
return {
|
549
|
+
'total_files_processed': total_files,
|
550
|
+
'successful_optimizations': successful_optimizations,
|
551
|
+
'success_rate': successful_optimizations / total_files * 100,
|
552
|
+
'average_quality_score': avg_quality_score,
|
553
|
+
'average_size_reduction': avg_size_reduction * 100,
|
554
|
+
'total_data_saved_mb': sum(
|
555
|
+
m['original_size'] * m['size_reduction']
|
556
|
+
for m in self.quality_metrics
|
557
|
+
)
|
558
|
+
}
|