diff --git a/Rayko_Save_Image_Pair.py b/Rayko_Save_Image_Pair.py index c1e3f2a..850888f 100644 --- a/Rayko_Save_Image_Pair.py +++ b/Rayko_Save_Image_Pair.py @@ -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]) diff --git a/fonts/Alegreya-Italic.ttf b/fonts/Alegreya-Italic.ttf new file mode 100644 index 0000000..890f8f1 Binary files /dev/null and b/fonts/Alegreya-Italic.ttf differ diff --git a/fonts/AlumniSansCollegiateOne-Regular.ttf b/fonts/AlumniSansCollegiateOne-Regular.ttf new file mode 100644 index 0000000..047451f Binary files /dev/null and b/fonts/AlumniSansCollegiateOne-Regular.ttf differ diff --git a/fonts/Arina.ttf b/fonts/Arina.ttf new file mode 100644 index 0000000..efa57c5 Binary files /dev/null and b/fonts/Arina.ttf differ diff --git a/fonts/Arsenica-Alt-Bold-trial.ttf b/fonts/Arsenica-Alt-Bold-trial.ttf new file mode 100644 index 0000000..b5c01c1 Binary files /dev/null and b/fonts/Arsenica-Alt-Bold-trial.ttf differ diff --git a/fonts/Calvino-Semibold-Italic-trial.ttf b/fonts/Calvino-Semibold-Italic-trial.ttf new file mode 100644 index 0000000..6e46af8 Binary files /dev/null and b/fonts/Calvino-Semibold-Italic-trial.ttf differ diff --git a/fonts/Calvino-Semibold-trial.ttf b/fonts/Calvino-Semibold-trial.ttf new file mode 100644 index 0000000..5bffaa6 Binary files /dev/null and b/fonts/Calvino-Semibold-trial.ttf differ diff --git a/fonts/Caveat-VariableFont_wght.ttf b/fonts/Caveat-VariableFont_wght.ttf new file mode 100644 index 0000000..0ae4d99 Binary files /dev/null and b/fonts/Caveat-VariableFont_wght.ttf differ diff --git a/fonts/NotoSansArabic-Regular.ttf b/fonts/NotoSansArabic-Regular.ttf new file mode 100644 index 0000000..79359c4 Binary files /dev/null and b/fonts/NotoSansArabic-Regular.ttf differ diff --git a/fonts/Oswald-Bold.ttf b/fonts/Oswald-Bold.ttf new file mode 100644 index 0000000..b9c6e37 Binary files /dev/null and b/fonts/Oswald-Bold.ttf differ diff --git a/fonts/PixelifySans-Bold.ttf b/fonts/PixelifySans-Bold.ttf new file mode 100644 index 0000000..4d8aeb6 Binary files /dev/null and b/fonts/PixelifySans-Bold.ttf differ diff --git a/fonts/Quicksand-Bold.ttf b/fonts/Quicksand-Bold.ttf new file mode 100644 index 0000000..07d5127 Binary files /dev/null and b/fonts/Quicksand-Bold.ttf differ diff --git a/fonts/Roboto-Regular.ttf b/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..3033308 Binary files /dev/null and b/fonts/Roboto-Regular.ttf differ diff --git a/fonts/YoungSerif-Regular.ttf b/fonts/YoungSerif-Regular.ttf new file mode 100644 index 0000000..f454fbe Binary files /dev/null and b/fonts/YoungSerif-Regular.ttf differ diff --git a/fonts/allegrettoscriptone.ttf b/fonts/allegrettoscriptone.ttf new file mode 100644 index 0000000..cf9b452 Binary files /dev/null and b/fonts/allegrettoscriptone.ttf differ diff --git a/fonts/comic.ttf b/fonts/comic.ttf new file mode 100644 index 0000000..2d8e9ca Binary files /dev/null and b/fonts/comic.ttf differ diff --git a/fonts/impact.ttf b/fonts/impact.ttf new file mode 100644 index 0000000..2675688 Binary files /dev/null and b/fonts/impact.ttf differ