Workflow replacement
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Raykosan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,96 @@
|
||||
## 🦊 ComfyUI_RaykoStudio
|
||||
ComfyUI_RaykoStudio — это набор кастомных нод для ComfyUI, предоставляющий дополнительные возможности для работы с изображениями
|
||||
|
||||

|
||||
|
||||
## 🦊 RS_RusTextOverlay Node
|
||||
|
||||
## 🔥 Описание
|
||||
|
||||
`RS_RusTextOverlay` позволяет накладывать текст на изображение в области, заданной маской. Нода поддерживает поворот текста в соответствии с углом наклона маски, автоматический подбор размера шрифта, кастомные шрифты, перенос строк и настройку прозрачности текста.
|
||||
|
||||
### Возможности
|
||||
|
||||
- Наложение текста на изображение в области, заданной маской.
|
||||
- Автоматический поворот текста в соответствии с углом наклона маски (опционально).
|
||||
- Поддержка кастомных шрифтов (`.ttf` или `.otf`), загружаемых из папки `fonts`.
|
||||
- Автоматический подбор размера шрифта для вписывания текста в область маски.
|
||||
- Настройка цвета текста и прозрачности (от 0 до 100%).
|
||||
- Настройка выравнивания текста (вертикальное: top/center/bottom, горизонтальное: left/center/right).
|
||||
- Настройка отступов (padding) вокруг текста.
|
||||
|
||||
### Входы
|
||||
|
||||
- `image`: Изображение (тензор формата RGB).
|
||||
- `mask`: Маска (тензор формата L), определяющая область для текста.
|
||||
|
||||
### Выходы
|
||||
|
||||
- `IMAGE`: Изображение с наложенным текстом (тензор формата RGB).
|
||||
|
||||
## ⚙️ Параметры
|
||||
|
||||
- `text`: Текст для наложения. Поддерживает перенос строк.
|
||||
- `font_name`: Выберите шрифт из списка доступных в папке `fonts` (или используйте `default`).
|
||||
- `text_color`: Цвет текста (в формате HEX, например `#FFFFFF`).
|
||||
- `text_opacity`: Прозрачность текста (0–100%, где 0 — полностью прозрачный, 100 — полностью непрозрачный).
|
||||
- `min_font_size`: Минимальный размер шрифта (если текст не помещается, используется шрифт по умолчанию).
|
||||
- `padding`: Отступы вокруг текста внутри маски.
|
||||
- `vertical_align`: Вертикальное выравнивание текста (top/center/bottom).
|
||||
- `horizontal_align`: Горизонтальное выравнивание текста (left/center/right).
|
||||
- `rotate_with_mask`: Включить/выключить поворот текста в соответствии с углом наклона маски.
|
||||
|
||||
## 🛠 Установка
|
||||
|
||||
1. Клонируйте репозиторий в папку `ComfyUI/custom_nodes/`:
|
||||
|
||||
git clone [https://github.com/Raykosan/ComfyUI_RaykoStudio.git](https://github.com/Raykosan/ComfyUI_RaykoStudio)
|
||||
|
||||
Перезагрузите ComfyU
|
||||
|
||||
или
|
||||
|
||||
скопируйте папку RaykoStudio_Nodes в папку: ComfyUI/custom_nodes/
|
||||
|
||||
Перезагрузите ComfyU
|
||||
|
||||
3. Убедитесь, что папка `fonts` содержит шрифты (`.ttf` или `.otf`). Вы можете добавить свои шрифты в эту папку. Если папка пуста или отсутствует, будет использоваться шрифт по умолчанию.
|
||||
|
||||
4. Установите необходимые зависимости, указанные в `requirements.txt`: pip install -r requirements.txt
|
||||
Перезапустите ComfyUI, чтобы нода стала доступна.
|
||||
|
||||
## ⛓️💥 Зависимости
|
||||
|
||||
Для работы ноды требуются следующие библиотеки (указаны в requirements.txt):
|
||||
|
||||
torch>=1.7.0
|
||||
numpy>=1.19.0
|
||||
Pillow>=8.0.0
|
||||
opencv-python>=4.5.0
|
||||
|
||||
## 🎛 Использование
|
||||
|
||||
После установки найдите ноду в ComfyUI под названием 🦊 RS_RusTextOverlay (категория: 🦊 RaykoStudio/Image).
|
||||
Подключите входные данные (image и mask) и настройте параметры.
|
||||
Пример рабочего процесса можно найти в папке examples:
|
||||
- example workflow.json: Пример рабочего процесса для ComfyUI.
|
||||
- example.png: Скриншот результата.
|
||||
|
||||
|
||||
## 🤝 Сообщение об ошибках (Issues)
|
||||
|
||||
Если вы столкнулись с проблемой или нашли баг:
|
||||
|
||||
Проверьте раздел Issues на GitHub, возможно, проблема уже известна.
|
||||
Если проблема новая, создайте новый Issue, описав:
|
||||
- Версию ComfyUI и Python.
|
||||
- Описание проблемы и шаги для ее воспроизведения.
|
||||
- Скриншоты или логи ошибок (если есть).
|
||||
|
||||
## 📜 Лицензия
|
||||
|
||||
MIT License. Используйте ноду на свое усмотрение, но без каких-либо гарантий.
|
||||
|
||||
## ❤️ Благодарности
|
||||
|
||||
Спасибо сообществу ComfyUI за вдохновение и поддержку! Если вам понравилась эта нода, не забудьте поставить звездочку на GitHub!
|
||||
@@ -0,0 +1,3 @@
|
||||
from .nodes.russian_text import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"]
|
||||
}
|
||||
@@ -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']
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
torch>=2.0.0
|
||||
Pillow>=8.0.0
|
||||
numpy>=1.19.0
|
||||
opencv-python-headless>=4.2.0
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 449 KiB |
Reference in New Issue
Block a user