Spaces:
Running
on
Zero
Running
on
Zero
| # 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 |