File size: 32,027 Bytes
31931d9
 
 
 
 
 
 
8001a73
 
31931d9
 
 
 
 
8001a73
4a4069a
31931d9
 
 
7addd34
8001a73
 
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a4069a
 
 
 
2abe227
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8001a73
 
31931d9
 
 
4a4069a
 
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
4a4069a
 
 
 
8001a73
4a4069a
 
 
 
ba8de52
8001a73
 
4a4069a
8001a73
4a4069a
8001a73
 
4a4069a
 
 
 
 
 
 
 
 
 
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a4069a
31931d9
4a4069a
31931d9
 
4a4069a
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
2abe227
31931d9
ba8de52
 
 
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a4069a
 
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6ef117e
31931d9
 
 
 
 
 
 
 
6ef117e
31931d9
 
 
 
6ef117e
 
31931d9
 
4a4069a
 
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a4069a
 
 
 
 
 
 
 
 
ba8de52
4a4069a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2abe227
4a4069a
31931d9
4a4069a
31931d9
 
4a4069a
31931d9
 
 
 
 
 
 
47fbc1a
31931d9
6ef117e
 
47fbc1a
e0bbbbc
47fbc1a
e0bbbbc
31931d9
 
 
6ef117e
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6ef117e
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a4069a
31931d9
4a4069a
31931d9
4a4069a
31931d9
 
 
 
 
 
4a4069a
 
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2abe227
31931d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2abe227
31931d9
8001a73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# utils/hex_grid.py
import os
import math
from PIL import Image
import cairocffi as cairo
import pangocffi
import pangocairocffi
import numpy as np
import cv2
from PIL import Image, ImageDraw, ImageChops, ImageFont #, ImageColor
#from pilmoji import Pilmoji  # Import Pilmoji for handling emojis
from utils.excluded_colors import (
    excluded_color_list,
)
from utils.image_utils import alpha_composite_with_control, open_image
from utils.color_utils import update_color_opacity, parse_hex_color, draw_rotated_text_with_emojis, hex_to_rgb
import random  # For random text options
import utils.constants as constants  # Import constants
import ast
from utils.misc import number_to_letter
from utils.file_utils import get_file_parts
current_grid = None

def calculate_font_size(hex_size, padding=0.6, size_ceil=20, min_font_size=8):
    """
    Calculate the font size based on the hexagon size.

    Parameters:
        hex_size (int): The size of the hexagon side.
        padding (float): The fraction of the hex size to use for font size.

    Returns:
        int or None: The calculated font size or None if hex is too small.
    """
    font_size = int(hex_size * padding)
    if font_size < min_font_size:
        return None  # Hex is too small for text
    return min(font_size, size_ceil)

def map_sides(selected_side):
    mapping = {"triangle": 3, "square": 4, "hexagon": 6}
    return mapping[selected_side]

