# utils/hex_grid.py import os import math from PIL import Image import cairocffi as cairo import pangocffi import pangocairocffi 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 multiply_and_blend_images from utils.color_utils import update_color_opacity, parse_hex_color, draw_text_with_emojis, hex_to_rgb import random # For random text options import utils.constants as constants # Import constants import ast 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 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): 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 #* 0.8660254 hex_vertical_spacing = hex_height + (hex_border_size if hex_border_size < 0 else 0) + y_spacing 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) 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 x_offset = hex_width // 2 y_offset = (hex_height // 2) #* 1.15470054342517 # 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), outline_color=border_color, outline_width=hex_border_size) else: draw_hexagon(x + x_offset, y + y_offset, color="#000000", outline_color=border_color, outline_width=hex_border_size) 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, outline_color=border_color, outline_width=hex_border_size) 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, add_hex_text_option=None, custom_text_list=None, custom_text_color_list=None): 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 #* 0.8660254 hex_vertical_spacing = hex_height + (hex_border_size if hex_border_size < 0 else 0) + y_spacing 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) 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 x_offset = hex_width // 2 y_offset = (hex_height // 2) #* 1.15470054342517 # 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), outline_color=border_color, outline_width=hex_border_size) else: draw_hexagon(x + x_offset, y + y_offset, color="#00000000", outline_color=border_color, outline_width=hex_border_size) 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, outline_color=border_color, outline_width=hex_border_size) # 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 == "Row-Column Coordinates": text = f"{col},{row}" elif add_hex_text_option == "Sequential Numbers": text = f"{hex_index}" 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 center text in hexagon text_x = x + x_offset - (hex_size / 1.75) text_y = y + y_offset - (hex_size / 1.25) # Draw the text directly onto the image font_image = draw_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 ) # # 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): 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 ) overlay_image = multiply_and_blend_images(image, hexagon_grid_image, 50) return hexagon_grid_image, overlay_image