#---------------------------------------------------------------------------------------------------------------------# # 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 import platform from PIL import Image, ImageDraw, ImageOps, ImageFont from ..categories import icons from ..config import color_mapping, COLORS from .functions_graphics import * ''' try: from bidi.algorithm import get_display except ImportError: import subprocess subprocess.check_call(['python', '-m', 'pip', 'install', 'python_bidi']) try: import arabic_reshaper except ImportError: import subprocess subprocess.check_call(['python', '-m', 'pip', 'install', 'arabic_reshaper']) ''' def get_offset_for_true_mm(text, draw, font): anchor_bbox = draw.textbbox((0, 0), text, font=font, anchor='lt') anchor_center = (anchor_bbox[0] + anchor_bbox[2]) // 2, (anchor_bbox[1] + anchor_bbox[3]) // 2 mask_bbox = font.getmask(text).getbbox() mask_center = (mask_bbox[0] + mask_bbox[2]) // 2, (mask_bbox[1] + mask_bbox[3]) // 2 return anchor_center[0] - mask_center[0], anchor_center[1] - mask_center[1] class AnyType(str): """A special type that can be connected to any other types. Credit to pythongosssss""" def __ne__(self, __value: object) -> bool: return False any_type = AnyType("*") #---------------------------------------------------------------------------------------------------------------------# ALIGN_OPTIONS = ["center", "top", "bottom"] ROTATE_OPTIONS = ["text center", "image center"] JUSTIFY_OPTIONS = ["center", "left", "right"] PERSPECTIVE_OPTIONS = ["top", "bottom", "left", "right"] #---------------------------------------------------------------------------------------------------------------------# class CR_OverlayText: @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",), "text": ("STRING", {"multiline": True, "default": "text"}), "font_name": (file_list,), "font_size": ("INT", {"default": 50, "min": 1, "max": 1024}), "font_color": (COLORS,), "align": (ALIGN_OPTIONS,), "justify": (JUSTIFY_OPTIONS,), "margins": ("INT", {"default": 0, "min": -1024, "max": 1024}), "line_spacing": ("INT", {"default": 0, "min": -1024, "max": 1024}), "position_x": ("INT", {"default": 0, "min": -4096, "max": 4096}), "position_y": ("INT", {"default": 0, "min": -4096, "max": 4096}), "rotation_angle": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 0.1}), "rotation_options": (ROTATE_OPTIONS,), }, "optional": {"font_color_hex": ("STRING", {"multiline": False, "default": "#000000"}) } } RETURN_TYPES = ("IMAGE", "STRING",) RETURN_NAMES = ("IMAGE", "show_help",) FUNCTION = "overlay_text" CATEGORY = icons.get("Comfyroll/Graphics/Text") def overlay_text(self, image, text, font_name, font_size, font_color, margins, line_spacing, position_x, position_y, align, justify, rotation_angle, rotation_options, font_color_hex='#000000'): # Get RGB values for the text color text_color = get_color_values(font_color, font_color_hex, color_mapping) # Convert tensor images image_3d = image[0, :, :, :] # Create PIL images for the text and background layers and text mask back_image = tensor2pil(image_3d) text_image = Image.new('RGB', back_image.size, text_color) text_mask = Image.new('L', back_image.size) # Draw the text on the text mask rotated_text_mask = draw_masked_text(text_mask, text, font_name, font_size, margins, line_spacing, position_x, position_y, align, justify, rotation_angle, rotation_options) # Composite the text image onto the background image using the rotated text mask image_out = Image.composite(text_image, back_image, rotated_text_mask) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Text-Nodes#cr-overlay-text" # Convert the PIL image back to a torch tensor return (pil2tensor(image_out), show_help,) #---------------------------------------------------------------------------------------------------------------------# class CR_DrawText: @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_width": ("INT", {"default": 512, "min": 64, "max": 2048}), "image_height": ("INT", {"default": 512, "min": 64, "max": 2048}), "text": ("STRING", {"multiline": True, "default": "text"}), "font_name": (file_list,), "font_size": ("INT", {"default": 50, "min": 1, "max": 1024}), "font_color": (COLORS,), "background_color": (COLORS,), "align": (ALIGN_OPTIONS,), "justify": (JUSTIFY_OPTIONS,), "margins": ("INT", {"default": 0, "min": -1024, "max": 1024}), "line_spacing": ("INT", {"default": 0, "min": -1024, "max": 1024}), "position_x": ("INT", {"default": 0, "min": -4096, "max": 4096}), "position_y": ("INT", {"default": 0, "min": -4096, "max": 4096}), "rotation_angle": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 0.1}), "rotation_options": (ROTATE_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 = "draw_text" CATEGORY = icons.get("Comfyroll/Graphics/Text") def draw_text(self, image_width, image_height, text, font_name, font_size, font_color, background_color, margins, line_spacing, position_x, position_y, align, justify, rotation_angle, rotation_options, font_color_hex='#000000', bg_color_hex='#000000'): # Get RGB values for the text and background colors text_color = get_color_values(font_color, font_color_hex, color_mapping) bg_color = get_color_values(background_color, bg_color_hex, color_mapping) # Create PIL images for the text and background layers and text mask size = (image_width, image_height) text_image = Image.new('RGB', size, text_color) back_image = Image.new('RGB', size, bg_color) text_mask = Image.new('L', back_image.size) # Draw the text on the text mask rotated_text_mask = draw_masked_text(text_mask, text, font_name, font_size, margins, line_spacing, position_x, position_y, align, justify, rotation_angle, rotation_options) # Composite the text image onto the background image using the rotated text mask image_out = Image.composite(text_image, back_image, rotated_text_mask) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Text-Nodes#cr-draw-text" # Convert the PIL image back to a torch tensor return (pil2tensor(image_out), show_help,) #---------------------------------------------------------------------------------------------------------------------# class CR_MaskText: @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",), "text": ("STRING", {"multiline": True, "default": "text"}), "font_name": (file_list,), "font_size": ("INT", {"default": 50, "min": 1, "max": 1024}), "background_color": (COLORS,), "align": (ALIGN_OPTIONS,), "justify": (JUSTIFY_OPTIONS,), "margins": ("INT", {"default": 0, "min": -1024, "max": 1024}), "line_spacing": ("INT", {"default": 0, "min": -1024, "max": 1024}), "position_x": ("INT", {"default": 0, "min": -4096, "max": 4096}), "position_y": ("INT", {"default": 0, "min": -4096, "max": 4096}), "rotation_angle": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 0.1}), "rotation_options": (ROTATE_OPTIONS,), }, "optional": { "bg_color_hex": ("STRING", {"multiline": False, "default": "#000000"}) } } RETURN_TYPES = ("IMAGE", "STRING",) RETURN_NAMES = ("IMAGE", "show_help",) FUNCTION = "mask_text" CATEGORY = icons.get("Comfyroll/Graphics/Text") def mask_text(self, image, text, font_name, font_size, margins, line_spacing, position_x, position_y, background_color, align, justify, rotation_angle, rotation_options, bg_color_hex='#000000'): # Get RGB values for the background color bg_color = get_color_values(background_color, bg_color_hex, color_mapping) # Convert tensor images image_3d = image[0, :, :, :] # Create PIL images for the text and background layers and text mask text_image = tensor2pil(image_3d) text_mask = Image.new('L', text_image.size) background_image = Image.new('RGB', text_mask.size, bg_color) # Draw the text on the text mask rotated_text_mask = draw_masked_text(text_mask, text, font_name, font_size, margins, line_spacing, position_x, position_y, align, justify, rotation_angle, rotation_options) # Invert the text mask (so the text is white and the background is black) text_mask = ImageOps.invert(rotated_text_mask) # Composite the text image onto the background image using the inverted text mask image_out = Image.composite(background_image, text_image, text_mask) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Text-Nodes#cr-mask-text" # Convert the PIL image back to a torch tensor return (pil2tensor(image_out), show_help,) #---------------------------------------------------------------------------------------------------------------------# class CR_CompositeText: @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_text": ("IMAGE",), "image_background": ("IMAGE",), "text": ("STRING", {"multiline": True, "default": "text"}), "font_name": (file_list,), "font_size": ("INT", {"default": 50, "min": 1, "max": 1024}), "align": (ALIGN_OPTIONS,), "justify": (JUSTIFY_OPTIONS,), "margins": ("INT", {"default": 0, "min": -1024, "max": 1024}), "line_spacing": ("INT", {"default": 0, "min": -1024, "max": 1024}), "position_x": ("INT", {"default": 0, "min": -4096, "max": 4096}), "position_y": ("INT", {"default": 0, "min": -4096, "max": 4096}), "rotation_angle": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 0.1}), "rotation_options": (ROTATE_OPTIONS,), } } RETURN_TYPES = ("IMAGE", "STRING",) RETURN_NAMES = ("IMAGE", "show_help",) FUNCTION = "composite_text" CATEGORY = icons.get("Comfyroll/Graphics/Text") def composite_text(self, image_text, image_background, text, font_name, font_size, margins, line_spacing, position_x, position_y, align, justify, rotation_angle, rotation_options): # Convert tensor images image_text_3d = image_text[0, :, :, :] image_back_3d = image_background[0, :, :, :] # Create PIL images for the text and background layers and text mask text_image = tensor2pil(image_text_3d) back_image = tensor2pil(image_back_3d) text_mask = Image.new('L', back_image.size) # Draw the text on the text mask rotated_text_mask = draw_masked_text(text_mask, text, font_name, font_size, margins, line_spacing, position_x, position_y, align, justify, rotation_angle, rotation_options) # Composite the text image onto the background image using the rotated text mask image_out = Image.composite(text_image, back_image, rotated_text_mask) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Text-Nodes#cr-composite-text" # Convert the PIL image back to a torch tensor return (pil2tensor(image_out), show_help,) #---------------------------------------------------------------------------------------------------------------------# class CR_ArabicTextRTL: @classmethod def INPUT_TYPES(s): return {"required": { "arabic_text": ("STRING", {"multiline": True, "default": "شمس"}), } } RETURN_TYPES = ("STRING", "STRING", ) RETURN_NAMES = ("arabic_text_rtl", "show help", ) FUNCTION = "adjust_arabic_to_rtl" CATEGORY = icons.get("Comfyroll/Graphics/Text") def adjust_arabic_to_rtl(self, arabic_text): """ Adjust Arabic text to read from right to left (RTL). Args: arabic_text (str): The Arabic text to be adjusted. Returns: str: The adjusted Arabic text in RTL format. """ arabic_text_reshaped = arabic_reshaper.reshape(arabic_text) rtl_text = get_display(arabic_text_reshaped) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Text-Nodes#cr-arabic-text-rtl" return (rtl_text, show_help,) #---------------------------------------------------------------------------------------------------------------------# class CR_SimpleTextWatermark: @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")] ALIGN_OPTIONS = ["center", "top left", "top center", "top right", "bottom left", "bottom center", "bottom right"] return {"required": { "image": ("IMAGE",), "text": ("STRING", {"multiline": False, "default": "@ your name"}), "align": (ALIGN_OPTIONS,), "opacity": ("FLOAT", {"default": 0.30, "min": 0.00, "max": 1.00, "step": 0.01}), "font_name": (file_list,), "font_size": ("INT", {"default": 50, "min": 1, "max": 1024}), "font_color": (COLORS,), "x_margin": ("INT", {"default": 20, "min": -1024, "max": 1024}), "y_margin": ("INT", {"default": 20, "min": -1024, "max": 1024}), }, "optional": { "font_color_hex": ("STRING", {"multiline": False, "default": "#000000"}), } } RETURN_TYPES = ("IMAGE", "STRING", ) RETURN_NAMES = ("IMAGE", "show_help", ) FUNCTION = "overlay_text" CATEGORY = icons.get("Comfyroll/Graphics/Text") def overlay_text(self, image, text, align, font_name, font_size, font_color, opacity, x_margin, y_margin, font_color_hex='#000000'): # Get RGB values for the text color text_color = get_color_values(font_color, font_color_hex, color_mapping) total_images = [] for img in image: # Create PIL images for the background layer img = tensor2pil(img) textlayer = Image.new("RGBA", img.size) draw = ImageDraw.Draw(textlayer) # Load the font font_file = os.path.join("fonts", str(font_name)) resolved_font_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), font_file) font = ImageFont.truetype(str(resolved_font_path), size=font_size) # Get the size of the text textsize = get_text_size(draw, text, font) # Calculate the position to place the text based on the alignment if align == 'center': textpos = [(img.size[0] - textsize[0]) // 2, (img.size[1] - textsize[1]) // 2] elif align == 'top left': textpos = [x_margin, y_margin] elif align == 'top center': textpos = [(img.size[0] - textsize[0]) // 2, y_margin] elif align == 'top right': textpos = [img.size[0] - textsize[0] - x_margin, y_margin] elif align == 'bottom left': textpos = [x_margin, img.size[1] - textsize[1] - y_margin] elif align == 'bottom center': textpos = [(img.size[0] - textsize[0]) // 2, img.size[1] - textsize[1] - y_margin] elif align == 'bottom right': textpos = [img.size[0] - textsize[0] - x_margin, img.size[1] - textsize[1] - y_margin] # Draw the text on the text layer draw.text(textpos, text, font=font, fill=text_color) # Adjust the opacity of the text layer if needed if opacity != 1: textlayer = reduce_opacity(textlayer, opacity) # Composite the text layer on top of the original image out_image = Image.composite(textlayer, img, textlayer) # convert to tensor out_image = np.array(out_image.convert("RGB")).astype(np.float32) / 255.0 out_image = torch.from_numpy(out_image).unsqueeze(0) total_images.append(out_image) images_out = torch.cat(total_images, 0) show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Text-Nodes#cr-simple-text-watermark" # Convert the PIL image back to a torch tensor return (images_out, show_help, ) #---------------------------------------------------------------------------------------------------------------------# class CR_SelectFont: def __init__(self): pass @classmethod def INPUT_TYPES(cls): if platform.system() == "Windows": system_root = os.environ.get("SystemRoot") font_dir = os.path.join(system_root, "Fonts") if system_root else None # Default debian-based Linux & MacOS font dirs elif platform.system() == "Linux": font_dir = "/usr/share/fonts/truetype" elif platform.system() == "Darwin": font_dir = "/System/Library/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": { "font_name": (file_list,), } } RETURN_TYPES = (any_type, "STRING",) RETURN_NAMES = ("font_name", "show_help",) FUNCTION = "select_font" CATEGORY = icons.get("Comfyroll/Graphics/Text") def select_font(self, font_name): show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/Text-Nodes#cr-select-font" return (font_name, show_help,) #---------------------------------------------------------------------------------------------------------------------# # MAPPINGS #---------------------------------------------------------------------------------------------------------------------# # For reference only, actual mappings are in __init__.py ''' NODE_CLASS_MAPPINGS = { "CR Overlay Text": CR_OverlayText, "CR Draw Text": CR_DrawText, "CR Mask Text": CR_MaskText, "CR Composite Text": CR_CompositeText, "CR Draw Perspective Text": CR_DrawPerspectiveText, "CR Arabic Text RTL": CR_ArabicTextRTL, "CR Simple Text Watermark": CR_SimpleTextWatermark, "CR Select Font": CR_SelectFont, } '''