|
|
|
import os |
|
import math |
|
from PIL import Image |
|
import cairocffi as cairo |
|
import pangocffi |
|
import pangocairocffi |
|
from PIL import Image, ImageDraw, ImageChops, ImageFont |
|
|
|
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 |
|
import utils.constants as 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 |
|
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 |
|
|
|
|
|
if rotation != 0: |
|
|
|
|
|
rotated_input_image = input_image.rotate(rotation, expand=True) |
|
rotated_image_width, rotated_image_height = rotated_input_image.size |
|
|
|
|
|
|
|
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)))) |
|
|
|
additional_width = rotated_image_width - image_width |
|
additional_height = rotated_image_height - image_height |
|
|
|
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 |
|
|
|
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 |
|
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)) |
|
|
|
def frange(start, stop, step): |
|
i = start |
|
while i < stop: |
|
yield i |
|
i += step |
|
|
|
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) |
|
|
|
if col % 2 == 1: |
|
y_offset -= (hex_height // 2) |
|
if rotated_input_image: |
|
|
|
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: |
|
|
|
filtered_colors = [color for color in sample_colors if color not in excluded_color_list] |
|
|
|
if 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), 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: |
|
|
|
|
|
rotated_image = image.rotate(-rotation, expand=True) |
|
|
|
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 |
|
|
|
|
|
if rotation != 0: |
|
|
|
|
|
rotated_input_image = input_image.rotate(rotation, expand=True) |
|
rotated_image_width, rotated_image_height = rotated_input_image.size |
|
|
|
|
|
|
|
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)))) |
|
|
|
additional_width = rotated_image_width - image_width |
|
additional_height = rotated_image_height - image_height |
|
|
|
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 |
|
|
|
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 |
|
hex_vertical_spacing = hex_height + (hex_border_size if hex_border_size < 0 else 0) + y_spacing |
|
col = 0 |
|
row = 0 |
|
|
|
if add_hex_text_option != "None": |
|
|
|
font_name = "Segoe UI Emoji" |
|
if os.name == 'nt': |
|
font_path = "./fonts/seguiemj.ttf" |
|
else: |
|
font_path = "./fonts/seguiemj.ttf" |
|
if not os.path.exists(font_path): |
|
raise FileNotFoundError("Emoji font not found in './fonts' directory.") |
|
|
|
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 = ast.literal_eval(custom_text_list) if custom_text_list else None |
|
if custom_text_color_list: |
|
|
|
color_list = ast.literal_eval(custom_text_color_list) if custom_text_color_list else None |
|
else: |
|
|
|
pass |
|
hex_index = -1 |
|
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)) |
|
|
|
def frange(start, stop, step): |
|
i = start |
|
while i < stop: |
|
yield i |
|
i += step |
|
|
|
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 |
|
x_offset = hex_width // 2 |
|
y_offset = (hex_height // 2) |
|
|
|
if col % 2 == 1: |
|
y_offset -= (hex_height // 2) |
|
if rotated_input_image: |
|
|
|
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: |
|
|
|
filtered_colors = [color for color in sample_colors if color not in excluded_color_list] |
|
|
|
if 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), 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) |
|
|
|
if add_hex_text_option != None: |
|
font_size = calculate_font_size(hex_size, 0.333, 20, 7) |
|
|
|
if font_size: |
|
font = ImageFont.truetype(font_path, font_size) |
|
|
|
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 |
|
|
|
if 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 |
|
text_color = update_color_opacity(hex_to_rgb(color_list[hex_index % len(color_list)]), opacity) |
|
else: |
|
|
|
text_color = border_color |
|
|
|
|
|
if text != None: |
|
print(f"Drawing Text: {text} color: {text_color} size: {font_size}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
text_x = x + x_offset - (hex_size / 1.75) |
|
text_y = y + y_offset - (hex_size / 1.25) |
|
|
|
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 |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
image.paste(font_image, (0, 0), font_image) |
|
if rotation != 0: |
|
|
|
rotated_image = image.rotate(-rotation, expand=True, fillcolor=background_color) |
|
bbox = rotated_image.split()[3].getbbox(alpha_only=False) |
|
|
|
if bbox: |
|
|
|
rotated_width, rotated_height = rotated_image.size |
|
|
|
|
|
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)}") |
|
|
|
|
|
left = bbox[0] + box_width_adjust |
|
upper = bbox[1] + bbox_height_adjust |
|
right = bbox[2] - box_width_adjust |
|
lower = bbox[3] - bbox_height_adjust |
|
|
|
|
|
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)) |
|
|
|
|
|
if right > left and lower > upper: |
|
|
|
cropped_image = rotated_image.crop((left, upper, right, lower)) |
|
|
|
final_image = cropped_image.resize((image_width, image_height)) |
|
else: |
|
|
|
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 |