|
from PIL import Image |
|
|
|
from ..log import log |
|
from ..utils import comfy_dir, font_path, pil2tensor |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MTB_UnsplashImage: |
|
"""Unsplash Image given a keyword and a size""" |
|
|
|
@classmethod |
|
def INPUT_TYPES(cls): |
|
return { |
|
"required": { |
|
"width": ( |
|
"INT", |
|
{"default": 512, "max": 8096, "min": 0, "step": 1}, |
|
), |
|
"height": ( |
|
"INT", |
|
{"default": 512, "max": 8096, "min": 0, "step": 1}, |
|
), |
|
"random_seed": ( |
|
"INT", |
|
{"default": 0, "max": 1e5, "min": 0, "step": 1}, |
|
), |
|
}, |
|
"optional": { |
|
"keyword": ("STRING", {"default": "nature"}), |
|
}, |
|
} |
|
|
|
RETURN_TYPES = ("IMAGE",) |
|
FUNCTION = "do_unsplash_image" |
|
CATEGORY = "mtb/generate" |
|
|
|
def do_unsplash_image(self, width, height, random_seed, keyword=None): |
|
import io |
|
|
|
import requests |
|
|
|
base_url = "https://source.unsplash.com/random/" |
|
|
|
if width and height: |
|
base_url += f"/{width}x{height}" |
|
|
|
if keyword: |
|
keyword = keyword.replace(" ", "%20") |
|
base_url += f"?{keyword}&{random_seed}" |
|
else: |
|
base_url += f"?&{random_seed}" |
|
try: |
|
log.debug(f"Getting unsplash image from {base_url}") |
|
response = requests.get(base_url) |
|
response.raise_for_status() |
|
|
|
image = Image.open(io.BytesIO(response.content)) |
|
return ( |
|
pil2tensor( |
|
image, |
|
), |
|
) |
|
|
|
except requests.exceptions.RequestException as e: |
|
print("Error retrieving image:", e) |
|
return (None,) |
|
|
|
|
|
def bbox_dim(bbox): |
|
left, upper, right, lower = bbox |
|
width = right - left |
|
height = lower - upper |
|
return width, height |
|
|
|
|
|
|
|
|
|
|
|
class MTB_TextToImage: |
|
"""Utils to convert text to image using a font. |
|
|
|
The tool looks for any .ttf file in the Comfy folder hierarchy. |
|
""" |
|
|
|
fonts = {} |
|
DESCRIPTION = """# Text to Image |
|
|
|
This node look for any font files in comfy_dir/fonts. |
|
by default it fallsback to a default font. |
|
|
|
![img](https://i.imgur.com/3GT92hy.gif) |
|
""" |
|
|
|
def __init__(self): |
|
|
|
|
|
pass |
|
|
|
@classmethod |
|
def CACHE_FONTS(cls): |
|
font_extensions = ["*.ttf", "*.otf", "*.woff", "*.woff2", "*.eot"] |
|
fonts = [font_path] |
|
|
|
for extension in font_extensions: |
|
try: |
|
if comfy_dir.exists(): |
|
fonts.extend(comfy_dir.glob(f"fonts/**/{extension}")) |
|
else: |
|
log.warn(f"Directory {comfy_dir} does not exist.") |
|
except Exception as e: |
|
log.error(f"Error during font caching: {e}") |
|
|
|
for font in fonts: |
|
log.debug(f"Adding font {font}") |
|
MTB_TextToImage.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": ("BOOLEAN", {"default": True}), |
|
"trim": ("BOOLEAN", {"default": True}), |
|
"line_height": ( |
|
"FLOAT", |
|
{"default": 1.0, "min": 0, "step": 0.1}, |
|
), |
|
"font_size": ( |
|
"INT", |
|
{"default": 32, "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}, |
|
), |
|
"color": ( |
|
"COLOR", |
|
{"default": "black"}, |
|
), |
|
"background": ( |
|
"COLOR", |
|
{"default": "white"}, |
|
), |
|
"h_align": (("left", "center", "right"), {"default": "left"}), |
|
"v_align": (("top", "center", "bottom"), {"default": "top"}), |
|
"h_offset": ( |
|
"INT", |
|
{"default": 0, "min": 0, "max": 8096, "step": 1}, |
|
), |
|
"v_offset": ( |
|
"INT", |
|
{"default": 0, "min": 0, "max": 8096, "step": 1}, |
|
), |
|
"h_coverage": ( |
|
"INT", |
|
{"default": 100, "min": 1, "max": 100, "step": 1}, |
|
), |
|
} |
|
} |
|
|
|
RETURN_TYPES = ("IMAGE",) |
|
RETURN_NAMES = ("image",) |
|
FUNCTION = "text_to_image" |
|
CATEGORY = "mtb/generate" |
|
|
|
def text_to_image( |
|
self, |
|
text: str, |
|
font, |
|
wrap, |
|
trim, |
|
line_height, |
|
font_size, |
|
width, |
|
height, |
|
color, |
|
background, |
|
h_align="left", |
|
v_align="top", |
|
h_offset=0, |
|
v_offset=0, |
|
h_coverage=100, |
|
): |
|
import textwrap |
|
|
|
from PIL import Image, ImageDraw, ImageFont |
|
|
|
font_path = self.fonts[font] |
|
|
|
text = ( |
|
text.encode("ascii", "ignore").decode().strip() if trim else text |
|
) |
|
|
|
if wrap: |
|
wrap_width = (((width / 100) * h_coverage) / font_size) * 2 |
|
lines = textwrap.wrap(text, width=wrap_width) |
|
else: |
|
lines = [text] |
|
font = ImageFont.truetype(font_path, size=font_size) |
|
log.debug(f"Lines: {lines}") |
|
img = Image.new("RGBA", (width, height), background) |
|
draw = ImageDraw.Draw(img) |
|
|
|
line_height_px = line_height * font_size |
|
|
|
|
|
if v_align == "top": |
|
y_text = v_offset |
|
elif v_align == "center": |
|
y_text = ((height - (line_height_px * len(lines))) // 2) + v_offset |
|
else: |
|
y_text = (height - (line_height_px * len(lines))) - v_offset |
|
|
|
def get_width(line): |
|
if hasattr(font, "getsize"): |
|
return font.getsize(line)[0] |
|
else: |
|
return font.getlength(line) |
|
|
|
|
|
for line in lines: |
|
line_width = get_width(line) |
|
|
|
if h_align == "left": |
|
x_text = h_offset |
|
elif h_align == "center": |
|
x_text = ((width - line_width) // 2) + h_offset |
|
else: |
|
x_text = (width - line_width) - h_offset |
|
|
|
draw.text((x_text, y_text), line, fill=color, font=font) |
|
y_text += line_height_px |
|
|
|
return (pil2tensor(img),) |
|
|
|
|
|
__nodes__ = [ |
|
MTB_UnsplashImage, |
|
MTB_TextToImage, |
|
|
|
] |
|
|