Validation utilities for web assets.
Validates file sizes, dimensions, formats, and accessibility requirements.
from typing import List, Dict, Tuple, Optional
# Platform-specific requirements
PLATFORM_REQUIREMENTS = {
'max_file_size': 8 * 1024 * 1024, # 8MB
'recommended_size': (1200, 630),
'formats': ['png', 'jpg', 'jpeg'],
'max_file_size': 5 * 1024 * 1024, # 5MB
'recommended_size': (1200, 675),
'formats': ['png', 'jpg', 'jpeg', 'webp'],
'max_file_size': 5 * 1024 * 1024, # 5MB
'recommended_size': (1200, 627),
'formats': ['png', 'jpg', 'jpeg'],
'max_file_size': 8 * 1024 * 1024, # 8MB (same as Facebook)
'recommended_size': (1200, 630),
'formats': ['png', 'jpg', 'jpeg'],
# WCAG contrast ratio requirements
"""Represents the result of a validation check."""
def __init__(self, passed: bool, message: str, level: str = 'info'):
Initialize validation result.
passed: Whether the validation passed
message: Description of the result
level: 'success', 'warning', 'error', 'info'
return f"{icon} {self.message}"
return f"ValidationResult(passed={self.passed}, level='{self.level}', message='{self.message}')"
def validate_file_size(file_path: str, platform: str = 'facebook') -> ValidationResult:
Validate file size against platform requirements.
file_path: Path to the image file
platform: Platform name (facebook, twitter, linkedin, whatsapp)
ValidationResult with pass/fail and message
if platform not in PLATFORM_REQUIREMENTS:
f"Unknown platform: {platform}",
file_path = Path(file_path)
if not file_path.exists():
f"File not found: {file_path}",
file_size = os.path.getsize(file_path)
max_size = PLATFORM_REQUIREMENTS[platform]['max_file_size']
# Convert to MB for display
file_size_mb = file_size / (1024 * 1024)
max_size_mb = max_size / (1024 * 1024)
f"File size {file_size_mb:.1f}MB exceeds {platform.title()} limit of {max_size_mb:.0f}MB",
elif file_size > max_size * 0.8: # Warn if within 80% of limit
f"File size {file_size_mb:.1f}MB is close to {platform.title()} limit ({max_size_mb:.0f}MB)",
f"File size {file_size_mb:.1f}MB is within {platform.title()} limits",
def validate_dimensions(file_path: str, platform: str = 'facebook') -> ValidationResult:
Validate image dimensions against platform requirements.
file_path: Path to the image file
ValidationResult with pass/fail and message
if platform not in PLATFORM_REQUIREMENTS:
f"Unknown platform: {platform}",
file_path = Path(file_path)
if not file_path.exists():
f"File not found: {file_path}",
with Image.open(file_path) as img:
f"Could not read image dimensions: {e}",
requirements = PLATFORM_REQUIREMENTS[platform]
rec_width, rec_height = requirements['recommended_size']
min_width, min_height = requirements['min_size']
# Check if dimensions match recommended size
if (width, height) == (rec_width, rec_height):
f"Dimensions {width}x{height} match {platform.title()} recommended size",
# Check if dimensions meet minimum requirements
if width < min_width or height < min_height:
f"Dimensions {width}x{height} below {platform.title()} minimum ({min_width}x{min_height})",
actual_ratio = width / height
expected_ratio = requirements['aspect_ratio']
ratio_diff = abs(actual_ratio - expected_ratio)
if ratio_diff > 0.1: # Allow 10% variance
f"Dimensions {width}x{height} have non-standard aspect ratio (expected {expected_ratio:.2f}:1, got {actual_ratio:.2f}:1)",
f"Dimensions {width}x{height} meet {platform.title()} requirements",
def validate_format(file_path: str, platform: str = 'facebook') -> ValidationResult:
Validate image format against platform requirements.
file_path: Path to the image file
ValidationResult with pass/fail and message
if platform not in PLATFORM_REQUIREMENTS:
f"Unknown platform: {platform}",
file_path = Path(file_path)
if not file_path.exists():
f"File not found: {file_path}",
with Image.open(file_path) as img:
format_name = img.format.lower() if img.format else None
f"Could not read image format: {e}",
allowed_formats = PLATFORM_REQUIREMENTS[platform]['formats']
if format_name in allowed_formats:
f"Format {format_name.upper()} is supported by {platform.title()}",
f"Format {format_name.upper()} not supported by {platform.title()} (use {', '.join(allowed_formats).upper()})",
def calculate_contrast_ratio(color1: Tuple[int, int, int], color2: Tuple[int, int, int]) -> float:
Calculate contrast ratio between two RGB colors.
Based on WCAG 2.0 formula.
color1: RGB tuple (r, g, b) 0-255
color2: RGB tuple (r, g, b) 0-255
Contrast ratio (1.0 to 21.0)
def relative_luminance(rgb):
"""Calculate relative luminance of an RGB color."""
r, g, b = [c / 255.0 for c in rgb]
r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
return 0.2126 * r + 0.7152 * g + 0.0722 * b
lum1 = relative_luminance(color1)
lum2 = relative_luminance(color2)
# Ensure lighter color is in numerator
lighter = max(lum1, lum2)
return (lighter + 0.05) / (darker + 0.05)
text_color: Tuple[int, int, int],
bg_color: Tuple[int, int, int],
Validate contrast ratio meets WCAG requirements.
text_color: RGB tuple for text
bg_color: RGB tuple for background
font_size: Font size in pixels
is_bold: Whether text is bold
ValidationResult with WCAG compliance level
ratio = calculate_contrast_ratio(text_color, bg_color)
# Determine if text is "large" (18pt+ or 14pt+ bold)
# Assuming 16px = 12pt, so 1px = 0.75pt
pt_size = font_size * 0.75
is_large = pt_size >= 18 or (pt_size >= 14 and is_bold)
min_aaa = WCAG_AAA_NORMAL
f"Contrast ratio {ratio:.1f}:1 meets WCAG AAA standards ({min_aaa:.1f}:1 required)",
f"Contrast ratio {ratio:.1f}:1 meets WCAG AA standards ({min_aa:.1f}:1 required)",
f"Contrast ratio {ratio:.1f}:1 fails WCAG AA standards ({min_aa:.1f}:1 required)",
def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
Convert hex color to RGB tuple.
hex_color: Hex color string (e.g., "#4F46E5" or "4F46E5")
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def validate_all(file_path: str, platforms: List[str] = None) -> Dict[str, List[ValidationResult]]:
Run all validations on an image file for specified platforms.
file_path: Path to the image file
platforms: List of platforms to validate against (default: ['facebook', 'twitter'])
Dictionary of platform -> list of ValidationResults
platforms = ['facebook', 'twitter']
for platform in platforms:
if platform not in PLATFORM_REQUIREMENTS:
ValidationResult(False, f"Unknown platform: {platform}", 'error')
validate_file_size(file_path, platform),
validate_dimensions(file_path, platform),
validate_format(file_path, platform),
results[platform] = platform_results
def print_validation_results(results: Dict[str, List[ValidationResult]], verbose: bool = True):
Print validation results in a formatted way.
results: Dictionary of platform -> list of ValidationResults
verbose: If True, show all results; if False, only show warnings/errors
for platform, checks in results.items():
print(f"\n{platform.title()} Validation:")
if verbose or result.level in ['warning', 'error']:
total_checks = sum(len(checks) for checks in results.values())
passed_checks = sum(1 for checks in results.values() for r in checks if r.passed)
failed_checks = total_checks - passed_checks
print(f"Summary: {passed_checks}/{total_checks} checks passed")
print(f"⚠ {failed_checks} issue(s) need attention")
print("✓ All validations passed!")
if __name__ == '__main__':
print("Usage: python validators.py <image_file>")
print(f"Validating: {file_path}")
results = validate_all(file_path, platforms=['facebook', 'twitter', 'linkedin'])
print_validation_results(results)
# Test contrast validation
print("Contrast Ratio Tests:")
("White on Black", (255, 255, 255), (0, 0, 0), 16, False),
("Black on White", (0, 0, 0), (255, 255, 255), 16, False),
("Gray on White", (128, 128, 128), (255, 255, 255), 16, False),
("Purple on White", hex_to_rgb("#4F46E5"), (255, 255, 255), 16, False),
("White on Purple", (255, 255, 255), hex_to_rgb("#4F46E5"), 80, True),
for name, text_color, bg_color, size, bold in test_cases:
result = validate_contrast(text_color, bg_color, size, bold)
print(f" {name} ({size}px{'bold' if bold else ''}): {result}")