|
|
|
|
|
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) |
|
|
|
|
|
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 |
|
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 |
|
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) |
|
|
|
|
|
def hex_to_rgb(hex): |
|
if hex.startswith("#"): |
|
clean_hex = hex.replace('#','') |
|
|
|
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. |
|
""" |
|
|
|
if isinstance(color, tuple): |
|
if len(color) == 3 or len(color) == 4: |
|
|
|
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)}") |
|
|
|
if isinstance(color, str): |
|
color = color.strip() |
|
|
|
try: |
|
rgba = ImageColor.getcolor(color, "RGBA") |
|
return rgba |
|
except ValueError: |
|
pass |
|
|
|
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))), |
|
) |
|
|
|
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, |
|
) |
|
|
|
|
|
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. |
|
""" |
|
|
|
opacity = max(0, min(255, int(opacity))) |
|
|
|
if len(color) == 3: |
|
|
|
return color + (opacity,) |
|
elif len(color) == 4: |
|
|
|
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.") |
|
|
|
img_data = bytearray(image.tobytes("raw", "BGRA")) |
|
|
|
surface = cairo.ImageSurface.create_for_data( |
|
img_data, |
|
cairo.FORMAT_ARGB32, |
|
image.width, |
|
image.height, |
|
image.width * 4 |
|
) |
|
context = cairo.Context(surface) |
|
|
|
layout = pangocairocffi.create_layout(context) |
|
layout._set_text(text) |
|
|
|
desc = pangocffi.FontDescription() |
|
desc._set_family(font_name) |
|
desc._set_size(pangocffi.units_from_double(font_size)) |
|
layout._set_font_description(desc) |
|
|
|
r, g, b, a = parse_hex_color(font_color) |
|
context.set_source_rgba(r , g , b , a ) |
|
|
|
context.move_to(offset_x, offset_y) |
|
|
|
pangocairocffi.show_layout(context, layout) |
|
|
|
surface.flush() |
|
|
|
modified_image = Image.frombuffer( |
|
"RGBA", |
|
(image.width, image.height), |
|
bytes(img_data), |
|
"raw", |
|
"BGRA", |
|
surface.get_stride(), |
|
).convert("RGBA") |
|
return modified_image |