def generate_hexagon_grid(hex_size, border_size, input_image=None, image_width=0, image_height=0, start_x=0, start_y=0, end_x=0, end_y=0, rotation=0, background_color="#ede9ac44", border_color="#12165380", fill_hex=True, excluded_color_list=excluded_color_list, filter_color=False, x_spacing=0, y_spacing=0, sides=6):
    if input_image:
        image_width, image_height = input_image.size
    # Use half hex_size, thus do not double border size
    # Calculate the dimensions of the grid before rotation
    if rotation != 0:
        # Calculate rotated dimensions
        # modified to rotate input image and process to reduce calculation errors at edge conditions
        rotated_input_image = input_image.rotate(rotation, expand=True)
        rotated_image_width, rotated_image_height = rotated_input_image.size
        #rotated_image_height = abs(math.ceil((image_height ) * math.sin(math.radians(90 - rotation)) + (image_width ) * math.cos(math.radians(90 - rotation))))
        #rotated_image_width = abs(math.ceil((image_width ) * math.sin(math.radians(90 - rotation)) + (image_height ) * math.cos(math.radians(90 - rotation))))
        # Adjust hexagon size, spacing adjustments and border for rotation
        hex_size = abs(math.ceil((hex_size // 2) * math.sin(math.radians(90 - abs(rotation))) + (hex_size // 2) * math.cos(math.radians(90 - abs(rotation)))))
        hex_border_size = math.ceil(border_size * math.sin(math.radians(90 - abs(rotation))) + border_size * math.cos(math.radians(90 - abs(rotation))))
        x_spacing = math.ceil(x_spacing * math.sin(math.radians(90 - abs(rotation))) + x_spacing * math.cos(math.radians(90 - abs(rotation))))
        y_spacing = math.ceil(y_spacing * math.sin(math.radians(90 - abs(rotation))) + y_spacing * math.cos(math.radians(90 - abs(rotation))))
        # Calculate additional width and height due to rotation
        additional_width = rotated_image_width - image_width
        additional_height = rotated_image_height - image_height
        #rotated_input_image.show()
    else:
        rotated_input_image = input_image
        rotated_image_width = image_width 
        rotated_image_height = image_height
        hex_size = hex_size // 2
        hex_border_size = border_size
        additional_width = 0
        additional_height = 0
    # Create a new image with white background (adjusted for rotation)
    image = Image.new("RGBA", (rotated_image_width, rotated_image_height), background_color)
    draw = ImageDraw.Draw(image, mode="RGBA")
    hex_width = hex_size * 2
    hex_height = hex_size * 2
    hex_horizontal_spacing = (hex_width + (hex_border_size if hex_border_size < 0 else 0) + x_spacing) * ((6 / sides) if sides > 3 else 1.3333) #* 0.8660254
    hex_vertical_spacing = (hex_height + (hex_border_size if hex_border_size < 0 else 0) + y_spacing) * ((6 / sides) if sides > 3 else 3.0)
    col = 0
    row = 0
    def draw_hexagon(x, y, color="#FFFFFFFF", rotation=0, outline_color="#12165380", outline_width=0, sides=6):
        #side_length = (hex_size * 2) / math.sqrt(3) #hexagons only
        side_length = 2 * hex_size * math.tan(math.pi / sides) #hexagons, squares, triangle can tile
        points = [(x + side_length * math.cos(math.radians(angle + rotation)), y + side_length * math.sin(math.radians(angle + rotation))) for angle in range(0, 360, (360 // sides))]
        draw.polygon(points, fill=color, outline=outline_color, width=max(-5, outline_width))
    # Function to range a floating number
    def frange(start, stop, step):
        i = start
        while i < stop:
            yield i
            i += step
    # Draw hexagons 
    for y in frange(start_y, max(image_height + additional_height, image_height, rotated_image_height) + (end_y - start_y), hex_vertical_spacing):
        row += 1
        col = 0 
        for x in frange(start_x, max(image_width + additional_width, image_width, rotated_image_width) + (end_x - start_x), hex_horizontal_spacing):
            col += 1

            # Calculate offsets based on the number of sides
            if sides == 4:
                # Squares line up perfectly; no vertical offset is needed.
                x_offset = hex_size
                y_offset = 0
                rotation_offset = -45
            elif sides == 3:
                # For equilateral triangles, you might offset rows by about one-third
                # of the triangle�s height. Adjust as needed.
                x_offset = -hex_border_size * 2 #hex_width * math.tan(math.pi / sides) - hex_border_size * 2
                y_offset = -hex_border_size * 2
                rotation_offset = -60
                # Adjust y_offset for columns 1 and 2 to overlap
                if col % 2 == 1:
                    x_offset += int(hex_size * 0.8660254)
                    y_offset -= int(hex_height * 1.5) 
                    rotation_offset = 0
            else:
                # Default behavior (6 sides)
                x_offset = hex_width // 2
                y_offset = (hex_height // 2) #* 1.15470054342517
                rotation_offset = 0
                # Adjust y_offset for columns 1 and 3 to overlap
                if col % 2 == 1:
                    y_offset -= (hex_height // 2) #* 0.8660254

            if rotated_input_image:
                # Sample the colors of the pixels in the hexagon, if fill_hex is True
                if fill_hex:
                    sample_size = max(2, math.ceil(math.sqrt(hex_size)))
                    sample_x = int(x + x_offset)
                    sample_y = int(y + y_offset)
                    sample_colors = []
                    for i in range(-sample_size // 2, sample_size // 2 + 1):
                        for j in range(-sample_size // 2, sample_size // 2 + 1):
                            print(f"    Progress : {str(min(rotated_image_width - 1,max(1,sample_x + i)))}  {str(min(rotated_image_height - 1,max(1,sample_y + j)))}", end="\r")
                            sample_colors.append(rotated_input_image.getpixel((min(rotated_image_width - 1,max(1,sample_x + i)), min(rotated_image_height - 1,max(1,sample_y + j)))))
                    if filter_color:
                        # Filter out the excluded colors
                        filtered_colors = [color for color in sample_colors if color not in excluded_color_list]
                        # Ensure there are colors left after filtering
                        if filtered_colors:
                            # Calculate the average color of the filtered colors
                            avg_color = tuple(int(sum(channel) / len(filtered_colors)) for channel in zip(*filtered_colors))
                        else:
                            avg_color = excluded_color_list[0] if excluded_color_list else (0,0,0,0)
                    else:
                        avg_color = tuple(int(sum(channel) / len(sample_colors)) for channel in zip(*sample_colors))
                    if avg_color in excluded_color_list:
                        print(f"color excluded: {avg_color}")
                        avg_color = (0,0,0,0)
                    else:
                        print(f"color found: {avg_color}")
                        #draw_hexagon(x + x_offset, y + y_offset, color="#{:02x}{:02x}{:02x}{:02x}".format(*avg_color if fill_hex else (0,0,0,0)), outline_color=border_color, outline_width=hex_border_size)
                        draw_hexagon(x + x_offset, y + y_offset, color="#{:02x}{:02x}{:02x}{:02x}".format(*avg_color), rotation=rotation_offset, outline_color=border_color, outline_width=hex_border_size, sides=sides)
                else:
                    draw_hexagon(x + x_offset, y + y_offset, color="#000000", rotation=rotation_offset, outline_color=border_color, outline_width=hex_border_size, sides=sides)
            else:
                color = "#%02x%02x%02x%02x" % (128, math.ceil(y) % 255, math.ceil(x) % 255, 255) if fill_hex else (0,0,0,0)
                draw_hexagon(x + x_offset, y + y_offset, color=color, rotation=rotation_offset, outline_color=border_color, outline_width=hex_border_size, sides=sides)
    if rotation != 0:
        #image.show()
        # Rotate the final image
        rotated_image = image.rotate(-rotation, expand=True)
        #rotated_image.show()
        bbox = rotated_image.split()[3].getbbox(False)
        if bbox:
            final_image = rotated_image.crop(bbox).resize((image_width,image_height))
        else:
            final_image = rotated_image.resize((image_width,image_height))
    else:
        final_image = image
    return final_image

def generate_hexagon_grid_with_text(hex_size, border_size, input_image=None, image_width=0, image_height=0, start_x=0, start_y=0, end_x=0, end_y=0, rotation=0, background_color="#ede9ac44", border_color="#12165380", fill_hex=True, excluded_color_list=excluded_color_list, filter_color=False, x_spacing=0, y_spacing=0, sides=6,
        add_hex_text_option=None, custom_text_list=None, custom_text_color_list=None):
    if hex_size + x_spacing == 0 or hex_size + y_spacing == 0:
        print("Hexagon size and spacing cannot equal zero")
        raise ValueError("Hexagon size and spacing cannot equal zero")

    if input_image:
        image_width, image_height = input_image.size
    # Use half hex_size, thus do not double border size
    # Calculate the dimensions of the grid before rotation
    if rotation != 0:
        # Calculate rotated dimensions
        # modified to rotate input image and process to reduce calculation errors at edge conditions
        rotated_input_image = input_image.rotate(rotation, expand=True)
        rotated_image_width, rotated_image_height = rotated_input_image.size
        #rotated_image_height = abs(math.ceil((image_height ) * math.sin(math.radians(90 - rotation)) + (image_width ) * math.cos(math.radians(90 - rotation))))
        #rotated_image_width = abs(math.ceil((image_width ) * math.sin(math.radians(90 - rotation)) + (image_height ) * math.cos(math.radians(90 - rotation))))
        # Adjust hexagon size, spacing adjustments and border for rotation
        hex_size = abs(math.ceil((hex_size // 2) * math.sin(math.radians(90 - abs(rotation))) + (hex_size // 2) * math.cos(math.radians(90 - abs(rotation)))))
        hex_border_size = math.ceil(border_size * math.sin(math.radians(90 - abs(rotation))) + border_size * math.cos(math.radians(90 - abs(rotation))))
        x_spacing = math.ceil(x_spacing * math.sin(math.radians(90 - abs(rotation))) + x_spacing * math.cos(math.radians(90 - abs(rotation))))
        y_spacing = math.ceil(y_spacing * math.sin(math.radians(90 - abs(rotation))) + y_spacing * math.cos(math.radians(90 - abs(rotation))))
        # Calculate additional width and height due to rotation
        additional_width = rotated_image_width - image_width
        additional_height = rotated_image_height - image_height
        #rotated_input_image.show()
    else:
        rotated_input_image = input_image
        rotated_image_width = image_width 
        rotated_image_height = image_height
        hex_size = hex_size // 2
        hex_border_size = border_size
        additional_width = 0
        additional_height = 0
    # Create a new image with white background (adjusted for rotation)
    image = Image.new("RGBA", (rotated_image_width, rotated_image_height), background_color)
    font_image = Image.new("RGBA", (rotated_image_width, rotated_image_height), (0,0,0,0))
    draw = ImageDraw.Draw(image, mode="RGBA")
    hex_width = hex_size * 2
    hex_height = hex_size * 2
    hex_horizontal_spacing = (hex_width + (hex_border_size if hex_border_size < 0 else 0) + x_spacing) * ((6 / sides) if sides > 3 else 1.3333) #* 0.8660254
    hex_vertical_spacing = (hex_height + (hex_border_size if hex_border_size < 0 else 0) + y_spacing) * ((6 / sides) if sides > 3 else 3.0)
    col = 0
    row = 0
    ## Function to draw optional text
    if add_hex_text_option != "None":
        # Load the emoji font
        font_name = "Segoe UI Emoji"
        if os.name == 'nt':  # Windows
            font_path = "./fonts/seguiemj.ttf"
        else:  # Other OS (Linux, macOS, etc.)
            font_path = "./fonts/seguiemj.ttf"
        if not os.path.exists(font_path):
            raise FileNotFoundError("Emoji font not found in './fonts' directory.")
        # Prepare the text and color lists
        text_list = []
        color_list = []
        if add_hex_text_option == "Playing Cards Sequential":
            text_list = constants.cards
            color_list = constants.card_colors
        elif add_hex_text_option == "Playing Cards Alternate Red and Black":
            text_list = constants.cards_alternating
            color_list = constants.card_colors_alternating
        elif add_hex_text_option == "Custom List":
            if custom_text_list:
                #text_list = [text.strip() for text in custom_text_list.split(",")]
                text_list = ast.literal_eval(custom_text_list) if custom_text_list else None
            if custom_text_color_list:
                #color_list = [color.strip() for color in custom_text_color_list.split(",")]
                color_list = ast.literal_eval(custom_text_color_list) if custom_text_color_list else None
        else:
            # Coordinates will be generated dynamically
            pass
    hex_index = -1  # Initialize hex index
    def draw_hexagon(x, y, color="#FFFFFFFF", rotation=0, outline_color="#12165380", outline_width=0, sides=6):
        #side_length = (hex_size * 2) / math.sqrt(3) #hexagons only
        side_length = 2 * hex_size * math.tan(math.pi / sides) #hexagons, squares, triangle can tile
        points = [(x + side_length * math.cos(math.radians(angle + rotation)), y + side_length * math.sin(math.radians(angle + rotation))) for angle in range(0, 360, (360 // sides))]
        draw.polygon(points, fill=color, outline=outline_color, width=max(-5, outline_width))
    # Function to range a floating number
    def frange(start, stop, step):
        i = start
        while i < stop:
            yield i
            i += step
    # Draw hexagons 
    for y in frange(start_y, max(image_height + additional_height, image_height, rotated_image_height) + (end_y - start_y), hex_vertical_spacing):
        row += 1
        col = 0 
        for x in frange(start_x, max(image_width + additional_width, image_width, rotated_image_width) + (end_x - start_x), hex_horizontal_spacing):
            col += 1
            hex_index += 1  # Increment hex index

            # Calculate offsets based on the number of sides
            if sides == 4:
                # Squares line up perfectly; no vertical offset is needed.
                x_offset = hex_size
                y_offset = 0
                rotation_offset = -45
            elif sides == 3:
                # For equilateral triangles, you might offset rows by about one-third
                # of the triangle�s height. Adjust as needed.
                x_offset = -hex_border_size * 2 #hex_width * math.tan(math.pi / sides) - hex_border_size * 2
                y_offset = -hex_border_size * 2
                rotation_offset = -60
                # Adjust y_offset for columns 1 and 2 to overlap
                if col % 2 == 1:
                    x_offset += int(hex_size * 0.8660254)
                    y_offset -= int(hex_height * 1.5) 
                    rotation_offset = 0
            else:
                # Default behavior (6 sides)
                x_offset = hex_width // 2
                y_offset = (hex_height // 2) #* 1.15470054342517
                rotation_offset = 0
                # Adjust y_offset for columns 1 and 3 to overlap
                if col % 2 == 1:
                    y_offset -= (hex_height // 2) #* 0.8660254

            if rotated_input_image:
                # Sample the colors of the pixels in the hexagon, if fill_hex is True
                if fill_hex:
                    sample_size = max(2, math.ceil(math.sqrt(hex_size)))
                    sample_x = int(x + x_offset)
                    sample_y = int(y + y_offset)
                    sample_colors = []
                    for i in range(-sample_size // 2, sample_size // 2 + 1):
                        for j in range(-sample_size // 2, sample_size // 2 + 1):
                            print(f"    Progress : {str(min(rotated_image_width - 1,max(1,sample_x + i)))}  {str(min(rotated_image_height - 1,max(1,sample_y + j)))}", end="\r")
                            sample_colors.append(rotated_input_image.getpixel((min(rotated_image_width - 1,max(1,sample_x + i)), min(rotated_image_height - 1,max(1,sample_y + j)))))
                    if filter_color:
                        # Filter out the excluded colors
                        filtered_colors = [color for color in sample_colors if color not in excluded_color_list]
                        # Ensure there are colors left after filtering
                        if filtered_colors:
                            # Calculate the average color of the filtered colors
                            avg_color = tuple(int(sum(channel) / len(filtered_colors)) for channel in zip(*filtered_colors))
                        else:
                            avg_color = excluded_color_list[0] if excluded_color_list else (0,0,0,0)
                    else:
                        avg_color = tuple(int(sum(channel) / len(sample_colors)) for channel in zip(*sample_colors))
                    if avg_color in excluded_color_list:
                        print(f"color excluded: {avg_color}")
                        avg_color = (0,0,0,0)
                    else:
                        print(f"color found: {avg_color}")
                        #draw_hexagon(x + x_offset, y + y_offset, color="#{:02x}{:02x}{:02x}{:02x}".format(*avg_color if fill_hex else (0,0,0,0)), outline_color=border_color, outline_width=hex_border_size, sides=sides)
                        draw_hexagon(x + x_offset, y + y_offset, color="#{:02x}{:02x}{:02x}{:02x}".format(*avg_color), rotation=rotation_offset, outline_color=border_color, outline_width=hex_border_size, sides=sides)
                else:
                    draw_hexagon(x + x_offset, y + y_offset, color="#00000000", rotation=rotation_offset, outline_color=border_color, outline_width=hex_border_size, sides=sides)
            else:
                color = "#%02x%02x%02x%02x" % (128, math.ceil(y) % 255, math.ceil(x) % 255, 255) if fill_hex else (0,0,0,0)
                draw_hexagon(x + x_offset, y + y_offset, color=color, rotation=rotation_offset, outline_color=border_color, outline_width=hex_border_size, sides=sides)
            # Draw text in hexagon
            if add_hex_text_option != None:
                font_size = calculate_font_size(hex_size, 0.333, 20, 7)
                # Skip drawing text if font size is too small
                if font_size:
                    font = ImageFont.truetype(font_path, font_size)
                    # Determine the text to draw
                    if add_hex_text_option == "Column-Row Coordinates":
                        text = f"{col},{row}"
                    elif add_hex_text_option == "Sequential Numbers":
                        text = f"{hex_index}"
                    elif add_hex_text_option == "Column(Letter)-Row Coordinates":
                        text = f"{number_to_letter(col)}{row}"
                    elif add_hex_text_option == "Column-Row(Letter) Coordinates":
                        text = f"{col}{number_to_letter(row)}"
                    elif text_list:
                        text = text_list[hex_index % len(text_list)]
                    else:
                        text = None
                    # Determine the text color
                    if color_list:
                        # Extract the opacity from the border color and add to the color_list
                        if isinstance(border_color, str):
                            opacity = int(border_color[-2:], 16)
                        elif isinstance(border_color, tuple) and len(border_color) == 4:
                            opacity = border_color[3]
                        else:
                            opacity = 255  # Default to full opacity if format is unexpected
                        text_color = update_color_opacity(hex_to_rgb(color_list[hex_index % len(color_list)]), opacity)
                    else:
                        # Use border color and opacity
                        text_color = border_color 
                        #text_color = "#{:02x}{:02x}{:02x}{:02x}".format(*text_color)
                    # Skip if text is empty
                    if text != None:
                        print(f"Drawing Text: {text} color: {text_color} size: {font_size}")
                        # Calculate text size using Pango
                        # Create a temporary surface to calculate text size
                        # temp_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
                        # temp_context = cairo.Context(temp_surface)
                        # temp_layout = pangocairocffi.create_layout(temp_context)
                        # temp_layout._set_text(text)
                        # temp_desc = pangocffi.FontDescription()
                        # temp_desc._set_family(font_name)
                        # temp_desc._set_size(pangocffi.units_from_double(font_size))
                        # temp_layout._set_font_description(temp_desc)
                        # pangocairocffi.show_layout(temp_context, temp_layout)
                        # ink_rect, logical_rect = temp_layout.get_extents()
                        # text_width = logical_rect.width
                        # text_height = logical_rect.height                
                        # Calculate position to center text in hexagon
                        # text_x = x + x_offset - (text_width / 2)
                        # text_y = y + y_offset - (text_height / 2)
                        # Calculate position to top left text in hexagon
                        text_x = x + x_offset - (hex_size / 1.75)
                        text_y = y + y_offset - (hex_size / 1.75)
                        # Draw the text directly onto the image
                        font_image = draw_rotated_text_with_emojis(
                            image=font_image,
                            text=text,
                            font_color=update_color_opacity(text_color,255),
                            offset_x=text_x,
                            offset_y=text_y,
                            font_name=font_name,
                            font_size=font_size,
                            angle = -1.0 * rotation
                        )                        
                        # # Use Pilmoji to draw text with emojis
                        # with Pilmoji(image) as pilmoji:
                        #     # Calculate text size
                        #     w, h = pilmoji.getsize(text, font=font)
                        #     # Calculate position to center text in hexagon
                        #     text_x = x + x_offset - w / 2
                        #     text_y = y + y_offset - h / 2
                        #     # Draw text
                        #     pilmoji.text(
                        #         (text_x, text_y),
                        #         text,
                        #         font=font,
                        #         fill=text_color
                        #     )
    image.paste(font_image, (0, 0), font_image)
    if rotation != 0:
        # Rotate the final image
        rotated_image = image.rotate(-rotation, expand=True, fillcolor=background_color)
        bbox = rotated_image.split()[3].getbbox(alpha_only=False)

        if bbox:
            # Calculate the size of the rotated image
            rotated_width, rotated_height = rotated_image.size

            # Calculate the size of the cropped area
            box_width = bbox[2] - bbox[0]
            box_height = bbox[3] - bbox[1]

            box_width_adjust = (box_width - image_width) / 2
            bbox_height_adjust = (box_height - image_height) / 2

            print(f"\nbbox: {bbox}: size: {(image_width, image_height)} estimated size: {(box_width, box_height)}")

            # Calculate adjusted box coordinates
            left = bbox[0] + box_width_adjust
            upper = bbox[1] + bbox_height_adjust
            right = bbox[2] - box_width_adjust
            lower = bbox[3] - bbox_height_adjust

            # Ensure coordinates are within image bounds
            left = max(0, min(left, rotated_width))
            upper = max(0, min(upper, rotated_height))
            right = max(0, min(right, rotated_width))
            lower = max(0, min(lower, rotated_height))

            # Ensure the box has positive width and height
            if right > left and lower > upper:
                # Crop the image using the adjusted box
                cropped_image = rotated_image.crop((left, upper, right, lower))
                # Resize the cropped image to the desired size
                final_image = cropped_image.resize((image_width, image_height))
            else:
                # If the box is invalid, resize the entire rotated image
                final_image = rotated_image.resize((image_width, image_height))
        else:
            final_image = rotated_image.resize((image_width, image_height))
    else:
        final_image = image
    return final_image

def generate_hexagon_grid_interface(hex_size, border_size, image, start_x, start_y, end_x, end_y, rotation, background_color, border_color, fill_hex, excluded_color_list, filter_color, x_spacing, y_spacing, add_hex_text_option=None, custom_text_list=None, custom_text_color_list=None, sides=6):
    print(f"Generating Hexagon Grid with Parameters: Hex Size: {hex_size}, Border Size: {border_size}, Start X: {start_x}, Start Y: {start_y}, End X: {end_x}, End Y: {end_y}, Rotation: {rotation}, Background Color: {background_color}, Border Color: {border_color}, Fill Hex: {fill_hex}, Excluded Color List: {excluded_color_list}, Filter Color: {filter_color}, X Spacing: {x_spacing}, Y Spacing: {y_spacing}, add Text Option {add_hex_text_option}\n")
    hexagon_grid_image = generate_hexagon_grid_with_text(
        hex_size=abs(hex_size),
        border_size=border_size,
        input_image=image,
        start_x=start_x,
        start_y=start_y,
        end_x=end_x,
        end_y=end_y,
        rotation=rotation,
        background_color=background_color,
        border_color=border_color,
        fill_hex = fill_hex,
        excluded_color_list = excluded_color_list,
        filter_color = filter_color, 
        x_spacing = x_spacing if abs(hex_size) > abs(x_spacing) else (hex_size if x_spacing >= 0 else -hex_size), 
        y_spacing = y_spacing if abs(hex_size) > abs(y_spacing) else (hex_size if y_spacing >= 0 else -hex_size),
        add_hex_text_option = add_hex_text_option,
        custom_text_list = custom_text_list,
        custom_text_color_list= custom_text_color_list, sides=sides
    )
    overlay_image = alpha_composite_with_control(image, hexagon_grid_image, 50)
    return hexagon_grid_image, overlay_image


def transform_grid(grid_path, tilt_angle=0, rotation_angle=0):
    """
    Transform a 2D grid image with a perspective tilt and optional rotation.
    
    Args:
        grid_path (str): Filepath to the 2D grid image.
        tilt_angle (float): Tilt angle in degrees (0 to 90) for z-axis perspective.
        rotation_angle (float): Rotation angle in degrees (0 to 360) in the x-y plane.
    
    Returns:
        str: Filepath to the transformed grid image.
    """
    if grid_path is None or tilt_angle is None or rotation_angle is None:
        return grid_path

    global current_grid
    if "_transform" not in grid_path:
        #save current grid for next round
        current_grid = grid_path
    else:
        grid_path = current_grid    

    # Load the grid image
    grid_original = open_image(grid_path).convert('RGBA')  # RGBA for transparency
    grid = grid_original.copy()
    width, height = grid.size

    # Step 1: Rotate the grid in the x-y plane (around z-axis) if needed
    if rotation_angle != 0:
        grid = grid.rotate(rotation_angle, expand=True, fillcolor=(0, 0, 0, 0))
        # Resize back to original dimensions if necessary
        if grid.size != (width, height):
            grid = grid.resize((width, height), Image.Resampling.LANCZOS)

    # Step 2: Define the perspective transformation
    # Original corners of the grid (in grid space)
    src_pts = np.array([
        [0, 0],           # Top-left
        [width, 0],       # Top-right
        [width, height],  # Bottom-right
        [0, height]       # Bottom-left
    ], dtype=np.float32)

    # Calculate the perspective shift based on tilt_angle
    # Tilt angle of 0 means no perspective (flat), 90 means extreme perspective
    # We simulate the top moving away by shrinking the top width
    perspective_factor = np.sin(np.radians(tilt_angle))  # 0 to 1
    top_width_shrink = width * (1 - perspective_factor * 0.8)  # Shrink top by up to 80%
    top_shift = (width - top_width_shrink) / 2

    # Destination points after perspective transform
    dst_pts = np.array([
        [top_shift, 0],                    # Top-left
        [width - top_shift, 0],            # Top-right
        [width, height],                   # Bottom-right
        [0, height]                        # Bottom-left
    ], dtype=np.float32)

    # Step 3: Compute the perspective transformation matrix
    M = cv2.getPerspectiveTransform(src_pts, dst_pts)

    # Step 4: Apply the perspective transformation using OpenCV
    grid_np = np.array(grid)
    transformed = cv2.warpPerspective(
        grid_np,
        M,
        (width, height),
        flags=cv2.INTER_LINEAR,
        borderMode=cv2.BORDER_CONSTANT,
        borderValue=(0, 0, 0, 0)  # Transparent background
    )

    # Step 5: Convert back to PIL image
    transformed_image = Image.fromarray(transformed)

    # Step 6: Save the result
    directory, _, name, _, new_ext = get_file_parts(grid_path)
    if constants.TMPDIR:
        directory = constants.TMPDIR
    #save original image to temp folder
    # output_path = os.path.join(directory, name + new_ext)
    # grid_original.save(output_path)
    # save new image to temp folder
    new_filename = name + "_transform" + new_ext
    output_path = os.path.join(directory, new_filename)
    transformed_image.save(output_path)

    return output_path