#---------------------------------------------------------------------------------------------------------------------# # Comfyroll 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 ..categories import icons from ..config import color_mapping, COLORS from .graphics_functions import (hex_to_rgb, get_color_values, text_panel, combine_images, apply_outline_and_border, get_font_size, draw_text_on_image) #try: # import Markdown #except ImportError: # import pip # pip.main(['install', 'Markdown']) #---------------------------------------------------------------------------------------------------------------------# ALIGN_OPTIONS = ["top", "center", "bottom"] ROTATE_OPTIONS = ["text center", "image center"] JUSTIFY_OPTIONS = ["left", "center", "right"] PERSPECTIVE_OPTIONS = ["top", "bottom", "left", "right"] #---------------------------------------------------------------------------------------------------------------------# def tensor2pil(image): return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) def pil2tensor(image): return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) #---------------------------------------------------------------------------------------------------------------------# class CR_PageLayout: @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")] layout_options = ["header", "footer", "header and footer", "no header or footer"] return {"required": { "layout_options": (layout_options,), "image_panel": ("IMAGE",), "header_height": ("INT", {"default": 0, "min": 0, "max": 1024}), "header_text": ("STRING", {"multiline": True, "default": "text"}), "header_align": (JUSTIFY_OPTIONS, ), "footer_height": ("INT", {"default": 0, "min": 0, "max": 1024}), "footer_text": ("STRING", {"multiline": True, "default": "text"}), "footer_align": (JUSTIFY_OPTIONS, ), "font_name": (file_list,), "font_color": (COLORS,), "header_font_size": ("INT", {"default": 150, "min": 0, "max": 1024}), "footer_font_size": ("INT", {"default": 50, "min": 0, "max": 1024}), "border_thickness": ("INT", {"default": 0, "min": 0, "max": 1024}), "border_color": (COLORS,), "background_color": (COLORS,), }, "optional": { "font_color_hex": ("STRING", {"multiline": False, "default": "#000000"}), "border_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/Layout") def layout(self, layout_options, image_panel, border_thickness, border_color, background_color, header_height, header_text, header_align, footer_height, footer_text, footer_align, font_name, font_color, header_font_size, footer_font_size, font_color_hex='#000000', border_color_hex='#000000', bg_color_hex='#000000'): # Get RGB values for the text and background colors font_color = get_color_values(font_color, font_color_hex, color_mapping) border_color = get_color_values(border_color, border_color_hex, color_mapping) bg_color = get_color_values(background_color, bg_color_hex, color_mapping) main_panel = tensor2pil(image_panel) # Get image width and height image_width = main_panel.width image_height = main_panel.height # 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" images = [] ### Create text panels and add to images array if layout_options == "header" or layout_options == "header and footer": header_panel = text_panel(image_width, header_height, header_text, font_name, header_font_size, font_color, font_outline_thickness, font_outline_color, bg_color, margins, line_spacing, position_x, position_y, align, header_align, rotation_angle, rotation_options) images.append(header_panel) images.append(main_panel) if layout_options == "footer" or layout_options == "header and footer": footer_panel = text_panel(image_width, footer_height, footer_text, font_name, footer_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) images.append(footer_panel) combined_image = combine_images(images, 'vertical') # Add a border to the combined image if border_thickness > 0: combined_image = ImageOps.expand(combined_image, border_thickness, border_color) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Layout-Nodes#cr-page-layout" return (pil2tensor(combined_image), show_help, ) #---------------------------------------------------------------------------------------------------------------------# class CR_SimpleTitles: @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")] layout_options = ["header", "footer", "header and footer", "no header or footer"] return {"required": { "image": ("IMAGE",), "header_text": ("STRING", {"multiline": True, "default": "text"}), "header_height": ("INT", {"default": 0, "min": 0, "max": 1024}), "header_font_size": ("INT", {"default": 150, "min": 0, "max": 1024}), "header_align": (JUSTIFY_OPTIONS, ), "footer_text": ("STRING", {"multiline": True, "default": "text"}), "footer_height": ("INT", {"default": 0, "min": 0, "max": 1024}), "footer_font_size": ("INT", {"default": 50, "min": 0, "max": 1024}), "footer_align": (JUSTIFY_OPTIONS, ), "font_name": (file_list,), "font_color": (COLORS,), "background_color": (COLORS,), }, "optional": { "font_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/Layout") def layout(self, image, header_height, header_text, header_align, header_font_size, footer_height, footer_text, footer_align, footer_font_size, font_name, font_color, background_color, font_color_hex='#000000', bg_color_hex='#000000',): # Get RGB values for the text and background colors font_color = get_color_values(font_color, font_color_hex, color_mapping) bg_color = get_color_values(background_color, bg_color_hex, color_mapping) main_panel = tensor2pil(image) # Get image width and height image_width = main_panel.width image_height = main_panel.height # 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" images = [] ### Create text panels and add to images array if header_height >0: header_panel = text_panel(image_width, header_height, header_text, font_name, header_font_size, font_color, font_outline_thickness, font_outline_color, bg_color, margins, line_spacing, position_x, position_y, align, header_align, rotation_angle, rotation_options) images.append(header_panel) images.append(main_panel) if footer_height >0: footer_panel = text_panel(image_width, footer_height, footer_text, font_name, footer_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) images.append(footer_panel) combined_image = combine_images(images, 'vertical') show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Layout-Nodes#cr-simple_titles" return (pil2tensor(combined_image), show_help, ) #---------------------------------------------------------------------------------------------------------------------# class CR_ImagePanel: @classmethod def INPUT_TYPES(s): directions = ["horizontal", "vertical"] return {"required": { "image_1": ("IMAGE",), "border_thickness": ("INT", {"default": 0, "min": 0, "max": 1024}), "border_color": (COLORS,), "outline_thickness": ("INT", {"default": 0, "min": 0, "max": 1024}), "outline_color": (COLORS[1:],), "layout_direction": (directions,), }, "optional": { "image_2": ("IMAGE",), "image_3": ("IMAGE",), "image_4": ("IMAGE",), "border_color_hex": ("STRING", {"multiline": False, "default": "#000000"}) } } RETURN_TYPES = ("IMAGE", "STRING", ) RETURN_NAMES = ("image", "show_help", ) FUNCTION = "make_panel" CATEGORY = icons.get("Comfyroll/Graphics/Layout") def make_panel(self, image_1, border_thickness, border_color, outline_thickness, outline_color, layout_direction, image_2=None, image_3=None, image_4=None, border_color_hex='#000000'): border_color = get_color_values(border_color, border_color_hex, color_mapping) # Convert PIL images to NumPy arrays images = [] #image_1 = image_1[0, :, :, :] images.append(tensor2pil(image_1)) if image_2 is not None: #image_2 = image_2[0, :, :, :] images.append(tensor2pil(image_2)) if image_3 is not None: #image_3 = image_3[0, :, :, :] images.append(tensor2pil(image_3)) if image_4 is not None: #image_4 = image_4[0, :, :, :] images.append(tensor2pil(image_4)) # Apply borders and outlines to each image images = apply_outline_and_border(images, outline_thickness, outline_color, border_thickness, border_color) combined_image = combine_images(images, layout_direction) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Layout-Nodes#cr-image-panel" return (pil2tensor(combined_image), show_help, ) #---------------------------------------------------------------------------------------------------------------------# class CR_ImageGridPanel: @classmethod def INPUT_TYPES(s): directions = ["horizontal", "vertical"] return {"required": { "images": ("IMAGE",), "border_thickness": ("INT", {"default": 0, "min": 0, "max": 1024}), "border_color": (COLORS,), "outline_thickness": ("INT", {"default": 0, "min": 0, "max": 1024}), "outline_color": (COLORS[1:],), "max_columns": ("INT", {"default": 5, "min": 0, "max": 256}), }, "optional": { "border_color_hex": ("STRING", {"multiline": False, "default": "#000000"}) } } RETURN_TYPES = ("IMAGE", "STRING", ) RETURN_NAMES = ("image", "show_help", ) FUNCTION = "make_panel" CATEGORY = icons.get("Comfyroll/Graphics/Layout") def make_panel(self, images, border_thickness, border_color, outline_thickness, outline_color, max_columns, border_color_hex='#000000'): border_color = get_color_values(border_color, border_color_hex, color_mapping) # Convert PIL images to NumPy arrays images = [tensor2pil(image) for image in images] # Apply borders and outlines to each image images = apply_outline_and_border(images, outline_thickness, outline_color, border_thickness, border_color) # Calculate dimensions for the grid num_images = len(images) num_rows = (num_images - 1) // max_columns + 1 combined_width = max(image.width for image in images) * min(max_columns, num_images) combined_height = max(image.height for image in images) * num_rows combined_image = Image.new('RGB', (combined_width, combined_height)) x_offset, y_offset = 0, 0 # Initialize offsets for image in images: combined_image.paste(image, (x_offset, y_offset)) x_offset += image.width if x_offset >= max_columns * image.width: x_offset = 0 y_offset += image.height show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Layout-Nodes#cr-image-grid-panel" return (pil2tensor(combined_image), show_help, ) #---------------------------------------------------------------------------------------------------------------------# class CR_ImageBorder: @classmethod def INPUT_TYPES(s): return {"required": { "image": ("IMAGE",), "top_thickness": ("INT", {"default": 0, "min": 0, "max": 4096}), "bottom_thickness": ("INT", {"default": 0, "min": 0, "max": 4096}), "left_thickness": ("INT", {"default": 0, "min": 0, "max": 4096}), "right_thickness": ("INT", {"default": 0, "min": 0, "max": 4096}), "border_color": (COLORS,), "outline_thickness": ("INT", {"default": 0, "min": 0, "max": 1024}), "outline_color": (COLORS[1:],), }, "optional": { "border_color_hex": ("STRING", {"multiline": False, "default": "#000000"}) } } RETURN_TYPES = ("IMAGE", "STRING", ) RETURN_NAMES = ("image", "show_help", ) FUNCTION = "make_panel" CATEGORY = icons.get("Comfyroll/Graphics/Layout") def make_panel(self, image, top_thickness, bottom_thickness, left_thickness, right_thickness, border_color, outline_thickness, outline_color, border_color_hex='#000000'): images = [] border_color = get_color_values(border_color, border_color_hex, color_mapping) for img in image: img = tensor2pil(img) # Apply the outline if outline_thickness > 0: img = ImageOps.expand(img, outline_thickness, fill=outline_color) # Apply the borders if left_thickness > 0 or right_thickness > 0 or top_thickness > 0 or bottom_thickness > 0: img = ImageOps.expand(img, (left_thickness, top_thickness, right_thickness, bottom_thickness), fill=border_color) images.append(pil2tensor(img)) images = torch.cat(images, dim=0) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Layout-Nodes#cr-image-border" return (images, show_help, ) #---------------------------------------------------------------------------------------------------------------------# class CR_ColorPanel: @classmethod def INPUT_TYPES(s): return {"required": { "panel_width": ("INT", {"default": 512, "min": 8, "max": 4096}), "panel_height": ("INT", {"default": 512, "min": 8, "max": 4096}), "fill_color": (COLORS,), }, "optional": { "fill_color_hex": ("STRING", {"multiline": False, "default": "#000000"}) } } RETURN_TYPES = ("IMAGE", "STRING", ) RETURN_NAMES = ("image", "show_help", ) FUNCTION = "make_panel" CATEGORY = icons.get("Comfyroll/Graphics/Layout") def make_panel(self, panel_width, panel_height, fill_color, fill_color_hex='#000000'): fill_color = get_color_values(fill_color, fill_color_hex, color_mapping) size = (panel_width, panel_height) panel = Image.new('RGB', size, fill_color) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Layout-Nodes#cr-color-panel" return (pil2tensor(panel), show_help, ) #---------------------------------------------------------------------------------------------------------------------# class CR_SimpleTextPanel: @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": { "panel_width": ("INT", {"default": 512, "min": 8, "max": 4096}), "panel_height": ("INT", {"default": 512, "min": 8, "max": 4096}), "text": ("STRING", {"multiline": True, "default": "text"}), "font_name": (file_list,), "font_color": (COLORS,), "font_size": ("INT", {"default": 100, "min": 0, "max": 1024}), "font_outline_thickness": ("INT", {"default": 0, "min": 0, "max": 50}), "font_outline_color": (COLORS,), "background_color": (COLORS,), "align": (ALIGN_OPTIONS, ), "justify": (JUSTIFY_OPTIONS, ), }, "optional": { "font_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/Layout") def layout(self, panel_width, panel_height, text, align, justify, font_name, font_color, font_size, font_outline_thickness, font_outline_color, background_color, font_color_hex='#000000', font_outline_color_hex='#000000', bg_color_hex='#000000'): # Get RGB values for the text and background colors font_color = get_color_values(font_color, font_color_hex, color_mapping) outline_color = get_color_values(font_outline_color, font_outline_color_hex, color_mapping) bg_color = get_color_values(background_color, bg_color_hex, color_mapping) # Set defaults margins = 50 line_spacing = 0 position_x = 0 position_y = 0 rotation_angle = 0 rotation_options = "image center" ### Create text panels panel = text_panel(panel_width, panel_height, text, font_name, font_size, font_color, font_outline_thickness, outline_color, bg_color, margins, line_spacing, position_x, position_y, align, justify, rotation_angle, rotation_options) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Layout-Nodes#cr-simple-text-panel" return (pil2tensor(panel), show_help, ) #---------------------------------------------------------------------------------------------------------------------# class CR_OverlayTransparentImage: @classmethod def INPUT_TYPES(s): return {"required": { "back_image": ("IMAGE",), "overlay_image": ("IMAGE",), "transparency": ("FLOAT", {"default": 0, "min": 0, "max": 1, "step": 0.1}), "offset_x": ("INT", {"default": 0, "min": -4096, "max": 4096}), "offset_y": ("INT", {"default": 0, "min": -4096, "max": 4096}), "rotation_angle": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 0.1}), "overlay_scale_factor": ("FLOAT", {"default": 1.0, "min": -0.1, "max": 100.0, "step": 0.1}), } } RETURN_TYPES = ("IMAGE", ) FUNCTION = "overlay_image" CATEGORY = icons.get("Comfyroll/Graphics/Layout") def overlay_image(self, back_image, overlay_image, transparency, offset_x, offset_y, rotation_angle, overlay_scale_factor=1.0): """ Overlay an image onto another image with transparency, rotation, and scaling. Args: back_image (torch.Tensor): Background image tensor. overlay_image (torch.Tensor): Overlay image tensor. transparency (float): Transparency level for the overlay image (0.0 to 1.0). offset_x (int): X-coordinate relative to the center of the back image. offset_y (int): Y-coordinate relative to the center of the back image. rotation_angle (float): Rotation angle in degrees. scale_factor (float): Scaling factor for the overlay image. Returns: torch.Tensor: Resulting image tensor. """ # Convert tensor images #back_image = back_image[0, :, :, :] #overlay_image = overlay_image[0, :, :, :] # Create PIL images for the text and background layers and text mask back_image = tensor2pil(back_image) overlay_image = tensor2pil(overlay_image) # Apply transparency to overlay image overlay_image.putalpha(int(255 * (1 - transparency))) # Rotate overlay image overlay_image = overlay_image.rotate(rotation_angle, expand=True) # Scale overlay image overlay_width, overlay_height = overlay_image.size new_size = (int(overlay_width * overlay_scale_factor), int(overlay_height * overlay_scale_factor)) overlay_image = overlay_image.resize(new_size, Image.ANTIALIAS) # Calculate centered position relative to the center of the background image center_x = back_image.width // 2 center_y = back_image.height // 2 position_x = center_x - overlay_image.width // 2 + offset_x position_y = center_y - overlay_image.height // 2 + offset_y # Paste the rotated overlay image onto the new back image at the specified position back_image.paste(overlay_image, (position_x, position_y), overlay_image) # Convert the PIL image back to a torch tensor return pil2tensor(back_image), #---------------------------------------------------------------------------------------------------------------------# # MAPPINGS #---------------------------------------------------------------------------------------------------------------------# # For reference only, actual mappings are in __init__.py ''' NODE_CLASS_MAPPINGS = { "CR Page Layout": CR_PageLayout, "CR Image Grid Panel": CR_ImageGridPanel, "CR Image XY Panel": CR_ImageXYPanel, "CR Image Border": CR_ImageBorder, "CR Color Panel": CR_ColorPanel, "CR Simple Text Panel": CR_SimpleTextPanel, "CR Overlay Transparent Image": CR_OverlayTransparentImage, "CR Simple Titles": CR_SimpleTitles, } '''