Add files via upload

This commit is contained in:
Raykosan
2025-04-08 03:42:14 +03:00
committed by GitHub
parent b1db2cfcf2
commit 756455bc86
10 changed files with 289 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
from .nodes.russian_text import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+1
View File
@@ -0,0 +1 @@
{"id":"b6018927-99c5-43b8-9240-b70aa7b69a24","revision":0,"last_node_id":79,"last_link_id":288,"nodes":[{"id":1,"type":"LoadImage","pos":[-430,430],"size":[370,330],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[286]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":[287]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.27","Node name for S&R":"LoadImage","rejects_ue_links":true},"widgets_values":["BackPhone.jpg","image",""],"bgcolor":"hsla(0, 0%, 20.8%, 1)"},{"id":2,"type":"SaveImage","pos":[230,430],"size":[330,330],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":288}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.27","Node name for S&R":"SaveImage"},"widgets_values":["ComfyUI/img",""]},{"id":79,"type":"RS_RusTextOverlay","pos":[-30,430],"size":[230,330],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":286},{"localized_name":"mask","name":"mask","type":"MASK","link":287}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[288]}],"properties":{"Node name for S&R":"RS_RusTextOverlay"},"widgets_values":["","XTypewriter-Bold.otf","#00ff00",100,8,10,"center","center",[false,true],[false,true]]}],"links":[[286,1,0,79,0,"IMAGE"],[287,1,1,79,1,"MASK"],[288,79,0,2,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.0152559799477074,"offset":[737.3011338934954,-176.41735327185543]},"ue_links":[]},"version":0.4}
Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

BIN
View File
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
{
"name": "🦊 RS_RusTextOverlay",
"author": "RaykoStudio",
"version": "1.0.0",
"comfyui": "0.3.27",
"description": "A node for ComfyUI that allows you to overlay text on an image in an area defined by a mask, with support for text rotation, custom fonts, line breaks and transparency.",
"node_classes": ["RS_RusTextOverlay"],
"tags": ["image", "mask", "text", "overlay", "adjustment"],
"requirements": ["torch>=2.5.0"]
}
+11
View File
@@ -0,0 +1,11 @@
from .russian_text import RS_RusTextOverlay
NODE_CLASS_MAPPINGS = {
"RS_RusTextOverlay": RS_RusTextOverlay
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RS_RusTextOverlay": "🦊 RS Русский Текст"
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+244
View File
@@ -0,0 +1,244 @@
# nodes/russian_text.py
import os
import torch
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import cv2
class RS_RusTextOverlay:
def __init__(self):
print("\033[93m🦊\033[0m \033[93mRaykoStudio - Russian Text Overlay Node \033[92mLOADED\033[0m")
@classmethod
def INPUT_TYPES(cls):
current_dir = os.path.dirname(os.path.abspath(__file__))
fonts_dir = os.path.join(os.path.dirname(current_dir), "fonts")
font_list = []
if os.path.exists(fonts_dir):
for f in os.listdir(fonts_dir):
if f.lower().endswith(('.ttf', '.otf')):
font_list.append(f)
if not font_list:
font_list = ["default"]
print("RS_RusTextOverlay: No fonts found, using default")
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"text": ("STRING", {
"default": "Текст\nс переносом\tи табуляцией",
"multiline": True
}),
"font_name": (font_list, {"default": font_list[0]}),
"text_color": ("COLOR", {"default": "#FFFFFF"}),
"text_opacity": ("INT", {"default": 100, "min": 0, "max": 100}),
"min_font_size": ("INT", {"default": 8, "min": 1, "max": 500}),
"padding": ("INT", {"default": 5, "min": 0, "max": 100}),
"vertical_align": (["top", "center", "bottom"], {"default": "center"}),
"horizontal_align": (["left", "center", "right"], {"default": "center"}),
"rotate_with_mask": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_text"
CATEGORY = "🦊 RaykoStudio/Image"
def apply_text(self, image, mask, text, font_name, text_color, text_opacity, min_font_size, padding, vertical_align, horizontal_align, rotate_with_mask):
try:
# Конвертация tensor -> PIL
image_pil = Image.fromarray((image[0].cpu().numpy() * 255).astype(np.uint8), 'RGB')
mask_pil = Image.fromarray((mask[0].cpu().numpy() * 255).astype(np.uint8), 'L')
# Получаем область маски
bbox = mask_pil.getbbox()
if not bbox:
return (image,)
x1, y1, x2, y2 = bbox
mask_width = x2 - x1 - 2*padding
mask_height = y2 - y1 - 2*padding
# Загрузка шрифта
if font_name != "default":
font_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"fonts",
font_name
)
font = self.find_optimal_font(font_path, text, mask_width, mask_height, min_font_size)
else:
font = ImageFont.load_default()
# Преобразуем прозрачность (0-100%) в альфа-канал (0-255)
alpha = int((text_opacity / 100.0) * 255)
if text_color.startswith('#'):
text_color = text_color[1:]
r = int(text_color[0:2], 16)
g = int(text_color[2:4], 16)
b = int(text_color[4:6], 16)
text_color_rgba = (r, g, b, alpha)
# Создаем текстовый слой
text_layer = Image.new("RGBA", image_pil.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(text_layer)
# Разбиваем текст на строки
lines = text.split('\n')
line_heights = []
line_widths = []
for line in lines:
line = line.replace('\t', ' ')
text_bbox = draw.textbbox((0, 0), line, font=font)
line_widths.append(text_bbox[2] - text_bbox[0])
line_heights.append(text_bbox[3] - text_bbox[1])
total_text_height = sum(line_heights)
max_text_width = max(line_widths)
if rotate_with_mask:
mask_np = np.array(mask_pil)
contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
contour = max(contours, key=cv2.contourArea)
points = contour.reshape(-1, 2).astype(np.float32)
mean = np.mean(points, axis=0)
cov = np.cov(points.T)
eigenvalues, eigenvectors = np.linalg.eigh(cov)
main_axis = eigenvectors[:, np.argmax(eigenvalues)]
angle = np.arctan2(main_axis[1], main_axis[0]) * 180 / np.pi
leftmost = points[np.argmin(points[:, 0])]
rightmost = points[np.argmax(points[:, 0])]
direction_vector = rightmost - leftmost
direction_angle = np.arctan2(direction_vector[1], direction_vector[0]) * 180 / np.pi
angle_diff = (direction_angle - angle) % 360
if angle_diff > 90 and angle_diff < 270:
angle += 180
angle = angle % 360
if 90 < angle <= 270:
angle += 180
angle = angle % 360
angle = -angle
temp_size = int(max(max_text_width, total_text_height) * 3)
temp_text = Image.new("RGBA", (temp_size, temp_size), (0, 0, 0, 0))
temp_draw = ImageDraw.Draw(temp_text)
center_x, center_y = temp_size // 2, temp_size // 2
current_y = center_y - total_text_height // 2
for line, line_height, line_width in zip(lines, line_heights, line_widths):
line = line.replace('\t', ' ')
pos_x = center_x - line_width // 2
temp_draw.text(
(pos_x, current_y),
line,
font=font,
fill=text_color_rgba,
anchor="lt"
)
current_y += line_height
rotated_text = temp_text.rotate(
angle,
center=(center_x, center_y),
resample=Image.BICUBIC,
expand=False
)
rot_bbox = rotated_text.getbbox()
if rot_bbox:
rot_width = rot_bbox[2] - rot_bbox[0]
rot_height = rot_bbox[3] - rot_bbox[1]
mask_center_x = (x1 + x2) / 2
mask_center_y = (y1 + y2) / 2
paste_x = int(mask_center_x - rot_width / 2)
paste_y = int(mask_center_y - rot_height / 2)
text_layer.paste(
rotated_text.crop(rot_bbox),
(paste_x, paste_y),
rotated_text.crop(rot_bbox)
)
else:
print("Ошибка: не удалось определить bounding box повернутого текста")
else:
if vertical_align == "top":
current_y = y1 + padding
elif vertical_align == "bottom":
current_y = y2 - total_text_height - padding
else:
current_y = y1 + (mask_height - total_text_height) // 2 + padding
for line, line_height, line_width in zip(lines, line_heights, line_widths):
line = line.replace('\t', ' ')
pos_x = {
"left": x1 + padding,
"right": x2 - line_width - padding,
"center": x1 + (mask_width - line_width) // 2 + padding
}[horizontal_align]
draw.text(
(pos_x, current_y),
line,
font=font,
fill=text_color_rgba,
anchor="lt"
)
current_y += line_height
result = Image.alpha_composite(
image_pil.convert("RGBA"),
text_layer
).convert("RGB")
return (torch.from_numpy(np.array(result).astype(np.float32) / 255.0).unsqueeze(0),)
except Exception as e:
print(f"Ошибка в RS_RusTextOverlay: {str(e)}")
return (image,)
def find_optimal_font(self, font_path, text, max_width, max_height, min_size):
temp_img = Image.new("RGB", (max_width, max_height))
draw = ImageDraw.Draw(temp_img)
lines = text.split('\n')
for size in range(500, min_size - 1, -1):
try:
font = ImageFont.truetype(font_path, size)
fits = True
total_height = 0
max_line_width = 0
for line in lines:
line = line.replace('\t', ' ')
bbox = draw.textbbox((0, 0), line, font=font)
line_width = bbox[2] - bbox[0]
line_height = bbox[3] - bbox[1]
total_height += line_height
max_line_width = max(max_line_width, line_width)
if line_width > max_width:
fits = False
break
if fits and total_height <= max_height:
return font
except Exception as e:
print(f"RS_RusTextOverlay: Failed to load font {font_path} with size {size}: {str(e)}") # Отладка: ошибка загрузки шрифта
continue
return ImageFont.load_default()
NODE_CLASS_MAPPINGS = {
"RS_RusTextOverlay": RS_RusTextOverlay
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RS_RusTextOverlay": "🦊 RS RusTextOverlay"
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "comfyui-rs-text-overlay-node",
"version": "1.0.0",
"description": "A node for ComfyUI that allows you to overlay text on an image in an area defined by a mask, with support for text rotation, custom fonts, line breaks and transparency.",
"author": "Raykosan",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Raykosan/ComfyUI_RaykoStudio"
},
"comfyui": {
"nodes": [
"nodes/russian_text.py"
]
}
}
+4
View File
@@ -0,0 +1,4 @@
torch>=2.0.0
Pillow>=8.0.0
numpy>=1.19.0
opencv-python-headless>=4.2.0
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB