HexaGrid / utils /color_utils.py
Surn's picture
Merge from Main repository
6ef117e
# 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