File size: 8,204 Bytes
6ef117e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# 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 |