# utils/color_utils.py from PIL import Image, ImageColor import re import cairocffi as cairo import pangocffi import pangocairocffi def multiply_and_clamp(value, scale, min_value=0, max_value=255): return min(max(value * scale, min_value), max_value) # Convert decimal color to hexadecimal color (rgb or rgba) def rgb_to_hex(rgb): color = "#" for i in rgb: num = int(i) color += str(hex(num))[-2:].replace("x", "0").upper() return color def parse_hex_color(hex_color, base = 1): """ This function is set to pass the color in (1.0,1.0, 1.0, 1.0) format. Change base to 255 to get the color in (255, 255, 255, 255) format. Parses a hex color string or tuple into RGBA components. Parses color values specified in various formats and convert them into normalized RGBA components suitable for use in color calculations, rendering, or manipulation. Supports: - #RRGGBBAA - #RRGGBB (assumes full opacity) - (r, g, b, a) tuple """ if isinstance(hex_color, tuple): if len(hex_color) == 4: r, g, b, a = hex_color elif len(hex_color) == 3: r, g, b = hex_color a = 1.0 # Full opacity else: raise ValueError("Tuple must be in the format (r, g, b) or (r, g, b, a)") return r / 255.0, g / 255.0, b / 255.0, a / 255.0 if a <= 1 else a if hex_color.startswith("#"): if len(hex_color) == 6: r = int(hex_color[0:2], 16) / 255.0 g = int(hex_color[2:4], 16) / 255.0 b = int(hex_color[4:6], 16) / 255.0 a = 1.0 # Full opacity elif len(hex_color) == 8: r = int(hex_color[0:2], 16) / 255.0 g = int(hex_color[2:4], 16) / 255.0 b = int(hex_color[4:6], 16) / 255.0 a = int(hex_color[6:8], 16) / 255.0 else: try: r, g, b, a = ImageColor.getcolor(hex_color, "RGBA") r = r / 255 g = g / 255 b = b / 255 a = a / 255 except: raise ValueError("Hex color must be in the format RRGGBB, RRGGBBAA, ( r, g, b, a) or a common color name") return multiply_and_clamp(r,base, max_value= base), multiply_and_clamp(g, base, max_value= base), multiply_and_clamp(b , base, max_value= base), multiply_and_clamp(a , base, max_value= base) # Define a function to convert a hexadecimal color code to an RGB(A) tuple def hex_to_rgb(hex): if hex.startswith("#"): clean_hex = hex.replace('#','') # Use a generator expression to convert pairs of hexadecimal digits to integers and create a tuple return tuple(int(clean_hex[i:i+2], 16) for i in range(0, len(clean_hex),2)) else: return detect_color_format(hex) def detect_color_format(color): """ Detects if the color is in RGB, RGBA, or hex format, and converts it to an RGBA tuple with integer components. Args: color (str or tuple): The color to detect. Returns: tuple: The color in RGBA format as a tuple of 4 integers. Raises: ValueError: If the input color is not in a recognized format. """ # Handle color as a tuple of floats or integers if isinstance(color, tuple): if len(color) == 3 or len(color) == 4: # Ensure all components are numbers if all(isinstance(c, (int, float)) for c in color): r, g, b = color[:3] a = color[3] if len(color) == 4 else 255 return ( max(0, min(255, int(round(r)))), max(0, min(255, int(round(g)))), max(0, min(255, int(round(b)))), max(0, min(255, int(round(a * 255)) if a <= 1 else round(a))), ) else: raise ValueError(f"Invalid color tuple length: {len(color)}") # Handle hex color codes if isinstance(color, str): color = color.strip() # Try to use PIL's ImageColor try: rgba = ImageColor.getcolor(color, "RGBA") return rgba except ValueError: pass # Handle 'rgba(r, g, b, a)' string format rgba_match = re.match(r'rgba\(\s*([0-9.]+),\s*([0-9.]+),\s*([0-9.]+),\s*([0-9.]+)\s*\)', color) if rgba_match: r, g, b, a = map(float, rgba_match.groups()) return ( max(0, min(255, int(round(r)))), max(0, min(255, int(round(g)))), max(0, min(255, int(round(b)))), max(0, min(255, int(round(a * 255)) if a <= 1 else round(a))), ) # Handle 'rgb(r, g, b)' string format rgb_match = re.match(r'rgb\(\s*([0-9.]+),\s*([0-9.]+),\s*([0-9.]+)\s*\)', color) if rgb_match: r, g, b = map(float, rgb_match.groups()) return ( max(0, min(255, int(round(r)))), max(0, min(255, int(round(g)))), max(0, min(255, int(round(b)))), 255, ) # If none of the above conversions work, raise an error raise ValueError(f"Invalid color format: {color}") def update_color_opacity(color, opacity): """ Updates the opacity of a color value. Parameters: color (tuple): A color represented as an RGB or RGBA tuple. opacity (int): An integer between 0 and 255 representing the desired opacity. Returns: tuple: The color as an RGBA tuple with the updated opacity. """ # Ensure opacity is within the valid range opacity = max(0, min(255, int(opacity))) if len(color) == 3: # Color is RGB, add the opacity to make it RGBA return color + (opacity,) elif len(color) == 4: # Color is RGBA, replace the alpha value with the new opacity return color[:3] + (opacity,) else: raise ValueError(f"Invalid color format: {color}. Must be an RGB or RGBA tuple.") def draw_text_with_emojis(image, text, font_color, offset_x, offset_y, font_name, font_size): """ Draws text with emojis directly onto the given PIL image at specified coordinates with the specified color. Parameters: image (PIL.Image.Image): The RGBA image to draw on. text (str): The text to draw, including emojis. font_color (tuple): RGBA color tuple for the text (e.g., (255, 0, 0, 255)). offset_x (int): The x-coordinate for the text center position. offset_y (int): The y-coordinate for the text center position. font_name (str): The name of the font family. font_size (int): Size of the font. Returns: None: The function modifies the image in place. """ if image.mode != 'RGBA': raise ValueError("Image must be in RGBA mode.") # Convert PIL image to a mutable bytearray img_data = bytearray(image.tobytes("raw", "BGRA")) # Create a Cairo ImageSurface that wraps the image's data surface = cairo.ImageSurface.create_for_data( img_data, cairo.FORMAT_ARGB32, image.width, image.height, image.width * 4 ) context = cairo.Context(surface) # Create Pango layout layout = pangocairocffi.create_layout(context) layout._set_text(text) # Set font description desc = pangocffi.FontDescription() desc._set_family(font_name) desc._set_size(pangocffi.units_from_double(font_size)) layout._set_font_description(desc) # Set text color r, g, b, a = parse_hex_color(font_color) context.set_source_rgba(r , g , b , a ) # Move to the position (top-left corner adjusted to center the text) context.move_to(offset_x, offset_y) # Render the text pangocairocffi.show_layout(context, layout) # Flush the surface to ensure all drawing operations are complete surface.flush() # Convert the modified bytearray back to a PIL Image modified_image = Image.frombuffer( "RGBA", (image.width, image.height), bytes(img_data), "raw", "BGRA", # Cairo stores data in BGRA order surface.get_stride(), ).convert("RGBA") return modified_image