diff --git a/nodes/conditions.py b/nodes/conditions.py index 71eeef0..33d50e9 100644 --- a/nodes/conditions.py +++ b/nodes/conditions.py @@ -1,5 +1,4 @@ -from ..utils import pil2tensor -from ..utils import here, comfy_dir +from ..utils import here from ..log import log import folder_paths from pathlib import Path @@ -97,110 +96,4 @@ class StylesLoader: return (self.options[style_name][0], self.options[style_name][1]) -class TextToImage: - """Utils to convert text to image using a font - - - The tool looks for any .ttf file in the Comfy folder hierarchy. - """ - - fonts = {} - - def __init__(self): - # - This is executed when the graph is executed, we could conditionaly reload fonts there - pass - - @classmethod - def CACHE_FONTS(cls): - font_extensions = ["*.ttf", "*.otf", "*.woff", "*.woff2", "*.eot"] - fonts = [] - - for extension in font_extensions: - fonts.extend(comfy_dir.glob(f"**/{extension}")) - - if not fonts: - log.warn( - "> No fonts found in the comfy folder, place at least one font file somewhere in ComfyUI's hierarchy" - ) - else: - log.debug(f"> Found {len(fonts)} fonts") - - for font in fonts: - log.debug(f"Adding font {font}") - cls.fonts[font.stem] = font.as_posix() - - @classmethod - def INPUT_TYPES(cls): - if not cls.fonts: - cls.CACHE_FONTS() - else: - log.debug(f"Using cached fonts (count: {len(cls.fonts)})") - return { - "required": { - "text": ( - "STRING", - {"default": "Hello world!"}, - ), - "font": ((sorted(cls.fonts.keys())),), - "wrap": ( - "INT", - {"default": 120, "min": 0, "max": 8096, "step": 1}, - ), - "font_size": ( - "INT", - {"default": 12, "min": 1, "max": 2500, "step": 1}, - ), - "width": ( - "INT", - {"default": 512, "min": 1, "max": 8096, "step": 1}, - ), - "height": ( - "INT", - {"default": 512, "min": 1, "max": 8096, "step": 1}, - ), - # "position": (["INT"], {"default": 0, "min": 0, "max": 100, "step": 1}), - "color": ( - "COLOR", - {"default": "black"}, - ), - "background": ( - "COLOR", - {"default": "white"}, - ), - } - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - FUNCTION = "text_to_image" - CATEGORY = "mtb/generate" - - def text_to_image( - self, text, font, wrap, font_size, width, height, color, background - ): - from PIL import Image, ImageDraw, ImageFont - import textwrap - - font = self.fonts[font] - font = ImageFont.truetype(font, font_size) - if wrap == 0: - wrap = width / font_size - lines = textwrap.wrap(text, width=wrap) - log.debug(f"Lines: {lines}") - line_height = font.getsize("hg")[1] - img_height = height # line_height * len(lines) - img_width = width # max(font.getsize(line)[0] for line in lines) - - img = Image.new("RGBA", (img_width, img_height), background) - draw = ImageDraw.Draw(img) - y_text = 0 - for line in lines: - width, height = font.getsize(line) - draw.text((0, y_text), line, color, font=font) - y_text += height - - # img.save(os.path.join(folder_paths.base_path, f'{str(uuid.uuid4())}.png')) - return (pil2tensor(img),) - - -__nodes__ = [SmartStep, TextToImage, StylesLoader] +__nodes__ = [SmartStep, StylesLoader] diff --git a/nodes/generate.py b/nodes/generate.py index 332e11a..8fa55ee 100644 --- a/nodes/generate.py +++ b/nodes/generate.py @@ -1,5 +1,7 @@ import qrcode from ..utils import pil2tensor +from ..utils import comfy_dir +from typing import cast from PIL import Image from ..log import log @@ -130,6 +132,9 @@ class QrCode: CATEGORY = "mtb/generate" def do_qr(self, url, width, height, error_correct, box_size, border, invert): + log.warning( + "This node will soon be deprecated, there are much better alternatives like https://github.com/coreyryanhanson/comfy-qr" + ) if error_correct == "L" or error_correct not in ["M", "Q", "H"]: error_correct = qrcode.constants.ERROR_CORRECT_L elif error_correct == "M": @@ -159,8 +164,123 @@ class QrCode: return (pil2tensor(code),) +def bbox_dim(bbox): + left, upper, right, lower = bbox + width = right - left + height = lower - upper + return width, height + + +class TextToImage: + """Utils to convert text to image using a font + + + The tool looks for any .ttf file in the Comfy folder hierarchy. + """ + + fonts = {} + + def __init__(self): + # - This is executed when the graph is executed, we could conditionaly reload fonts there + pass + + @classmethod + def CACHE_FONTS(cls): + font_extensions = ["*.ttf", "*.otf", "*.woff", "*.woff2", "*.eot"] + fonts = [] + + for extension in font_extensions: + fonts.extend(comfy_dir.glob(f"**/{extension}")) + + if not fonts: + log.warn( + "> No fonts found in the comfy folder, place at least one font file somewhere in ComfyUI's hierarchy" + ) + else: + log.debug(f"> Found {len(fonts)} fonts") + + for font in fonts: + log.debug(f"Adding font {font}") + cls.fonts[font.stem] = font.as_posix() + + @classmethod + def INPUT_TYPES(cls): + if not cls.fonts: + cls.CACHE_FONTS() + else: + log.debug(f"Using cached fonts (count: {len(cls.fonts)})") + return { + "required": { + "text": ( + "STRING", + {"default": "Hello world!"}, + ), + "font": ((sorted(cls.fonts.keys())),), + "wrap": ( + "INT", + {"default": 120, "min": 0, "max": 8096, "step": 1}, + ), + "font_size": ( + "INT", + {"default": 12, "min": 1, "max": 2500, "step": 1}, + ), + "width": ( + "INT", + {"default": 512, "min": 1, "max": 8096, "step": 1}, + ), + "height": ( + "INT", + {"default": 512, "min": 1, "max": 8096, "step": 1}, + ), + # "position": (["INT"], {"default": 0, "min": 0, "max": 100, "step": 1}), + "color": ( + "COLOR", + {"default": "black"}, + ), + "background": ( + "COLOR", + {"default": "white"}, + ), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "text_to_image" + CATEGORY = "mtb/generate" + + def text_to_image( + self, text, font, wrap, font_size, width, height, color, background + ): + from PIL import Image, ImageDraw, ImageFont + import textwrap + + font = self.fonts[font] + font = cast(ImageFont.FreeTypeFont, ImageFont.truetype(font, font_size)) + if wrap == 0: + wrap = width / font_size + lines = textwrap.wrap(text, width=wrap) + log.debug(f"Lines: {lines}") + line_height = bbox_dim(font.getbbox("hg"))[1] + img_height = height # line_height * len(lines) + img_width = width # max(font.getsize(line)[0] for line in lines) + + img = Image.new("RGBA", (img_width, img_height), background) + draw = ImageDraw.Draw(img) + y_text = 0 + # - bbox is [left, upper, right, lower] + for line in lines: + width, height = bbox_dim(font.getbbox(line)) + draw.text((0, y_text), line, color, font=font) + y_text += height + + # img.save(os.path.join(folder_paths.base_path, f'{str(uuid.uuid4())}.png')) + return (pil2tensor(img),) + + __nodes__ = [ QrCode, - UnsplashImage + UnsplashImage, + TextToImage # MtbExamples, ] diff --git a/nodes/graph_utils.py b/nodes/graph_utils.py index 70c711b..a9ccc35 100644 --- a/nodes/graph_utils.py +++ b/nodes/graph_utils.py @@ -7,6 +7,7 @@ import json from comfy.cli_args import args from ..utils import pil2tensor import io +import numpy as np def get_image(filename, subfolder, folder_type): @@ -107,6 +108,37 @@ class GetBatchFromHistory: return (output,) +class AnyToString: + """Tries to take any input and convert it to a string""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {"input": ("*")}, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "do_str" + CATEGORY = "mtb/converters" + + def do_str(self, input): + if isinstance(input, str): + return (input,) + elif isinstance(input, torch.Tensor): + return (f"Tensor of shape {input.shape} and dtype {input.dtype}",) + elif isinstance(input, Image.Image): + return (f"PIL Image of size {input.size} and mode {input.mode}",) + elif isinstance(input, np.ndarray): + return (f"Numpy array of shape {input.shape} and dtype {input.dtype}",) + + elif isinstance(input, dict): + return (f"Dictionary of {len(input)} items, with keys {input.keys()}",) + + else: + log.debug(f"Falling back to string conversion of {input}") + return (str(input),) + + class StringReplace: """Basic string replacement""" @@ -178,4 +210,4 @@ class FitNumber: return (res,) -__nodes__ = [StringReplace, FitNumber, GetBatchFromHistory] +__nodes__ = [StringReplace, FitNumber, GetBatchFromHistory, AnyToString] diff --git a/web/comfy_shared.js b/web/comfy_shared.js index 3f34b54..f570fac 100644 --- a/web/comfy_shared.js +++ b/web/comfy_shared.js @@ -60,7 +60,7 @@ export function offsetDOMWidget( */ export function getWidgetType(config) { // Special handling for COMBO so we restrict links based on the entries - let type = config[0] + let type = config?.[0] let linkType = type if (type instanceof Array) { type = 'COMBO'