import math from typing import Optional, List class AvatarGenerator: DEFAULT_COLORS = ["#000000", "#8f1414", "#e50e0e", "#f3450f", "#fcac03"] DEFAULT_SIZE = 80 DEFAULT_VARIANT = 'marble' VALID_VARIANTS = {'marble', 'beam', 'pixel', 'sunset', 'ring', 'bauhaus'} def __init__(self, default_colors: Optional[List[str]] = None, default_size: Optional[int] = None, default_variant: Optional[str] = None): self.colors = default_colors if default_colors else self.DEFAULT_COLORS self.size = default_size if default_size else self.DEFAULT_SIZE self.variant = default_variant if default_variant else self.DEFAULT_VARIANT self.AVATAR_GENERATORS = { 'marble': self._generate_marble_avatar, 'beam': self._generate_beam_avatar, 'pixel': self._generate_pixel_avatar, 'sunset': self._generate_sunset_avatar, 'ring': self._generate_ring_avatar, 'bauhaus': self._generate_bauhaus_avatar, } @staticmethod def _hash_code(name: str) -> int: """Generate hash from name string - exact port of hashCode function""" try: if not name or not isinstance(name, str): # Consider raising a TypeError or returning a default hash for invalid input return 12345 # Fallback for invalid input hash_val = 0 for char in name: hash_val = (hash_val << 5) - hash_val + ord(char) hash_val &= hash_val # Convert to 32bit integer return abs(hash_val) except Exception: return 12345 # fallback hash value @staticmethod def _get_modulus(num: int, max_val: int) -> int: """Get modulus""" return num % max_val @staticmethod def _get_digit(number: int, ntn: int) -> int: """Get digit at position""" try: if not isinstance(number, int) or not isinstance(ntn, int): return 0 # Fallback for invalid input type return int((number / (10 ** ntn)) % 10) except (ZeroDivisionError, ValueError, OverflowError): return 0 @staticmethod def _get_boolean(number: int, ntn: int) -> bool: """Get boolean from digit""" return not ((AvatarGenerator._get_digit(number, ntn)) % 2) @staticmethod def _get_angle(x: float, y: float) -> float: """Get angle from coordinates""" return math.atan2(y, x) * 180 / math.pi @staticmethod def _get_unit(number: int, range_val: int, index: int = None) -> int: """Get unit value with optional negative""" try: if not isinstance(number, int) or not isinstance(range_val, int) or range_val == 0: return 0 # Fallback for invalid input value = number % range_val if index and ((AvatarGenerator._get_digit(number, index) % 2) == 0): return -value return value except (ZeroDivisionError, ValueError, TypeError): return 0 @staticmethod def _get_random_color(number: int, colors: List[str], range_val: int) -> str: """Get random color from palette""" try: if not colors or range_val == 0: return AvatarGenerator.DEFAULT_COLORS[0] # Fallback return colors[number % range_val] except (IndexError, TypeError, ZeroDivisionError): return AvatarGenerator.DEFAULT_COLORS[0] @staticmethod def _get_contrast(hexcolor: str) -> str: """Get contrasting color (black or white)""" try: if not hexcolor or not isinstance(hexcolor, str): return '#000000' # Fallback # Remove leading # if present if hexcolor.startswith('#'): hexcolor = hexcolor[1:] # Clean up any remaining quotes or whitespace hexcolor = hexcolor.strip().replace('"', '').replace("'", "") # Ensure we have exactly 6 hex characters if len(hexcolor) != 6: return '#000000' # Fallback # Validate hex characters try: int(hexcolor, 16) except ValueError: return '#000000' # Fallback # Convert to RGB r = int(hexcolor[0:2], 16) g = int(hexcolor[2:4], 16) b = int(hexcolor[4:6], 16) # Get YIQ ratio yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000 # Check contrast return '#000000' if yiq >= 128 else '#FFFFFF' except Exception: return '#000000' @staticmethod def normalize_colors(colors_param: Optional[str]) -> List[str]: """Normalize color palette from query parameter""" try: if not colors_param: return AvatarGenerator.DEFAULT_COLORS # Clean up URL encoding cleaned = colors_param.replace('%22', '"').replace('%20', ' ').replace('%5B', '[').replace('%5D', ']').strip() # Extract colors from different formats color_list = AvatarGenerator._extract_color_strings(cleaned) # Validate and normalize colors return AvatarGenerator._validate_color_list(color_list) except Exception: return AvatarGenerator.DEFAULT_COLORS @staticmethod def _extract_color_strings(cleaned_colors: str) -> List[str]: """Extract color strings from cleaned input""" # Handle array format: ["#xxxxxx", "#xxxxxx", ...] if cleaned_colors.startswith('[') and cleaned_colors.endswith(']'): inner_content = cleaned_colors[1:-1].strip() if not inner_content: return [] return [part.strip().replace('"', '').replace("'", '') for part in inner_content.split(',')] # Handle comma-separated format: #xxxxxx, #xxxxxx, ... return [part.strip().replace('"', '').replace("'", '') for part in cleaned_colors.split(',')] @staticmethod def _validate_color_list(color_list: List[str]) -> List[str]: """Validate and normalize hex colors""" normalized_colors = [] for color in color_list: color = color.strip() if not color: continue if not color.startswith('#'): color = '#' + color if AvatarGenerator._is_valid_hex_color(color): normalized_colors.append(color) return normalized_colors if normalized_colors else AvatarGenerator.DEFAULT_COLORS @staticmethod def _is_valid_hex_color(color: str) -> bool: """Check if string is valid hex color""" return len(color) == 7 and all(c in '0123456789ABCDEFabcdef' for c in color[1:]) def _generate_marble_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str: """Generate marble variant avatar""" ELEMENTS = 3 SIZE = 80 # Intrinsic size of the SVG design def generate_colors_marble(name: str, current_colors: List[str]): num_from_name = self._hash_code(name) range_val = len(current_colors) if current_colors else len(self.DEFAULT_COLORS) elements_properties = [] for i in range(ELEMENTS): elements_properties.append({ 'color': self._get_random_color(num_from_name + i, current_colors, range_val), 'translateX': self._get_unit(num_from_name * (i + 1), SIZE // 10, 1), 'translateY': self._get_unit(num_from_name * (i + 1), SIZE // 10, 2), 'scale': 1.2 + self._get_unit(num_from_name * (i + 1), SIZE // 20) / 10, 'rotate': self._get_unit(num_from_name * (i + 1), 360, 1) }) return elements_properties properties = generate_colors_marble(name, colors) mask_id = f"mask_{self._hash_code(name)}" filter_id = f"filter_{self._hash_code(name)}" # Use the passed 'size' for width/height, internal 'SIZE' for viewBox svg = f'''''' if title: svg += f'{name}' svg += f''' ''' return svg def _generate_beam_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str: """Generate beam variant avatar""" SIZE = 36 # Intrinsic size of the SVG design def generate_data_beam(name: str, current_colors: List[str]): num_from_name = self._hash_code(name) range_val = len(current_colors) if current_colors else len(self.DEFAULT_COLORS) wrapper_color = self._get_random_color(num_from_name, current_colors, range_val) pre_translate_x = self._get_unit(num_from_name, 10, 1) wrapper_translate_x = pre_translate_x + SIZE // 9 if pre_translate_x < 5 else pre_translate_x pre_translate_y = self._get_unit(num_from_name, 10, 2) wrapper_translate_y = pre_translate_y + SIZE // 9 if pre_translate_y < 5 else pre_translate_y data = { 'wrapperColor': wrapper_color, 'faceColor': self._get_contrast(wrapper_color), 'backgroundColor': self._get_random_color(num_from_name + 13, current_colors, range_val), 'wrapperTranslateX': wrapper_translate_x, 'wrapperTranslateY': wrapper_translate_y, 'wrapperRotate': self._get_unit(num_from_name, 360), 'wrapperScale': 1 + self._get_unit(num_from_name, SIZE // 12) / 10, 'isMouthOpen': self._get_boolean(num_from_name, 2), 'isCircle': self._get_boolean(num_from_name, 1), 'eyeSpread': self._get_unit(num_from_name, 5), 'mouthSpread': self._get_unit(num_from_name, 3), 'faceRotate': self._get_unit(num_from_name, 10, 3), 'faceTranslateX': wrapper_translate_x // 2 if wrapper_translate_x > SIZE // 6 else self._get_unit(num_from_name, 8, 1), 'faceTranslateY': wrapper_translate_y // 2 if wrapper_translate_y > SIZE // 6 else self._get_unit(num_from_name, 7, 2), } return data data = generate_data_beam(name, colors) mask_id = f"mask_{self._hash_code(name)}" svg = f'''''' if title: svg += f'{name}' rx_value = SIZE if data['isCircle'] else SIZE // 6 svg += f''' ''' if data['isMouthOpen']: svg += f''' ''' else: svg += f''' ''' svg += f''' ''' return svg def _generate_pixel_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str: """Generate pixel variant avatar""" ELEMENTS = 64 # 8x8 grid SIZE = 80 # Intrinsic SVG size def generate_colors_pixel(name: str, current_colors: List[str]): num_from_name = self._hash_code(name) # Use current_colors if provided, otherwise fallback to instance default, then class default final_colors = current_colors if current_colors else self.colors range_val = len(final_colors) color_list = [] for i in range(ELEMENTS): color_list.append(self._get_random_color(num_from_name + i, final_colors, range_val)) return color_list pixel_colors = generate_colors_pixel(name, colors) mask_id = f"mask_{self._hash_code(name)}" svg = f'''''' if title: svg += f'{name}' svg += f''' ''' # Generate 8x8 grid of pixels idx = 0 pixel_size = SIZE // 8 # Each pixel is 10x10 if SIZE is 80 for row in range(8): for col in range(8): svg += f''' ''' idx +=1 svg += ''' ''' return svg def _generate_sunset_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str: """Generate sunset variant avatar""" ELEMENTS = 4 # For 4 gradient stops SIZE = 80 # Intrinsic SVG size def generate_colors_sunset(name: str, current_colors: List[str]): num_from_name = self._hash_code(name) # Use current_colors if provided, otherwise fallback to instance default, then class default final_colors = current_colors if current_colors else self.colors # Ensure we have at least 4 colors for sunset, repeat if necessary if len(final_colors) < ELEMENTS: final_colors = (final_colors * (ELEMENTS // len(final_colors) + 1))[:ELEMENTS] range_val = len(final_colors) color_list = [] for i in range(ELEMENTS): color_list.append(self._get_random_color(num_from_name + i, final_colors, range_val)) return color_list sunset_colors = generate_colors_sunset(name, colors) # name_without_space = name.replace(' ', '').replace('-', '').replace('_', '') # Not used hash_val = abs(self._hash_code(name)) mask_id = f"mask_{hash_val}" gradient_id_1 = f"gradient_paint0_linear_{hash_val}" gradient_id_2 = f"gradient_paint1_linear_{hash_val}" rx_value = '' if square else f'rx="{SIZE * 2}"' svg = f'''''' if title: svg += f'{name}' svg += f''' ''' return svg def _generate_ring_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str: """Generate ring variant avatar""" SIZE = 90 # Intrinsic SVG size COLORS_NEEDED = 9 # Number of colors used in the SVG paths def generate_colors_ring(name: str, current_colors: List[str]): num_from_name = self._hash_code(name) # Use current_colors if provided, otherwise fallback to instance default, then class default final_colors = current_colors if current_colors else self.colors # Ensure we have enough colors, repeat if necessary if len(final_colors) < COLORS_NEEDED: final_colors = (final_colors * (COLORS_NEEDED // len(final_colors) + 1))[:COLORS_NEEDED] range_val = len(final_colors) ring_palette = [] for i in range(COLORS_NEEDED): ring_palette.append(self._get_random_color(num_from_name + i, final_colors, range_val)) return ring_palette ring_colors = generate_colors_ring(name, colors) mask_id = f"mask_{self._hash_code(name)}" svg = f'''''' if title: svg += f'{name}' svg += f''' ''' return svg def _generate_bauhaus_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str: """Generate bauhaus variant avatar""" ELEMENTS = 4 # Number of geometric elements SIZE = 80 # Intrinsic SVG size def generate_colors_bauhaus(name: str, current_colors: List[str]): num_from_name = self._hash_code(name) # Use current_colors if provided, otherwise fallback to instance default, then class default final_colors = current_colors if current_colors else self.colors range_val = len(final_colors) properties = [] for i in range(ELEMENTS): properties.append({ 'color': self._get_random_color(num_from_name + i, final_colors, range_val), 'translateX': self._get_unit(num_from_name * (i + 1), SIZE // 2 - (i + 17), 1), 'translateY': self._get_unit(num_from_name * (i + 1), SIZE // 2 - (i + 17), 2), 'rotate': self._get_unit(num_from_name * (i + 1), 360), 'isSquare': self._get_boolean(num_from_name, 2) }) return properties properties = generate_colors_bauhaus(name, colors) mask_id = f"mask_{self._hash_code(name)}" svg = f'''''' if title: svg += f'{name}' svg += f''' ''' return svg def generate_avatar(self, name: str, variant: Optional[str] = None, colors: Optional[List[str]] = None, # Allow passing colors as list of strings colors_param: Optional[str] = None, # Allow passing colors as a string parameter size: Optional[int] = None, square: bool = False, title: bool = True) -> str: """ Generate an SVG avatar. Args: name: The name to generate the avatar for. variant: The avatar variant (e.g., 'marble', 'beam'). Uses instance default if None. colors: A list of hex color strings. Overrides colors_param if provided. colors_param: A string representation of colors (e.g., "['#FF0000', '#00FF00']" or "#FF0000,#00FF00"). Used if 'colors' list is not provided. size: The desired size of the avatar in pixels. Uses instance default if None. square: If True, generates a square avatar. Otherwise, a circular one. title: If True, includes a tag with the name in the SVG. Returns: An SVG string representing the avatar. """ current_variant = variant if variant and variant in self.VALID_VARIANTS else self.variant current_size = size if size else self.size # Determine colors: prioritize direct list, then string param, then instance default if colors: # Validate and normalize if a list is directly passed final_colors = self._validate_color_list(colors) elif colors_param: final_colors = self.normalize_colors(colors_param) else: final_colors = self.colors # Use instance default colors if not final_colors: # Ensure there's always a fallback final_colors = self.DEFAULT_COLORS generator_func = self.AVATAR_GENERATORS.get(current_variant) if generator_func: return generator_func(name, final_colors, current_size, square, title) else: # Fallback to default variant if the chosen one is somehow invalid after checks default_gen_func = self.AVATAR_GENERATORS.get(self.DEFAULT_VARIANT) return default_gen_func(name, final_colors, current_size, square, title)