diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b397cf0 --- /dev/null +++ b/__init__.py @@ -0,0 +1,179 @@ +import os +from PIL import Image, ImageDraw, ImageFont +import numpy as np +import torch +import logging + +class TextOnImage: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + font_dir = os.path.join(os.path.dirname(__file__), "font") + font_files = [f for f in os.listdir(font_dir) if f.endswith(('.ttc', '.ttf'))] + if not font_files: + font_files = ["default"] + + return { + "required": { + "text": ("STRING", {"default": "", "multiline": True}), + "image": ("IMAGE",), + "x": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 1}), + "y": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 1}), + "font_size": ("INT", {"default": 12, "min": 0, "max": 320, "step": 1}), + "text_color": ("STRING", {"default": "#ffffff"}), + "text_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "stroke_width": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), + "stroke_color": ("STRING", {"default": "#000000"}), + "stroke_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "shadow_x": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}), + "shadow_y": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}), + "shadow_color": ("STRING", {"default": "#000000"}), + "shadow_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "font_file": (font_files, {"default": font_files[0]}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "apply_text" + CATEGORY = "💀Text On Image" + + def hex_to_rgb(self, hex_color): + """将 HEX 色值转换为 RGB 元组""" + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + + def apply_text(self, text, image, x, y, font_size, text_color, text_opacity, + stroke_width, stroke_color, stroke_opacity, + shadow_x, shadow_y, shadow_color, shadow_opacity, + font_file): + if text == "": + logging.info("Text is empty, returning original image.") + return (image,) + + # 将输入的 torch.Tensor 转换为 PIL 图像 + img = image[0].cpu().numpy() + img = (img * 255).astype(np.uint8) + base_img = Image.fromarray(img).convert('RGBA') + img_width, img_height = base_img.size + logging.info(f"Image size: {img_width}x{img_height}") + + # 构建字体文件路径 + font_dir = os.path.join(os.path.dirname(__file__), "font") + font_path = os.path.join(font_dir, font_file) + logging.info(f"Font path: {font_path}") + + # 加载字体 + try: + font = ImageFont.truetype(font_path, font_size) + logging.info(f"Font loaded successfully: {font_path}") + except IOError as e: + logging.warning(f"Failed to load font {font_path}: {e}, using default font.") + font = ImageFont.load_default() + + # 转换颜色 + text_color_rgb = self.hex_to_rgb(text_color) + stroke_color_rgb = self.hex_to_rgb(stroke_color) + shadow_color_rgb = None if shadow_color == "none" else self.hex_to_rgb(shadow_color) + + # 使用 ImageDraw.textbbox 精确计算文字区域 + temp_img = Image.new('RGBA', (1, 1), (0, 0, 0, 0)) + temp_draw = ImageDraw.Draw(temp_img) + text_bbox = temp_draw.textbbox((0, 0), text, font=font) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + + # 增加安全高度 + safety_margin = font_size // 2 + text_height += safety_margin + + logging.info(f"Text dimensions: width={text_width}, height={text_height}") + + # 计算总padding(包括描边和投影) + padding = max(stroke_width * 2, abs(shadow_x), abs(shadow_y)) + + # 调整文字位置 + text_left = max(0, x - padding) + text_top = max(0, y - padding) + text_right = min(img_width, x + text_width + padding) + text_bottom = min(img_height, y + text_height + padding) + + # 如果文字超出图像底部,向上调整 + if text_bottom > img_height: + offset = text_bottom - img_height + text_top = max(0, text_top - offset) + text_bottom = img_height + + logging.info(f"Canvas bounds: left={text_left}, top={text_top}, right={text_right}, bottom={text_bottom}") + + # 创建文字层 + canvas_width = text_right - text_left + canvas_height = text_bottom - text_top + if canvas_width <= 0 or canvas_height <= 0: + logging.warning("Canvas dimensions are invalid, returning original image.") + return (image,) + canvas = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0)) + canvas_draw = ImageDraw.Draw(canvas) + logging.info(f"Canvas created: {canvas_width}x{canvas_height}") + + # 计算文字在画布中的相对位置(y 作为顶部位置) + relative_x = x - text_left + relative_y = y - text_top + logging.info(f"Relative text position: x={relative_x}, y={relative_y}") + + # 绘制投影 + if shadow_color_rgb and (shadow_x != 0 or shadow_y != 0): + try: + shadow_pos = (relative_x + shadow_x, relative_y + shadow_y) + canvas_draw.text(shadow_pos, text, font=font, + fill=shadow_color_rgb + (int(255 * shadow_opacity),)) + logging.info("Shadow drawn successfully.") + except Exception as e: + logging.error(f"Failed to draw shadow: {e}") + + # 绘制描边 + if stroke_width > 0: + try: + for dx in range(-stroke_width, stroke_width + 1): + for dy in range(-stroke_width, stroke_width + 1): + if dx != 0 or dy != 0: + stroke_pos = (relative_x + dx, relative_y + dy) + canvas_draw.text(stroke_pos, text, font=font, + fill=stroke_color_rgb + (int(255 * stroke_opacity),)) + logging.info(f"Stroke drawn with width {stroke_width}.") + except Exception as e: + logging.error(f"Failed to draw stroke: {e}") + + # 绘制主文字 + try: + canvas_draw.text((relative_x, relative_y), text, font=font, + fill=text_color_rgb + (int(255 * text_opacity),)) + logging.info("Main text drawn successfully.") + except Exception as e: + logging.error(f"Failed to draw main text: {e}") + return (image,) + + # 将文字画布粘贴到主图像上 + try: + base_img.paste(canvas, (text_left, text_top), canvas) + logging.info("Canvas pasted to base image successfully.") + except Exception as e: + logging.error(f"Failed to paste canvas to base image: {e}") + return (image,) + + # 将 PIL.Image 转换回 torch.Tensor + img = np.array(base_img).astype(np.float32) / 255.0 + img = torch.from_numpy(img)[None,] + logging.info("Image converted back to tensor.") + + return (img,) + +# 注册节点 +NODE_CLASS_MAPPINGS = { + "TextOnImage": TextOnImage +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "TextOnImage": "💀Text On Image" +} \ No newline at end of file diff --git a/font/BebasNeue-Regular.ttf b/font/BebasNeue-Regular.ttf new file mode 100644 index 0000000..d2190b5 Binary files /dev/null and b/font/BebasNeue-Regular.ttf differ diff --git a/font/en-AllRound.ttf b/font/en-AllRound.ttf new file mode 100644 index 0000000..85c2e6b Binary files /dev/null and b/font/en-AllRound.ttf differ diff --git a/font/en-AllRoundItalic.ttf b/font/en-AllRoundItalic.ttf new file mode 100644 index 0000000..572c4ad Binary files /dev/null and b/font/en-AllRoundItalic.ttf differ diff --git a/font/en-Kairos.ttf b/font/en-Kairos.ttf new file mode 100644 index 0000000..268c7c9 Binary files /dev/null and b/font/en-Kairos.ttf differ diff --git a/font/en-LOL.ttf b/font/en-LOL.ttf new file mode 100644 index 0000000..767fc3c Binary files /dev/null and b/font/en-LOL.ttf differ diff --git a/font/en-LOLItalic.ttf b/font/en-LOLItalic.ttf new file mode 100644 index 0000000..a8b673c Binary files /dev/null and b/font/en-LOLItalic.ttf differ diff --git a/font/en-MatriceBlack.otf b/font/en-MatriceBlack.otf new file mode 100644 index 0000000..438a1b7 Binary files /dev/null and b/font/en-MatriceBlack.otf differ diff --git a/font/en-Montserrat-Black.ttf b/font/en-Montserrat-Black.ttf new file mode 100644 index 0000000..2d31930 Binary files /dev/null and b/font/en-Montserrat-Black.ttf differ diff --git a/font/en-Montserrat-Medium.ttf b/font/en-Montserrat-Medium.ttf new file mode 100644 index 0000000..dfbcfe4 Binary files /dev/null and b/font/en-Montserrat-Medium.ttf differ diff --git a/font/zhs-Lanting.ttf b/font/zhs-Lanting.ttf new file mode 100644 index 0000000..f1e966a Binary files /dev/null and b/font/zhs-Lanting.ttf differ diff --git a/font/zhs-hyMX.ttf b/font/zhs-hyMX.ttf new file mode 100644 index 0000000..5fcb0a4 Binary files /dev/null and b/font/zhs-hyMX.ttf differ