validators.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/usr/bin/env python3
  2. """
  3. Validators - Check if GIFs meet Slack's requirements.
  4. These validators help ensure your GIFs meet Slack's size and dimension constraints.
  5. """
  6. from pathlib import Path
  7. def validate_gif(
  8. gif_path: str | Path, is_emoji: bool = True, verbose: bool = True
  9. ) -> tuple[bool, dict]:
  10. """
  11. Validate GIF for Slack (dimensions, size, frame count).
  12. Args:
  13. gif_path: Path to GIF file
  14. is_emoji: True for emoji (128x128 recommended), False for message GIF
  15. verbose: Print validation details
  16. Returns:
  17. Tuple of (passes: bool, results: dict with all details)
  18. """
  19. from PIL import Image
  20. gif_path = Path(gif_path)
  21. if not gif_path.exists():
  22. return False, {"error": f"File not found: {gif_path}"}
  23. # Get file size
  24. size_bytes = gif_path.stat().st_size
  25. size_kb = size_bytes / 1024
  26. size_mb = size_kb / 1024
  27. # Get dimensions and frame info
  28. try:
  29. with Image.open(gif_path) as img:
  30. width, height = img.size
  31. # Count frames
  32. frame_count = 0
  33. try:
  34. while True:
  35. img.seek(frame_count)
  36. frame_count += 1
  37. except EOFError:
  38. pass
  39. # Get duration
  40. try:
  41. duration_ms = img.info.get("duration", 100)
  42. total_duration = (duration_ms * frame_count) / 1000
  43. fps = frame_count / total_duration if total_duration > 0 else 0
  44. except:
  45. total_duration = None
  46. fps = None
  47. except Exception as e:
  48. return False, {"error": f"Failed to read GIF: {e}"}
  49. # Validate dimensions
  50. if is_emoji:
  51. optimal = width == height == 128
  52. acceptable = width == height and 64 <= width <= 128
  53. dim_pass = acceptable
  54. else:
  55. aspect_ratio = (
  56. max(width, height) / min(width, height)
  57. if min(width, height) > 0
  58. else float("inf")
  59. )
  60. dim_pass = aspect_ratio <= 2.0 and 320 <= min(width, height) <= 640
  61. results = {
  62. "file": str(gif_path),
  63. "passes": dim_pass,
  64. "width": width,
  65. "height": height,
  66. "size_kb": size_kb,
  67. "size_mb": size_mb,
  68. "frame_count": frame_count,
  69. "duration_seconds": total_duration,
  70. "fps": fps,
  71. "is_emoji": is_emoji,
  72. "optimal": optimal if is_emoji else None,
  73. }
  74. # Print if verbose
  75. if verbose:
  76. print(f"\nValidating {gif_path.name}:")
  77. print(
  78. f" Dimensions: {width}x{height}"
  79. + (
  80. f" ({'optimal' if optimal else 'acceptable'})"
  81. if is_emoji and acceptable
  82. else ""
  83. )
  84. )
  85. print(
  86. f" Size: {size_kb:.1f} KB"
  87. + (f" ({size_mb:.2f} MB)" if size_mb >= 1.0 else "")
  88. )
  89. print(
  90. f" Frames: {frame_count}"
  91. + (f" @ {fps:.1f} fps ({total_duration:.1f}s)" if fps else "")
  92. )
  93. if not dim_pass:
  94. print(
  95. f" Note: {'Emoji should be 128x128' if is_emoji else 'Unusual dimensions for Slack'}"
  96. )
  97. if size_mb > 5.0:
  98. print(f" Note: Large file size - consider fewer frames/colors")
  99. return dim_pass, results
  100. def is_slack_ready(
  101. gif_path: str | Path, is_emoji: bool = True, verbose: bool = True
  102. ) -> bool:
  103. """
  104. Quick check if GIF is ready for Slack.
  105. Args:
  106. gif_path: Path to GIF file
  107. is_emoji: True for emoji GIF, False for message GIF
  108. verbose: Print feedback
  109. Returns:
  110. True if dimensions are acceptable
  111. """
  112. passes, _ = validate_gif(gif_path, is_emoji, verbose)
  113. return passes