Initial commit
This commit is contained in:
264
core/validators.py
Executable file
264
core/validators.py
Executable file
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validators - Check if GIFs meet Slack's requirements.
|
||||
|
||||
These validators help ensure your GIFs meet Slack's size and dimension constraints.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def check_slack_size(gif_path: str | Path, is_emoji: bool = True) -> tuple[bool, dict]:
|
||||
"""
|
||||
Check if GIF meets Slack size limits.
|
||||
|
||||
Args:
|
||||
gif_path: Path to GIF file
|
||||
is_emoji: True for emoji GIF (64KB limit), False for message GIF (2MB limit)
|
||||
|
||||
Returns:
|
||||
Tuple of (passes: bool, info: dict with details)
|
||||
"""
|
||||
gif_path = Path(gif_path)
|
||||
|
||||
if not gif_path.exists():
|
||||
return False, {'error': f'File not found: {gif_path}'}
|
||||
|
||||
size_bytes = gif_path.stat().st_size
|
||||
size_kb = size_bytes / 1024
|
||||
size_mb = size_kb / 1024
|
||||
|
||||
limit_kb = 64 if is_emoji else 2048
|
||||
limit_mb = limit_kb / 1024
|
||||
|
||||
passes = size_kb <= limit_kb
|
||||
|
||||
info = {
|
||||
'size_bytes': size_bytes,
|
||||
'size_kb': size_kb,
|
||||
'size_mb': size_mb,
|
||||
'limit_kb': limit_kb,
|
||||
'limit_mb': limit_mb,
|
||||
'passes': passes,
|
||||
'type': 'emoji' if is_emoji else 'message'
|
||||
}
|
||||
|
||||
# Print feedback
|
||||
if passes:
|
||||
print(f"✓ {size_kb:.1f} KB - within {limit_kb} KB limit")
|
||||
else:
|
||||
print(f"✗ {size_kb:.1f} KB - exceeds {limit_kb} KB limit")
|
||||
overage_kb = size_kb - limit_kb
|
||||
overage_percent = (overage_kb / limit_kb) * 100
|
||||
print(f" Over by: {overage_kb:.1f} KB ({overage_percent:.1f}%)")
|
||||
print(f" Try: fewer frames, fewer colors, or simpler design")
|
||||
|
||||
return passes, info
|
||||
|
||||
|
||||
def validate_dimensions(width: int, height: int, is_emoji: bool = True) -> tuple[bool, dict]:
|
||||
"""
|
||||
Check if dimensions are suitable for Slack.
|
||||
|
||||
Args:
|
||||
width: Frame width in pixels
|
||||
height: Frame height in pixels
|
||||
is_emoji: True for emoji GIF, False for message GIF
|
||||
|
||||
Returns:
|
||||
Tuple of (passes: bool, info: dict with details)
|
||||
"""
|
||||
info = {
|
||||
'width': width,
|
||||
'height': height,
|
||||
'is_square': width == height,
|
||||
'type': 'emoji' if is_emoji else 'message'
|
||||
}
|
||||
|
||||
if is_emoji:
|
||||
# Emoji GIFs should be 128x128
|
||||
optimal = width == height == 128
|
||||
acceptable = width == height and 64 <= width <= 128
|
||||
|
||||
info['optimal'] = optimal
|
||||
info['acceptable'] = acceptable
|
||||
|
||||
if optimal:
|
||||
print(f"✓ {width}x{height} - optimal for emoji")
|
||||
passes = True
|
||||
elif acceptable:
|
||||
print(f"⚠ {width}x{height} - acceptable but 128x128 is optimal")
|
||||
passes = True
|
||||
else:
|
||||
print(f"✗ {width}x{height} - emoji should be square, 128x128 recommended")
|
||||
passes = False
|
||||
else:
|
||||
# Message GIFs should be square-ish and reasonable size
|
||||
aspect_ratio = max(width, height) / min(width, height) if min(width, height) > 0 else float('inf')
|
||||
reasonable_size = 320 <= min(width, height) <= 640
|
||||
|
||||
info['aspect_ratio'] = aspect_ratio
|
||||
info['reasonable_size'] = reasonable_size
|
||||
|
||||
# Check if roughly square (within 2:1 ratio)
|
||||
is_square_ish = aspect_ratio <= 2.0
|
||||
|
||||
if is_square_ish and reasonable_size:
|
||||
print(f"✓ {width}x{height} - good for message GIF")
|
||||
passes = True
|
||||
elif is_square_ish:
|
||||
print(f"⚠ {width}x{height} - square-ish but unusual size")
|
||||
passes = True
|
||||
elif reasonable_size:
|
||||
print(f"⚠ {width}x{height} - good size but not square-ish")
|
||||
passes = True
|
||||
else:
|
||||
print(f"✗ {width}x{height} - unusual dimensions for Slack")
|
||||
passes = False
|
||||
|
||||
return passes, info
|
||||
|
||||
|
||||
def validate_gif(gif_path: str | Path, is_emoji: bool = True) -> tuple[bool, dict]:
|
||||
"""
|
||||
Run all validations on a GIF file.
|
||||
|
||||
Args:
|
||||
gif_path: Path to GIF file
|
||||
is_emoji: True for emoji GIF, False for message GIF
|
||||
|
||||
Returns:
|
||||
Tuple of (all_pass: bool, results: dict)
|
||||
"""
|
||||
from PIL import Image
|
||||
|
||||
gif_path = Path(gif_path)
|
||||
|
||||
if not gif_path.exists():
|
||||
return False, {'error': f'File not found: {gif_path}'}
|
||||
|
||||
print(f"\nValidating {gif_path.name} as {'emoji' if is_emoji else 'message'} GIF:")
|
||||
print("=" * 60)
|
||||
|
||||
# Check file size
|
||||
size_pass, size_info = check_slack_size(gif_path, is_emoji)
|
||||
|
||||
# Check dimensions
|
||||
try:
|
||||
with Image.open(gif_path) as img:
|
||||
width, height = img.size
|
||||
dim_pass, dim_info = validate_dimensions(width, height, is_emoji)
|
||||
|
||||
# Count frames
|
||||
frame_count = 0
|
||||
try:
|
||||
while True:
|
||||
img.seek(frame_count)
|
||||
frame_count += 1
|
||||
except EOFError:
|
||||
pass
|
||||
|
||||
# Get duration if available
|
||||
try:
|
||||
duration_ms = img.info.get('duration', 100)
|
||||
total_duration = (duration_ms * frame_count) / 1000
|
||||
fps = frame_count / total_duration if total_duration > 0 else 0
|
||||
except:
|
||||
duration_ms = None
|
||||
total_duration = None
|
||||
fps = None
|
||||
|
||||
except Exception as e:
|
||||
return False, {'error': f'Failed to read GIF: {e}'}
|
||||
|
||||
print(f"\nFrames: {frame_count}")
|
||||
if total_duration:
|
||||
print(f"Duration: {total_duration:.1f}s @ {fps:.1f} fps")
|
||||
|
||||
all_pass = size_pass and dim_pass
|
||||
|
||||
results = {
|
||||
'file': str(gif_path),
|
||||
'passes': all_pass,
|
||||
'size': size_info,
|
||||
'dimensions': dim_info,
|
||||
'frame_count': frame_count,
|
||||
'duration_seconds': total_duration,
|
||||
'fps': fps
|
||||
}
|
||||
|
||||
print("=" * 60)
|
||||
if all_pass:
|
||||
print("✓ All validations passed!")
|
||||
else:
|
||||
print("✗ Some validations failed")
|
||||
print()
|
||||
|
||||
return all_pass, results
|
||||
|
||||
|
||||
def get_optimization_suggestions(results: dict) -> list[str]:
|
||||
"""
|
||||
Get suggestions for optimizing a GIF based on validation results.
|
||||
|
||||
Args:
|
||||
results: Results dict from validate_gif()
|
||||
|
||||
Returns:
|
||||
List of suggestion strings
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
if not results.get('passes', False):
|
||||
size_info = results.get('size', {})
|
||||
dim_info = results.get('dimensions', {})
|
||||
|
||||
# Size suggestions
|
||||
if not size_info.get('passes', True):
|
||||
overage = size_info['size_kb'] - size_info['limit_kb']
|
||||
if size_info['type'] == 'emoji':
|
||||
suggestions.append(f"Reduce file size by {overage:.1f} KB:")
|
||||
suggestions.append(" - Limit to 10-12 frames")
|
||||
suggestions.append(" - Use 32-40 colors maximum")
|
||||
suggestions.append(" - Remove gradients (solid colors compress better)")
|
||||
suggestions.append(" - Simplify design")
|
||||
else:
|
||||
suggestions.append(f"Reduce file size by {overage:.1f} KB:")
|
||||
suggestions.append(" - Reduce frame count or FPS")
|
||||
suggestions.append(" - Use fewer colors (128 → 64)")
|
||||
suggestions.append(" - Reduce dimensions")
|
||||
|
||||
# Dimension suggestions
|
||||
if not dim_info.get('optimal', True) and dim_info.get('type') == 'emoji':
|
||||
suggestions.append("For optimal emoji GIF:")
|
||||
suggestions.append(" - Use 128x128 dimensions")
|
||||
suggestions.append(" - Ensure square aspect ratio")
|
||||
|
||||
return suggestions
|
||||
|
||||
|
||||
# Convenience function for quick checks
|
||||
def is_slack_ready(gif_path: str | Path, is_emoji: bool = True, verbose: bool = True) -> bool:
|
||||
"""
|
||||
Quick check if GIF is ready for Slack.
|
||||
|
||||
Args:
|
||||
gif_path: Path to GIF file
|
||||
is_emoji: True for emoji GIF, False for message GIF
|
||||
verbose: Print detailed feedback
|
||||
|
||||
Returns:
|
||||
True if ready, False otherwise
|
||||
"""
|
||||
if verbose:
|
||||
passes, results = validate_gif(gif_path, is_emoji)
|
||||
if not passes:
|
||||
suggestions = get_optimization_suggestions(results)
|
||||
if suggestions:
|
||||
print("\nSuggestions:")
|
||||
for suggestion in suggestions:
|
||||
print(suggestion)
|
||||
return passes
|
||||
else:
|
||||
size_pass, _ = check_slack_size(gif_path, is_emoji)
|
||||
return size_pass
|
||||
Reference in New Issue
Block a user