#---------------------------------------------------------------------------------------------------------------------# # 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, } '''