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