179 lines
7.6 KiB
Python
179 lines
7.6 KiB
Python
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"
|
|
} |