Files
2023-12-21 14:35:17 +08:00

494 lines
17 KiB
Python

from pathlib import Path
from typing import cast
from typing import List, Optional, Union
import numpy as np
import torch
from PIL import Image, ImageDraw, ImageFont
import math
#----------------------------------------------------------------------------
here = Path(__file__).parent.absolute()
comfy_dir = here.parent.parent
#----------------------------------------------------------------------------
def pil2tensor(image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor:
if isinstance(image, list):
return torch.cat([pil2tensor(img) for img in image], dim=0)
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
#----------------------------------------------------------------------------
# 添加一个辅助函数,用于交换宽度和高度
def swap_width_height(width, height):
return height, width
#----------------------------------------------------------------------------
import logging
import re
import os
base_log_level = logging.DEBUG if os.environ.get("MTB_DEBUG") else logging.INFO
# Custom object that discards the output
class NullWriter:
def write(self, text):
pass
class Formatter(logging.Formatter):
grey = "\x1b[38;20m"
cyan = "\x1b[36;20m"
purple = "\x1b[35;20m"
yellow = "\x1b[33;20m"
red = "\x1b[31;20m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
# format = "%(asctime)s - [%(name)s] - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
format = "[%(name)s] | %(levelname)s -> %(message)s"
FORMATS = {
logging.DEBUG: purple + format + reset,
logging.INFO: cyan + format + reset,
logging.WARNING: yellow + format + reset,
logging.ERROR: red + format + reset,
logging.CRITICAL: bold_red + format + reset,
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
def mklog(name, level=base_log_level):
logger = logging.getLogger(name)
logger.setLevel(level)
for handler in logger.handlers:
logger.removeHandler(handler)
ch = logging.StreamHandler()
ch.setLevel(level)
ch.setFormatter(Formatter())
logger.addHandler(ch)
# Disable log propagation
logger.propagate = False
return logger
# - The main app logger
log = mklog(__package__, base_log_level)
def log_user(arg):
print("\033[34mComfy MTB Utils:\033[0m {arg}")
def get_summary(docstring):
return docstring.strip().split("\n\n", 1)[0]
def blue_text(text):
return f"\033[94m{text}\033[0m"
def cyan_text(text):
return f"\033[96m{text}\033[0m"
def get_label(label):
words = re.findall(r"(?:^|[A-Z])[a-z]*", label)
return " ".join(words).strip()
# 禁用 aiohttp 的访问日志记录器
logging.getLogger('aiohttp.access').disabled = True
#----------------------------------------------------------------------------
def bbox_dim(bbox):
left, upper, right, lower = bbox
width = right - left
height = lower - upper
return width, height
#----------------------------------------------------------------------------
class Text_Image_Zho:
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": "ZHOZHOZHO"},
),
"selected_font": ((sorted(cls.fonts.keys())),),
"align": (["left", "center", "right"],
),
"wrap": (
"INT",
{"default": 0, "min": 0, "max": 8096, "step": 1},
),
"font_size": (
"INT",
{"default": 12, "min": 1, "max": 2500, "step": 1},
),
"color": (
"COLOR",
{"default": "red"},
),
"outline_size": (
"INT",
{"default": 0, "min": 0, "max": 8096, "step": 1},
),
"outline_color": (
"COLOR",
{"default": "blue"}, # 设置默认的描边颜色
),
"margin_x": (
"INT",
{"default": 0, "min": 0, "max": 8096, "step": 1},
),
"margin_y": (
"INT",
{"default": 0, "min": 0, "max": 8096, "step": 1},
),
"width": (
"INT",
{"default": 512, "min": 1, "max": 8096, "step": 1},
),
"height": (
"INT",
{"default": 512, "min": 1, "max": 8096, "step": 1},
),
"swap": ("BOOLEAN", {"default": False}), # 添加交换宽度和高度的按钮
"arc_text": ("BOOLEAN", {"default": False}),
"arc_radius": (
"INT",
{"default": 100, "min": 1, "max": 2500, "step": 1},
),
"arc_start_angle": (
"INT",
{"default": 180, "min": 0, "max": 360, "step": 1},
),
"arc_end_angle": (
"INT",
{"default": 360, "min": 0, "max": 360, "step": 1},
),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "text_to_image"
CATEGORY = "Zho模块组/text"
def draw_text_in_arc(self, image, draw, text, font, font_path, font_size, center, radius, start_angle, end_angle, fill='black', stroke_fill='blue', stroke_width=0):
# 文字的总长度
text_width = sum(font.getsize(char)[0] for char in text[:-1])
# 根据扇形角度计算角度步长
angle_range = end_angle - start_angle
angle_step = (angle_range / (len(text) - 1) if len(text) > 1 else 1)
# 开始绘制文字
current_angle = start_angle
for char in text:
char_width, char_height = font.getsize(char)
angle = math.radians(current_angle)
print(current_angle, angle, angle_step)
# 创建单独的图像用于旋转字符
super_sampling_multiplier = 10
char_image = Image.new("RGBA", (char_width * super_sampling_multiplier, char_height * super_sampling_multiplier), (0, 0, 0, 0))
char_draw = ImageDraw.Draw(char_image)
super_sampling_font = cast(ImageFont.FreeTypeFont, ImageFont.truetype(font_path, font_size * super_sampling_multiplier))
char_draw.text((0, 0), char, font=super_sampling_font, fill=fill, stroke_fill=stroke_fill, stroke_width=stroke_width)
# 计算旋转角度和字符位置
rotate_angle = current_angle - 90 - (current_angle - 180) * 2 # 使字符面向圆心
rotated_char_image = char_image.rotate(rotate_angle, expand=1, resample=Image.BICUBIC)
# 缩小图像大小
new_size = (int(rotated_char_image.width / 10), int(rotated_char_image.height / 10))
rotated_char_image_resized = rotated_char_image.resize(new_size, Image.ANTIALIAS)
# 计算缩小后的图像放置位置
x = center[0] + radius * math.cos(angle) - rotated_char_image_resized.size[0] / 2
y = center[1] + radius * math.sin(angle) - rotated_char_image_resized.size[1] / 2
# 粘贴缩小后的图像
image.paste(rotated_char_image_resized, (int(x), int(y)), rotated_char_image_resized)
# 更新下一个字符的开始角度
current_angle += angle_step
def text_to_image(
self, text, selected_font, align, wrap, font_size, width, height, color, outline_size, outline_color, margin_x, margin_y, swap=False, arc_text=False, arc_radius=100, arc_start_angle=180, arc_end_angle=360
):
import textwrap
# 如果用户选择交换宽度和高度,则调用交换函数
if swap:
width, height = swap_width_height(width, height)
font_path = self.fonts[selected_font]
(_, top, _, _) = ImageFont.truetype(font_path, font_size).getbbox(text)
font = cast(ImageFont.FreeTypeFont, ImageFont.truetype(font_path, font_size))
if wrap == 0:
wrap = width / font_size
wrap = int(wrap)
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), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# 曲线文字
if arc_text:
width, height = bbox_dim(font.getbbox(text))
center_x = (img_width) // 2
center_y = arc_radius + (height)
if align == "left":
center_x = arc_radius + (height) // 2
elif align == "right":
center_x = img_width - arc_radius - (height) // 2
center = (center_x + margin_x, center_y + margin_y)
self.draw_text_in_arc(img, draw, text, font, font_path, font_size, center, arc_radius, arc_start_angle, arc_end_angle,
fill=color, stroke_fill=outline_color, stroke_width=outline_size)
else:
# 初始化 y_text
y_text = margin_y + outline_size - top
for line in lines:
width, height = bbox_dim(font.getbbox(line))
# 根据 align 参数计算文本的 x 坐标
if align == "left":
x_text = margin_x
elif align == "center":
x_text = (img_width - width) // 2
elif align == "right":
x_text = img_width - width - margin_x
else:
x_text = margin_x # 默认为左对齐
draw.text(
(x_text, y_text),
text=line,
fill=color,
stroke_fill=outline_color,
stroke_width=outline_size,
font=font,
)
y_text += height
return (pil2tensor(img),)
#----------------------------------------------------------------------------
class Text_Image_Multiline_Zho:
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": "ZHOZHOZHO", "multiline": True},
),
"selected_font": ((sorted(cls.fonts.keys())),),
"align": (["left", "center", "right"],
),
"wrap": (
"INT",
{"default": 120, "min": 0, "max": 8096, "step": 1},
),
"graphspace": (
"INT",
{"default": 10, "min": 0, "max": 8096, "step": 1},
),
"linespace": (
"INT",
{"default": 2, "min": 0, "max": 8096, "step": 1},
),
"font_size": (
"INT",
{"default": 12, "min": 1, "max": 2500, "step": 1},
),
"color": (
"COLOR",
{"default": "red"},
),
"outline_size": (
"INT",
{"default": 0, "min": 0, "max": 8096, "step": 1},
),
"outline_color": (
"COLOR",
{"default": "blue"}, # 设置默认的描边颜色
),
"margin_x": (
"INT",
{"default": 0, "min": 0, "max": 8096, "step": 1},
),
"margin_y": (
"INT",
{"default": 0, "min": 0, "max": 8096, "step": 1},
),
"width": (
"INT",
{"default": 512, "min": 1, "max": 8096, "step": 1},
),
"height": (
"INT",
{"default": 512, "min": 1, "max": 8096, "step": 1},
),
"swap": ("BOOLEAN", {"default": False}), # 添加交换宽度和高度的按钮
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "text_to_image_multiline"
CATEGORY = "Zho模块组/text"
def text_to_image_multiline(
self, text, selected_font, align, wrap, graphspace, linespace, font_size, width, height, color, outline_size, outline_color, margin_x, margin_y, swap=False
):
from PIL import Image, ImageDraw, ImageFont
import textwrap
# 如果用户选择交换宽度和高度,则调用交换函数
if swap:
width, height = swap_width_height(width, height)
font_path = self.fonts[selected_font]
(_, top, _, _) = ImageFont.truetype(font_path, font_size).getbbox(text)
font = cast(ImageFont.FreeTypeFont, ImageFont.truetype(font_path, font_size))
if wrap == 0:
wrap = width / font_size
wrap = int(wrap)
paragraphs = text.split('\n')
log.debug(f"Paragraphs: {paragraphs}")
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), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# 初始化 y_text
y_text = margin_y + outline_size
for paragraph in paragraphs:
lines = textwrap.wrap(paragraph, width=wrap, expand_tabs=False, replace_whitespace=False)
for line in lines:
width, height = bbox_dim(font.getbbox(line))
# 根据 align 参数重新计算 x 坐标
if align == "left":
x_text = margin_x
elif align == "center":
x_text = (img_width - width) // 2
elif align == "right":
x_text = img_width - width - margin_x
else:
x_text = margin_x # 默认为左对齐
draw.text(
(x_text, y_text),
text=line,
fill=color,
stroke_fill=outline_color,
stroke_width=outline_size,
font=font,
)
# 更新 y 坐标,加上当前行的高度和一些额外的间距
y_text += height + linespace # linespace 是行之间的额外间距
# 段落之间添加一些额外的间距
y_text += graphspace # 可以根据需要调整
return (pil2tensor(img),)
#----------------------------------------------------------------------------
NODE_CLASS_MAPPINGS = {
"Text_Image_Zho": Text_Image_Zho,
"Text_Image_Multiline_Zho": Text_Image_Multiline_Zho,
}