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 | |
from utils.misc import number_to_letter | |
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 add_hex_text_option == "Column Letter, Row Number": | |
text = f"{number_to_letter(col)},{row}" | |
elif add_hex_text_option == "Column Number, Row Letter": | |
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 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 |