|
|
|
|
|
|
|
|
|
|
|
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'): |
|
|
|
|
|
text_color = get_color_values(font_color, font_color_hex, color_mapping) |
|
|
|
|
|
image_3d = image[0, :, :, :] |
|
|
|
|
|
back_image = tensor2pil(image_3d) |
|
text_image = Image.new('RGB', back_image.size, text_color) |
|
text_mask = Image.new('L', back_image.size) |
|
|
|
|
|
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) |
|
|
|
|
|
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" |
|
|
|
|
|
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'): |
|
|
|
|
|
text_color = get_color_values(font_color, font_color_hex, color_mapping) |
|
bg_color = get_color_values(background_color, bg_color_hex, color_mapping) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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" |
|
|
|
|
|
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'): |
|
|
|
|
|
bg_color = get_color_values(background_color, bg_color_hex, color_mapping) |
|
|
|
|
|
image_3d = image[0, :, :, :] |
|
|
|
|
|
text_image = tensor2pil(image_3d) |
|
text_mask = Image.new('L', text_image.size) |
|
background_image = Image.new('RGB', text_mask.size, bg_color) |
|
|
|
|
|
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) |
|
|
|
|
|
text_mask = ImageOps.invert(rotated_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" |
|
|
|
|
|
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): |
|
|
|
|
|
image_text_3d = image_text[0, :, :, :] |
|
image_back_3d = image_background[0, :, :, :] |
|
|
|
|
|
text_image = tensor2pil(image_text_3d) |
|
back_image = tensor2pil(image_back_3d) |
|
text_mask = Image.new('L', back_image.size) |
|
|
|
|
|
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) |
|
|
|
|
|
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" |
|
|
|
|
|
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'): |
|
|
|
|
|
text_color = get_color_values(font_color, font_color_hex, color_mapping) |
|
|
|
total_images = [] |
|
|
|
for img in image: |
|
|
|
|
|
img = tensor2pil(img) |
|
|
|
textlayer = Image.new("RGBA", img.size) |
|
draw = ImageDraw.Draw(textlayer) |
|
|
|
|
|
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) |
|
|
|
|
|
textsize = get_text_size(draw, text, font) |
|
|
|
|
|
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.text(textpos, text, font=font, fill=text_color) |
|
|
|
|
|
if opacity != 1: |
|
textlayer = reduce_opacity(textlayer, opacity) |
|
|
|
|
|
out_image = Image.composite(textlayer, img, textlayer) |
|
|
|
|
|
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" |
|
|
|
|
|
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 |
|
|
|
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,) |
|
|
|
|
|
|
|
|
|
|
|
''' |
|
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, |
|
} |
|
''' |
|
|
|
|