Spaces:
Running
on
Zero
Running
on
Zero
# 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 |