Added functionality for the RS Save Image Pair node
This commit is contained in:
+139
-43
@@ -1,12 +1,74 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image, PngImagePlugin
|
||||
from PIL import Image, PngImagePlugin, ImageDraw, ImageFont, ImageOps
|
||||
import json
|
||||
import os
|
||||
import folder_paths
|
||||
import textwrap
|
||||
|
||||
print("\033[93m🦊\033[0m \033[93mRaykoStudio - RS Save Image Pair \033[92mLOADED\033[0m")
|
||||
|
||||
def get_font_list(font_dir):
|
||||
if not os.path.isdir(font_dir):
|
||||
return []
|
||||
return [f for f in os.listdir(font_dir) if f.lower().endswith('.ttf')]
|
||||
|
||||
def create_text_panel(width, height, text, font_path, font_size, text_color, bg_color):
|
||||
panel = Image.new('RGB', (width, height), color=bg_color)
|
||||
draw = ImageDraw.Draw(panel)
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
except Exception:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
avg_char_width = font_size * 0.6
|
||||
max_chars = int(width / avg_char_width)
|
||||
wrapped_lines = []
|
||||
for line in text.split('\n'):
|
||||
wrapped_lines.extend(textwrap.wrap(line, width=max_chars) if max_chars > 0 else [line])
|
||||
|
||||
if not wrapped_lines:
|
||||
return panel
|
||||
|
||||
line_height = font_size + 4
|
||||
total_text_height = len(wrapped_lines) * line_height
|
||||
|
||||
y = (height - total_text_height) // 2
|
||||
|
||||
for line in wrapped_lines:
|
||||
try:
|
||||
bbox = draw.textbbox((0, 0), line, font=font)
|
||||
line_width = bbox[2] - bbox[0]
|
||||
except AttributeError:
|
||||
line_width = draw.textsize(line, font=font)[0]
|
||||
x = (width - line_width) // 2
|
||||
draw.text((x, y), line, font=font, fill=text_color)
|
||||
y += line_height
|
||||
|
||||
return panel
|
||||
|
||||
def combine_images_pil(images, direction='horizontal'):
|
||||
if direction == 'horizontal':
|
||||
widths, heights = zip(*(img.size for img in images))
|
||||
total_width = sum(widths)
|
||||
max_height = max(heights)
|
||||
combined = Image.new('RGB', (total_width, max_height))
|
||||
x_offset = 0
|
||||
for img in images:
|
||||
combined.paste(img, (x_offset, (max_height - img.height) // 2))
|
||||
x_offset += img.width
|
||||
else:
|
||||
widths, heights = zip(*(img.size for img in images))
|
||||
max_width = max(widths)
|
||||
total_height = sum(heights)
|
||||
combined = Image.new('RGB', (max_width, total_height))
|
||||
y_offset = 0
|
||||
for img in images:
|
||||
combined.paste(img, ((max_width - img.width) // 2, y_offset))
|
||||
y_offset += img.height
|
||||
return combined
|
||||
|
||||
class SaveImagePair:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
@@ -14,15 +76,33 @@ class SaveImagePair:
|
||||
self.prefix_append = ""
|
||||
self.compress_level = 4
|
||||
|
||||
self.script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
self.font_dir = os.path.join(self.script_dir, "fonts")
|
||||
self.font_list = get_font_list(self.font_dir)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
font_dir = os.path.join(script_dir, "fonts")
|
||||
font_list = get_font_list(font_dir)
|
||||
if not font_list:
|
||||
font_list = ["none"]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"reference": ("IMAGE",), # первое изображение (референс)
|
||||
"final": ("IMAGE",), # второе изображение (финальное)
|
||||
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
"reference": ("IMAGE",),
|
||||
"final": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "ThumbnailPair/img"}),
|
||||
"concat_type": (["horizontal", "vertical"], {"default": "horizontal"}),
|
||||
"fill_color": (["black", "white"], {"default": "black"}),
|
||||
},
|
||||
"optional": {
|
||||
"text1": ("STRING", {"multiline": True, "default": ""}),
|
||||
"text2": ("STRING", {"multiline": True, "default": ""}),
|
||||
"footer_height": ("INT", {"default": 100, "min": 0, "max": 1024}),
|
||||
"font_name": (font_list, {"default": font_list[0] if font_list else "none"}),
|
||||
"font_size": ("INT", {"default": 50, "min": 1, "max": 512}),
|
||||
"theme": (["dark", "light"], {"default": "light"}),
|
||||
"scale_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
@@ -35,10 +115,19 @@ class SaveImagePair:
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🦊 RaykoStudio/Image"
|
||||
|
||||
def save_images_pair(self, reference, final, filename_prefix="ComfyUI", concat_type="horizontal",
|
||||
fill_color="black", prompt=None, extra_pnginfo=None):
|
||||
def save_images_pair(self, reference, final, filename_prefix="ThumbnailPair/img", concat_type="horizontal",
|
||||
text1="", text2="", footer_height=100, font_name=None, font_size=50,
|
||||
theme="light", scale_factor=1.0, prompt=None, extra_pnginfo=None):
|
||||
filename_prefix += self.prefix_append
|
||||
|
||||
# Определяем цвета в зависимости от темы
|
||||
if theme == "dark":
|
||||
text_color_rgb = (255, 255, 255) # белый текст
|
||||
bg_color_rgb = (0, 0, 0) # чёрный фон
|
||||
else: # light
|
||||
text_color_rgb = (0, 0, 0) # чёрный текст
|
||||
bg_color_rgb = (255, 255, 255) # белый фон
|
||||
|
||||
# Проверка батчей
|
||||
batch_ref = reference.shape[0]
|
||||
batch_fin = final.shape[0]
|
||||
@@ -50,54 +139,61 @@ class SaveImagePair:
|
||||
else:
|
||||
min_batch = batch_ref
|
||||
|
||||
# Определяем цвет фона
|
||||
if fill_color == "black":
|
||||
bg_color = (0, 0, 0)
|
||||
else: # white
|
||||
bg_color = (255, 255, 255)
|
||||
font_path = None
|
||||
if font_name and font_name != "none":
|
||||
font_path = os.path.join(self.font_dir, font_name)
|
||||
if not os.path.isfile(font_path):
|
||||
font_path = None
|
||||
|
||||
combined_images = []
|
||||
for i in range(min_batch):
|
||||
# Конвертация тензоров в PIL Image
|
||||
img_ref = Image.fromarray((reference[i].cpu().numpy() * 255).astype(np.uint8))
|
||||
img_fin = Image.fromarray((final[i].cpu().numpy() * 255).astype(np.uint8))
|
||||
|
||||
# Автоматическое выравнивание с добавлением полей (padding)
|
||||
if concat_type == "horizontal":
|
||||
# Выравнивание по высоте
|
||||
target_h = max(img_ref.height, img_fin.height)
|
||||
if img_ref.height != target_h:
|
||||
new_ref = Image.new("RGB", (img_ref.width, target_h), bg_color)
|
||||
new_ref.paste(img_ref, (0, (target_h - img_ref.height) // 2))
|
||||
img_ref = new_ref
|
||||
if img_fin.height != target_h:
|
||||
new_fin = Image.new("RGB", (img_fin.width, target_h), bg_color)
|
||||
new_fin.paste(img_fin, (0, (target_h - img_fin.height) // 2))
|
||||
img_fin = new_fin
|
||||
else: # vertical
|
||||
# Выравнивание по ширине
|
||||
target_w = max(img_ref.width, img_fin.width)
|
||||
if img_ref.width != target_w:
|
||||
new_ref = Image.new("RGB", (target_w, img_ref.height), bg_color)
|
||||
new_ref.paste(img_ref, ((target_w - img_ref.width) // 2, 0))
|
||||
img_ref = new_ref
|
||||
if img_fin.width != target_w:
|
||||
new_fin = Image.new("RGB", (target_w, img_fin.height), bg_color)
|
||||
new_fin.paste(img_fin, ((target_w - img_fin.width) // 2, 0))
|
||||
img_fin = new_fin
|
||||
target_w, target_h = img_ref.size
|
||||
if img_fin.size != (target_w, target_h):
|
||||
img_fin = img_fin.resize((target_w, target_h), Image.Resampling.LANCZOS)
|
||||
|
||||
# Склейка
|
||||
if concat_type == "horizontal":
|
||||
new_img = np.concatenate((np.array(img_ref), np.array(img_fin)), axis=1)
|
||||
panel1 = None
|
||||
if footer_height > 0 and text1.strip():
|
||||
panel1 = create_text_panel(target_w, footer_height, text1, font_path, font_size,
|
||||
text_color_rgb, bg_color_rgb)
|
||||
panel2 = None
|
||||
if footer_height > 0 and text2.strip():
|
||||
panel2 = create_text_panel(target_w, footer_height, text2, font_path, font_size,
|
||||
text_color_rgb, bg_color_rgb)
|
||||
|
||||
blocks = []
|
||||
if panel1 is not None:
|
||||
ref_block = combine_images_pil([img_ref, panel1], 'vertical')
|
||||
else:
|
||||
new_img = np.concatenate((np.array(img_ref), np.array(img_fin)), axis=0)
|
||||
ref_block = img_ref
|
||||
if panel2 is not None:
|
||||
fin_block = combine_images_pil([img_fin, panel2], 'vertical')
|
||||
else:
|
||||
fin_block = img_fin
|
||||
|
||||
combined_images.append(new_img)
|
||||
if concat_type == "horizontal":
|
||||
combined_img = combine_images_pil([ref_block, fin_block], 'horizontal')
|
||||
else:
|
||||
combined_img = combine_images_pil([ref_block, fin_block], 'vertical')
|
||||
|
||||
# Масштабирование по самой длинной стороне
|
||||
if scale_factor < 1.0:
|
||||
old_width, old_height = combined_img.size
|
||||
max_side = max(old_width, old_height)
|
||||
new_max_side = int(max_side * scale_factor)
|
||||
if new_max_side == 0:
|
||||
new_max_side = 1
|
||||
ratio = new_max_side / max_side
|
||||
new_width = int(old_width * ratio)
|
||||
new_height = int(old_height * ratio)
|
||||
combined_img = combined_img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
combined_images.append(np.array(combined_img))
|
||||
|
||||
# Преобразуем обратно в тензор
|
||||
combined_tensor = torch.from_numpy(np.stack(combined_images, axis=0)).float() / 255.0
|
||||
|
||||
# Сохранение (как в стандартном SaveImage)
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
||||
filename_prefix, self.output_dir, combined_tensor[0].shape[1], combined_tensor[0].shape[0])
|
||||
|
||||
|
||||
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.
Reference in New Issue
Block a user