multimodalart's picture
Squashing commit
4450790 verified
#---------------------------------------------------------------------------------------------------------------------#
# Comfyroll Studio custom nodes by RockOfFire and Akatsuzi https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes
# for ComfyUI https://github.com/comfyanonymous/ComfyUI
#---------------------------------------------------------------------------------------------------------------------#
import numpy as np
import torch
import os
from PIL import Image, ImageDraw, ImageOps, ImageFont
from server import PromptServer, BinaryEventTypes
from ..categories import icons
from ..config import color_mapping, COLORS
from .functions_graphics import *
from .functions_upscale import apply_resize_image
font_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "fonts")
file_list = [f for f in os.listdir(font_dir) if os.path.isfile(os.path.join(font_dir, f)) and f.lower().endswith(".ttf")]
#---------------------------------------------------------------------------------------------------------------------#
ALIGN_OPTIONS = ["top", "center", "bottom"]
ROTATE_OPTIONS = ["text center", "image center"]
JUSTIFY_OPTIONS = ["left", "center", "right"]
PERSPECTIVE_OPTIONS = ["top", "bottom", "left", "right"]
#---------------------------------------------------------------------------------------------------------------------#
class CR_SimpleMemeTemplate:
@classmethod
def INPUT_TYPES(s):
font_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "fonts")
file_list = [f for f in os.listdir(font_dir) if os.path.isfile(os.path.join(font_dir, f)) and f.lower().endswith(".ttf")]
bar_opts = ["no bars", "top", "bottom", "top and bottom"]
simple_meme_presets = ["custom",
"One Does Not Simply ... MEME IN COMFY",
"This is fine.",
"Good Morning ... No Such Thing!"]
return {"required": {
"image": ("IMAGE",),
"preset": (simple_meme_presets,),
"text_top": ("STRING", {"multiline": True, "default": "text_top"}),
"text_bottom": ("STRING", {"multiline": True, "default": "text_bottom"}),
"font_name": (file_list,),
"max_font_size": ("INT", {"default": 150, "min": 20, "max": 2048}),
"font_color": (COLORS,),
"font_outline": (["none", "thin", "thick", "extra thick"],),
"bar_color": (COLORS,),
"bar_options": (bar_opts,),
},
"optional": {
"font_color_hex": ("STRING", {"multiline": False, "default": "#000000"}),
"bar_color_hex": ("STRING", {"multiline": False, "default": "#000000"})
}
}
RETURN_TYPES = ("IMAGE", "STRING", )
RETURN_NAMES = ("image", "show_help", )
FUNCTION = "make_meme"
CATEGORY = icons.get("Comfyroll/Graphics/Template")
def make_meme(self, image, preset,
text_top, text_bottom,
font_name, max_font_size, font_color, font_outline,
bar_color, bar_options,
font_color_hex='#000000', bar_color_hex='#000000'):
# Get RGB values for the text and bar colors
text_color = get_color_values(font_color, font_color_hex, color_mapping)
bar_color = get_color_values(bar_color, bar_color_hex, color_mapping)
total_images = []
for img in image:
# Calculate the height factor
if bar_options == "top":
height_factor = 1.2
elif bar_options == "bottom":
height_factor = 1.2
elif bar_options == "top and bottom":
height_factor = 1.4
else:
height_factor = 1.0
if preset == "One Does Not Simply ... MEME IN COMFY":
text_top = "One Does Not Simply"
text_bottom = "MEME IN COMFY"
if preset == "This is fine.":
text_top = "This is fine."
text_bottom = ""
if preset == "Good Morning ... No Such Thing!":
text_top = "Good Morning"
text_bottom = "\"No Such Thing!\""
# Create PIL images for the image and text bars
back_image = tensor2pil(img)
size = back_image.width, int(back_image.height * height_factor)
result_image = Image.new("RGB", size)
# Define font settings
#font_file = "fonts\\" + str(font_name)
font_file = os.path.join("fonts", font_name)
resolved_font_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), font_file)
# Create the drawing context
draw = ImageDraw.Draw(result_image)
# Create two color bars at the top and bottom
bar_width = back_image.width
bar_height = back_image.height // 5 ### add parameter for this in adv node
top_bar = Image.new("RGB", (bar_width, bar_height), bar_color)
bottom_bar = Image.new("RGB", (bar_width, bar_height), bar_color)
# Composite the result image onto the input image
if bar_options == "top" or bar_options == "top and bottom":
image_out = result_image.paste(back_image, (0, bar_height))
else:
image_out = result_image.paste(back_image, (0, 0))
# Get the font size and draw the text
if bar_options == "top" or bar_options == "top and bottom":
result_image.paste(top_bar, (0, 0))
font_top = get_font_size(draw, text_top, bar_width, bar_height, resolved_font_path, max_font_size)
draw_text_on_image(draw, 0, bar_width, bar_height, text_top, font_top, text_color, font_outline)
if bar_options == "bottom" or bar_options == "top and bottom":
result_image.paste(bottom_bar, (0, (result_image.height - bar_height)))
font_bottom = get_font_size(draw, text_bottom, bar_width, bar_height, resolved_font_path, max_font_size)
if bar_options == "bottom":
y_position = back_image.height
else:
y_position = bar_height + back_image.height
draw_text_on_image(draw, y_position, bar_width, bar_height, text_bottom, font_bottom, text_color, font_outline)
# Overlay text on image
if bar_options == "bottom" and text_top > "":
font_top = get_font_size(draw, text_top, bar_width, bar_height, resolved_font_path, max_font_size)
draw_text_on_image(draw, 0, bar_width, bar_height, text_top, font_top, text_color, font_outline)
if (bar_options == "top" or bar_options == "none") and text_bottom > "":
font_bottom = get_font_size(draw, text_bottom, bar_width, bar_height, resolved_font_path, max_font_size)
y_position = back_image.height
draw_text_on_image(draw, y_position, bar_width, bar_height, text_bottom, font_bottom, text_color, font_outline)
if bar_options == "no bars" and text_bottom > "":
font_bottom = get_font_size(draw, text_bottom, bar_width, bar_height, resolved_font_path, max_font_size)
y_position = back_image.height - bar_height
draw_text_on_image(draw, y_position, bar_width, bar_height, text_bottom, font_bottom, text_color, font_outline)
if bar_options == "no bars" and text_top > "":
font_top = get_font_size(draw, text_top, bar_width, bar_height, resolved_font_path, max_font_size)
draw_text_on_image(draw, 0, bar_width, bar_height, text_top, font_top, text_color, font_outline)
#image_out = np.array(result_image).astype(np.float32) / 255.0
#image_out = torch.from_numpy(image_out).unsqueeze(0)
# Convert the PIL image back to a torch tensor
#return (pil2tensor(image_out), show_help, )
#return (image_out, show_help, )
# Convert to tensor
out_image = np.array(result_image.convert("RGB")).astype(np.float32) / 255.0
out_image = torch.from_numpy(out_image).unsqueeze(0)
total_images.append(out_image)
# Batch the images
images_out = torch.cat(total_images, 0)
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Template-Nodes#cr-simple-meme-template"
return (images_out, show_help, )
#---------------------------------------------------------------------------------------------------------------------#
class CR_SimpleBanner:
@classmethod
def INPUT_TYPES(s):
font_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "fonts")
file_list = [f for f in os.listdir(font_dir) if os.path.isfile(os.path.join(font_dir, f)) and f.lower().endswith(".ttf")]
return {"required": {
"image": ("IMAGE",),
#"image_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}),
"banner_text": ("STRING", {"multiline": True, "default": "text"}),
"font_name": (file_list,),
"max_font_size": ("INT", {"default": 150, "min": 20, "max": 2048}),
"font_color": (COLORS,),
"outline_thickness": ("INT", {"default": 0, "min": 0, "max": 500}),
"outline_color": (COLORS,),
#"text_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}),
#"drop_shadow_angle": ("INT", {"default": 0, "min": 0, "max": 500}),
#"drop_shadow_offset": ("INT", {"default": 0, "min": 0, "max": 500}),
#"drop_shadow_color": (COLORS,),
#"drop_shadow_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}),
#"wrap_text": (["true", "false"],),
"margin_size": ("INT", {"default": 0, "min": 0, "max": 500}),
},
"optional": {
"font_color_hex": ("STRING", {"multiline": False, "default": "#000000"}),
"outline_color_hex": ("STRING", {"multiline": False, "default": "#000000"}),
}
}
RETURN_TYPES = ("IMAGE", "STRING", )
RETURN_NAMES = ("image", "show_help", )
FUNCTION = "make_banner"
CATEGORY = icons.get("Comfyroll/Graphics/Template")
def make_banner(self, image, banner_text,
font_name, max_font_size, font_color,
outline_thickness, outline_color, margin_size,
font_color_hex='#000000', outline_color_hex='#000000'):
# Get RGB values for the text and bar colors
text_color = get_color_values(font_color, font_color_hex, color_mapping)
outline_color = get_color_values(outline_color, outline_color_hex, color_mapping)
total_images = []
for img in image:
# Create PIL images for the image and text bars
back_image = tensor2pil(img).convert("RGBA")
size = back_image.width, back_image.height
#result_image = Image.new("RGB", size)
# Define font settings
font_file = os.path.join("fonts", font_name)
resolved_font_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), font_file)
# Create the drawing context
draw = ImageDraw.Draw(back_image)
area_width = back_image.width - (margin_size * 2)
area_height = back_image.width - (margin_size * 2)
# Get the font size and draw the text
font = get_font_size(draw, banner_text, area_width, area_height, resolved_font_path, max_font_size)
x = back_image.width // 2
y = back_image.height // 2
if outline_thickness > 0:
draw.text((x, y), banner_text, fill=text_color, font=font, anchor='mm', stroke_width=outline_thickness, stroke_fill=outline_color)
else:
draw.text((x, y), banner_text, fill=text_color, font=font, anchor='mm')
# Convert to tensor
out_image = np.array(back_image.convert("RGB")).astype(np.float32) / 255.0
out_image = torch.from_numpy(out_image).unsqueeze(0)
total_images.append(out_image)
# Batch the images
images_out = torch.cat(total_images, 0)
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Template-Nodes#cr-simple-banner"
return (images_out, show_help, )
#---------------------------------------------------------------------------------------------------------------------#
class CR_ComicPanelTemplates:
@classmethod
def INPUT_TYPES(s):
directions = ["left to right", "right to left"]
templates = ["custom",
"G22", "G33",
"H2", "H3",
"H12", "H13",
"H21", "H23",
"H31", "H32",
"V2", "V3",
"V12", "V13",
"V21", "V23",
"V31", "V32"]
return {"required": {
"page_width": ("INT", {"default": 512, "min": 8, "max": 4096}),
"page_height": ("INT", {"default": 512, "min": 8, "max": 4096}),
"template": (templates,),
"reading_direction": (directions,),
"border_thickness": ("INT", {"default": 5, "min": 0, "max": 1024}),
"outline_thickness": ("INT", {"default": 2, "min": 0, "max": 1024}),
"outline_color": (COLORS,),
"panel_color": (COLORS,),
"background_color": (COLORS,),
},
"optional": {
"images": ("IMAGE",),
"custom_panel_layout": ("STRING", {"multiline": False, "default": "H123"}),
"outline_color_hex": ("STRING", {"multiline": False, "default": "#000000"}),
"panel_color_hex": ("STRING", {"multiline": False, "default": "#000000"}),
"bg_color_hex": ("STRING", {"multiline": False, "default": "#000000"}),
}
}
RETURN_TYPES = ("IMAGE", "STRING", )
RETURN_NAMES = ("image", "show_help", )
FUNCTION = "layout"
CATEGORY = icons.get("Comfyroll/Graphics/Template")
def layout(self, page_width, page_height, template, reading_direction,
border_thickness, outline_thickness,
outline_color, panel_color, background_color,
images=None, custom_panel_layout='G44',
outline_color_hex='#000000', panel_color_hex='#000000', bg_color_hex='#000000'):
panels = []
k = 0
len_images = 0
# Convert tensor images to PIL
if images is not None:
images = [tensor2pil(image) for image in images]
len_images = len(images)
# Get RGB values for the text and background colors
outline_color = get_color_values(outline_color, outline_color_hex, color_mapping)
panel_color = get_color_values(panel_color, panel_color_hex, color_mapping)
bg_color = get_color_values(background_color, bg_color_hex, color_mapping)
# Create page and apply bg color
size = (page_width - (2 * border_thickness), page_height - (2 * border_thickness))
page = Image.new('RGB', size, bg_color)
draw = ImageDraw.Draw(page)
if template == "custom":
template = custom_panel_layout
# Calculate panel positions and add to bg image
first_char = template[0]
if first_char == "G":
rows = int(template[1])
columns = int(template[2])
panel_width = (page.width - (2 * columns * (border_thickness + outline_thickness))) // columns
panel_height = (page.height - (2 * rows * (border_thickness + outline_thickness))) // rows
# Row loop
for i in range(rows):
# Column Loop
for j in range(columns):
# Draw the panel
create_and_paste_panel(page, border_thickness, outline_thickness,
panel_width, panel_height, page.width,
panel_color, bg_color, outline_color,
images, i, j, k, len_images, reading_direction)
k += 1
elif first_char == "H":
rows = len(template) - 1
panel_height = (page.height - (2 * rows * (border_thickness + outline_thickness))) // rows
for i in range(rows):
columns = int(template[i+1])
panel_width = (page.width - (2 * columns * (border_thickness + outline_thickness))) // columns
for j in range(columns):
# Draw the panel
create_and_paste_panel(page, border_thickness, outline_thickness,
panel_width, panel_height, page.width,
panel_color, bg_color, outline_color,
images, i, j, k, len_images, reading_direction)
k += 1
elif first_char == "V":
columns = len(template) - 1
panel_width = (page.width - (2 * columns * (border_thickness + outline_thickness))) // columns
for j in range(columns):
rows = int(template[j+1])
panel_height = (page.height - (2 * rows * (border_thickness + outline_thickness))) // rows
for i in range(rows):
# Draw the panel
create_and_paste_panel(page, border_thickness, outline_thickness,
panel_width, panel_height, page.width,
panel_color, bg_color, outline_color,
images, i, j, k, len_images, reading_direction)
k += 1
# Add a border to the page
if border_thickness > 0:
page = ImageOps.expand(page, border_thickness, bg_color)
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Template-Nodes#cr-comic-panel-templates"
return (pil2tensor(page), show_help, )
#---------------------------------------------------------------------------------------------------------------------#
class CR_SimpleImageCompare:
@classmethod
def INPUT_TYPES(s):
font_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "fonts")
file_list = [f for f in os.listdir(font_dir) if os.path.isfile(os.path.join(font_dir, f)) and f.lower().endswith(".ttf")]
return {"required": {
"text1": ("STRING", {"multiline": True, "default": "text"}),
"text2": ("STRING", {"multiline": True, "default": "text"}),
"footer_height": ("INT", {"default": 100, "min": 0, "max": 1024}),
"font_name": (file_list,),
"font_size": ("INT", {"default": 50, "min": 0, "max": 1024}),
"mode": (["normal", "dark"],),
"border_thickness": ("INT", {"default": 20, "min": 0, "max": 1024}),
},
"optional": {
"image1": ("IMAGE",),
"image2": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE", "STRING", )
RETURN_NAMES = ("image", "show_help", )
FUNCTION = "layout"
CATEGORY = icons.get("Comfyroll/Graphics/Template")
def layout(self, text1, text2,
footer_height, font_name, font_size, mode,
border_thickness, image1=None, image2=None):
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Layout-Nodes#cr-simple-image-compare"
# Get RGB values for the text and background colors
if mode == "normal":
font_color = "black"
bg_color = "white"
else:
font_color = "white"
bg_color = "black"
if image1 is not None and image2 is not None:
img1 = tensor2pil(image1)
img2 = tensor2pil(image2)
# Get image width and height
image_width, image_height = img1.width, img1.height
if img2.width != img1.width or img2.height != img1.height:
img2 = apply_resize_image(img2, image_width, image_height, 8, "rescale", "false", 1, 256, "lanczos")
# Set defaults
margins = 50
line_spacing = 0
position_x = 0
position_y = 0
align = "center"
rotation_angle = 0
rotation_options = "image center"
font_outline_thickness = 0
font_outline_color = "black"
align = "center"
footer_align = "center"
outline_thickness = border_thickness//2
border_thickness = border_thickness//2
### Create text panel for image 1
if footer_height >0:
text_panel1 = text_panel(image_width, footer_height, text1,
font_name, font_size, font_color,
font_outline_thickness, font_outline_color,
bg_color,
margins, line_spacing,
position_x, position_y,
align, footer_align,
rotation_angle, rotation_options)
combined_img1 = combine_images([img1, text_panel1], 'vertical')
# Apply the outline
if outline_thickness > 0:
combined_img1 = ImageOps.expand(combined_img1, outline_thickness, fill=bg_color)
### Create text panel for image 2
if footer_height >0:
text_panel2 = text_panel(image_width, footer_height, text2,
font_name, font_size, font_color,
font_outline_thickness, font_outline_color,
bg_color,
margins, line_spacing,
position_x, position_y,
align, footer_align,
rotation_angle, rotation_options)
combined_img2 = combine_images([img2, text_panel2], 'vertical')
if outline_thickness > 0:
combined_img2 = ImageOps.expand(combined_img2, outline_thickness, fill=bg_color)
result_img = combine_images([combined_img1, combined_img2], 'horizontal')
else:
result_img = Image.new('RGB', (512,512), bg_color)
# Add a border to the combined image
if border_thickness > 0:
result_img = ImageOps.expand(result_img, border_thickness, bg_color)
return (pil2tensor(result_img), show_help, )
#---------------------------------------------------------------------------------------------------------------------
class CR_ThumbnailPreview:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"image": ("IMAGE",),
"rescale_factor": ("FLOAT", {"default": 0.25, "min": 0.10, "max": 1.00, "step": 0.01}),
"max_columns": ("INT", {"default": 5, "min": 0, "max": 256}),
}
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("show_help", )
OUTPUT_NODE = True
FUNCTION = "thumbnail"
CATEGORY = icons.get("Comfyroll/Graphics/Template")
def thumbnail(self, image, rescale_factor, max_columns):
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Template-Nodes#cr-thumbnail-preview"
result_images = []
outline_thickness = 1
for img in image:
pil_img = tensor2pil(img)
original_width, original_height = pil_img.size
rescaled_img = apply_resize_image(tensor2pil(img), original_width, original_height, 8, "rescale", "false", rescale_factor, 256, "lanczos")
outlined_img = ImageOps.expand(rescaled_img, outline_thickness, fill="black")
result_images.append(outlined_img)
combined_image = make_grid_panel(result_images, max_columns)
images_out = pil2tensor(combined_image)
# based on ETN_SendImageWebSocket
results = []
for tensor in images_out:
array = 255.0 * tensor.cpu().numpy()
image = Image.fromarray(np.clip(array, 0, 255).astype(np.uint8))
server = PromptServer.instance
server.send_sync(
BinaryEventTypes.UNENCODED_PREVIEW_IMAGE,
["PNG", image, None],
server.client_id,
)
results.append({"source": "websocket", "content-type": "image/png", "type": "output"})
return {"ui": {"images": results}, "result": (show_help,) }
#---------------------------------------------------------------------------------------------------------------------
class CR_SeamlessChecker:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"image": ("IMAGE",),
"rescale_factor": ("FLOAT", {"default": 0.25, "min": 0.10, "max": 1.00, "step": 0.01}),
"grid_options": (["2x2", "3x3", "4x4", "5x5", "6x6"],),
}
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("show_help", )
OUTPUT_NODE = True
FUNCTION = "thumbnail"
CATEGORY = icons.get("Comfyroll/Graphics/Template")
def thumbnail(self, image, rescale_factor, grid_options):
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Other-Nodes#cr-seamless-checker"
outline_thickness = 0
pil_img = tensor2pil(image)
original_width, original_height = pil_img.size
rescaled_img = apply_resize_image(tensor2pil(image), original_width, original_height, 8, "rescale", "false", rescale_factor, 256, "lanczos")
outlined_img = ImageOps.expand(rescaled_img, outline_thickness, fill="black")
max_columns = int(grid_options[0])
repeat_images = [outlined_img] * max_columns ** 2
combined_image = make_grid_panel(repeat_images, max_columns)
images_out = pil2tensor(combined_image)
# based on ETN_SendImageWebSocket
results = []
for tensor in images_out:
array = 255.0 * tensor.cpu().numpy()
image = Image.fromarray(np.clip(array, 0, 255).astype(np.uint8))
server = PromptServer.instance
server.send_sync(
BinaryEventTypes.UNENCODED_PREVIEW_IMAGE,
["PNG", image, None],
server.client_id,
)
results.append({"source": "websocket", "content-type": "image/png", "type": "output"})
return {"ui": {"images": results}, "result": (show_help,) }
#---------------------------------------------------------------------------------------------------------------------#
# MAPPINGS
#---------------------------------------------------------------------------------------------------------------------#
# For reference only, actual mappings are in __init__.py
'''
NODE_CLASS_MAPPINGS = {
"CR Simple Meme Template": CR_SimpleMemeTemplate,
"CR Simple Banner": CR_SimpleBanner,
"CR Comic Panel Templates": CR_ComicPanelTemplates,
"CR Simple Image Compare": CR_SimpleImageCompare,
"CR Thumbnail Preview": CR_ThumbnailPreview,
"CR Seamless Checker": CR_SeamlessChecker,
}
'''