Spaces:
Runtime error
Runtime error
| # modules/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 |