From 8d51618d06fd0cef9b4602356c6e10c648a71818 Mon Sep 17 00:00:00 2001 From: Meeeyo Date: Thu, 5 Jun 2025 00:24:51 +0800 Subject: [PATCH] update --- README.md | 4 +- meyo_node_File.py | 310 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 311 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 94e0fce..0abb579 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Accurately extracts specified content from text using positional indexing and ke - **Integration with Excel:** Achieves deep integration with Excel, supporting read and write operations on paged and tabular data, facilitating batch completion of complex tasks and significantly improving efficiency. -## Update Notes (2025-04-21): +## Update Notes (2025-06-05): 1.Added LoadAndAdjustImage node to load and resize images. 2.Added SaveImagEX node to save images to a specified directory (also added an image output port). @@ -26,6 +26,8 @@ Achieves deep integration with Excel, supporting read and write operations on pa 5.Added the feature of reading web page content. +5.Added text layout to image node, image layer overlay node. + ## Partial Node Display **Text Processing Node** ![image](image/meyo01.png) diff --git a/meyo_node_File.py b/meyo_node_File.py index 3a71681..310bfa7 100644 --- a/meyo_node_File.py +++ b/meyo_node_File.py @@ -1,7 +1,7 @@ import os, re, io, base64, csv, torch, shutil, requests, chardet, pathlib import openpyxl, folder_paths, node_helpers import numpy as np -from PIL import Image, ImageOps, ImageSequence +from PIL import Image, ImageOps, ImageSequence, ImageDraw, ImageFont from pathlib import Path from openpyxl.drawing.image import Image as OpenpyxlImage from PIL import Image as PILImage @@ -557,11 +557,15 @@ class WriteToTxtFile: def write_to_txt(self, text_content, file_path, any=None): try: + dir_path = os.path.dirname(file_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) file_exists = os.path.exists(file_path) mode = 'a' if file_exists else 'w' + with open(file_path, mode, encoding='utf-8') as f: if file_exists: - f.write('\n\n') # 如果文件存在,写入两个换行符 + f.write('\n\n') f.write(text_content) return ("Write successful: " + text_content,) except Exception as e: @@ -663,6 +667,306 @@ class FileListAndSuffix: return ("", 0, []) +#======文字图像 +class TextToImage: + @classmethod + def INPUT_TYPES(cls): + fonts_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts") + if not os.path.exists(fonts_dir): + os.makedirs(fonts_dir) + font_files = [] + else: + font_files = [file for file in os.listdir(fonts_dir) if file.lower().endswith(('.ttf', '.otf'))] + font_files = font_files or ["arial.ttf"] + return { + "required": { + "text": ("STRING", {"default": "Hello, World!", "multiline": True}), + "font": (font_files, ), + "max_width": ("INT", {"default": 300, "min": 1, "max": 2048, "step": 1}), + "font_properties": ("STRING", {"default": "#FFFFFF,1,1,C,1", "multiline": False}), + "font_stroke": ("STRING", {"default": "#000000,2,1", "multiline": False}), + "font_background": ("STRING", {"default": "#333333,5,10,1", "multiline": False}) + }, + "optional": {"any": (any_typ,)} + } + RETURN_TYPES = ("IMAGE",) + FUNCTION = "generate_text_image" + CATEGORY = "Meeeyo/File" + DESCRIPTION = note + def IS_CHANGED(): return float("NaN") + + def generate_text_image(self, text, font, max_width, font_properties, font_stroke, font_background): + fonts_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts") + font_path = os.path.join(fonts_dir, font) + try: + font = ImageFont.truetype(font_path, 1) + except Exception as e: + return None + + draw = ImageDraw.Draw(Image.new('RGBA', (1, 1))) + lines = text.split("\n") + max_text_width = 0 + text_height = 0 + for line in lines: + left, top, right, bottom = draw.textbbox((0, 0), line, font=font) + line_width = right - left + line_height = bottom - top + max_text_width = max(max_text_width, line_width) + text_height += line_height + + if max_text_width == 0: + max_text_width = 1 + + ratio = max_width / max_text_width + new_font_size = int(1 * ratio) + font = ImageFont.truetype(font_path, new_font_size) + + font_color = "#FFFFFF" + letter_spacing_factor = 1.0 + line_spacing_factor = 1.0 + alignment = "C" + opacity = 1.0 + stroke_color = "#000000" + stroke_width = 0.0 + stroke_opacity = 1.0 + background_color = "#333333" + expand_width = 5 + corner_radius = 10 + background_opacity = 0.9 + + try: + props = font_properties.split(',') + if len(props) >= 5: + font_color = props[0].strip() + letter_spacing_factor = float(props[1]) if props[1] else 1.0 + line_spacing_factor = float(props[2]) if props[2] else 1.0 + alignment = props[3].strip().upper() + opacity = float(props[4]) if props[4] else 1.0 + except Exception as e: + pass + + try: + stroke_props = font_stroke.split(',') + if len(stroke_props) >= 3: + stroke_color = stroke_props[0].strip() + stroke_width = float(stroke_props[1]) if stroke_props[1] else 1.0 + stroke_opacity = float(stroke_props[2]) if stroke_props[2] else 1.0 + except Exception as e: + pass + + try: + bg_props = font_background.split(',') + if len(bg_props) >= 4: + background_color = bg_props[0].strip() + expand_width = int(bg_props[1]) if bg_props[1] else 5 + corner_radius = int(bg_props[2]) if bg_props[2] else 10 + background_opacity = float(bg_props[3]) if bg_props[3] else 0.9 + except Exception as e: + pass + + font_ascent, font_descent = font.getmetrics() + line_height = font_ascent + font_descent + text_height = line_height * len(lines) * line_spacing_factor + + image_width = max_width + image_height = int(text_height + new_font_size * 0.5) + image = Image.new('RGBA', (image_width, image_height), (0, 0, 0, 0)) + draw = ImageDraw.Draw(image) + + try: + background_color_tuple = ( + int(background_color[1:3], 16), + int(background_color[3:5], 16), + int(background_color[5:7], 16), + int(background_opacity * 255) + ) + draw.rounded_rectangle( + [0, 0, image_width, image_height], + fill=background_color_tuple, + radius=corner_radius + ) + except Exception as e: + pass + + y_text = new_font_size * 0.2 + try: + font_color_tuple = ( + int(font_color[1:3], 16), + int(font_color[3:5], 16), + int(font_color[5:7], 16), + int(opacity * 255) + ) + stroke_color_tuple = ( + int(stroke_color[1:3], 16), + int(stroke_color[3:5], 16), + int(stroke_color[5:7], 16), + int(stroke_opacity * 255) + ) + except Exception as e: + font_color_tuple = (255, 255, 255, 255) + stroke_color_tuple = (0, 0, 0, 255) + + for i, line in enumerate(lines): + line_width = 0 + for char in line: + char_width = draw.textbbox((0, 0), char, font=font)[2] + line_width += char_width + (font.size * (letter_spacing_factor - 1)) + line_width = max(line_width, 1) + + if alignment == "L": + x = 0 + elif alignment == "R": + x = max_width - line_width + else: + x = (max_width - line_width) / 2 + + if stroke_width > 0: + for sx in range(-int(stroke_width), int(stroke_width) + 1): + for sy in range(-int(stroke_width), int(stroke_width) + 1): + if sx == 0 and sy == 0: + continue + char_x = x + sx + char_y = y_text + sy + for char in line: + char_width = draw.textbbox((0, 0), char, font=font)[2] + draw.text((char_x, char_y), char, font=font, fill=stroke_color_tuple) + char_x += char_width + (font.size * (letter_spacing_factor - 1)) + + char_x = x + for char in line: + char_width = draw.textbbox((0, 0), char, font=font)[2] + draw.text((char_x, y_text), char, font=font, fill=font_color_tuple) + char_x += char_width + (font.size * (letter_spacing_factor - 1)) + + y_text += line_height * line_spacing_factor + + image_data = np.array(image) + alpha_channel = image_data[:, :, 3] + non_zero_indices = np.where(alpha_channel > 0) + if len(non_zero_indices[0]) > 0: + min_y = np.min(non_zero_indices[0]) + max_y = np.max(non_zero_indices[0]) + image = image.crop((0, min_y, max_width, max_y + 1)) + else: + pass + + image_height = image.size[1] + if image_height < 1: + image_height = 1 + image = image.resize((max_width, image_height)) + + image_np = np.array(image).astype(np.float32) / 255.0 + image_tensor = torch.from_numpy(image_np).unsqueeze(0) + + return (image_tensor,) + + +#======图像层叠加 +class ImageOverlayAlignment: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image1": ("IMAGE", {"forceInput": True}), + "image2": ("IMAGE", {"forceInput": True}), + "alignment": (["top_left", "top_center", "top_right", "bottom_left", "bottom_center", "bottom_right", "center"], ), + "scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}), + "opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}), + "offset": ("STRING", {"default": "0,0,0,0", "multiline": False}) + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "overlay_images" + CATEGORY = "image" + + def overlay_images(self, image1, image2, alignment, offset, scale, opacity): + image1_np = image1.cpu().numpy().squeeze() + image2_np = image2.cpu().numpy().squeeze() + img1 = Image.fromarray((image1_np * 255).astype(np.uint8)).convert('RGBA') + img2 = Image.fromarray((image2_np * 255).astype(np.uint8)).convert('RGBA') + img1_width, img1_height = img1.size + new_width = int(img1_width * scale) + new_height = int(img1_height * scale) + img1 = img1.resize((new_width, new_height), Image.LANCZOS) + img1 = self.adjust_opacity(img1, opacity) + img1_width, img1_height = img1.size + img2_width, img2_height = img2.size + max_width = max(img1_width, img2_width) + max_height = max(img1_height, img2_height) + canvas = Image.new('RGBA', (max_width, max_height), (0, 0, 0, 0)) + if alignment == "top_left": + img2_x, img2_y = 0, 0 + elif alignment == "top_center": + img2_x = (max_width - img2_width) // 2 + img2_y = 0 + elif alignment == "top_right": + img2_x = max_width - img2_width + img2_y = 0 + elif alignment == "bottom_left": + img2_x = 0 + img2_y = max_height - img2_height + elif alignment == "bottom_center": + img2_x = (max_width - img2_width) // 2 + img2_y = max_height - img2_height + elif alignment == "bottom_right": + img2_x = max_width - img2_width + img2_y = max_height - img2_height + elif alignment == "center": + img2_x = (max_width - img2_width) // 2 + img2_y = (max_height - img2_height) // 2 + + right_offset, left_offset, down_offset, up_offset = 0, 0, 0, 0 + offset_list = offset.split(',') + if len(offset_list) >= 4: + try: + right_offset = int(offset_list[0]) if offset_list[0] else 0 + left_offset = int(offset_list[1]) if offset_list[1] else 0 + down_offset = int(offset_list[2]) if offset_list[2] else 0 + up_offset = int(offset_list[3]) if offset_list[3] else 0 + except ValueError: + pass + if alignment == "top_left": + img1_x, img1_y = 0, 0 + elif alignment == "top_center": + img1_x = (max_width - img1_width) // 2 + img1_y = 0 + elif alignment == "top_right": + img1_x = max_width - img1_width + img1_y = 0 + elif alignment == "bottom_left": + img1_x = 0 + img1_y = max_height - img1_height + elif alignment == "bottom_center": + img1_x = (max_width - img1_width) // 2 + img1_y = max_height - img1_height + elif alignment == "bottom_right": + img1_x = max_width - img1_width + img1_y = max_height - img1_height + elif alignment == "center": + img1_x = (max_width - img1_width) // 2 + img1_y = (max_height - img1_height) // 2 + img1_x += right_offset - left_offset + img1_y += down_offset - up_offset + img1_x = max(0, min(img1_x, max_width - img1_width)) + img1_y = max(0, min(img1_y, max_height - img1_height)) + img2_x = max(0, min(img2_x, max_width - img2_width)) + img2_y = max(0, min(img2_y, max_height - img2_height)) + canvas.paste(img2, (img2_x, img2_y), img2.split()[-1]) + canvas.paste(img1, (img1_x, img1_y), img1.split()[-1]) + output_image = np.array(canvas).astype(np.float32) / 255.0 + output_tensor = torch.from_numpy(output_image).unsqueeze(0) + return (output_tensor,) + + def adjust_opacity(self, img, opacity): + if opacity < 1.0: + img = img.copy() + alpha = np.array(img.split()[-1]) * opacity + alpha = alpha.astype(np.uint8) + img.putalpha(Image.fromarray(alpha)) + return img + + #======读取表格数据 class ReadExcelData: @classmethod @@ -977,3 +1281,5 @@ class ReadExcelRowOrColumnDiff: except Exception as e: return (f"Error: {str(e)}",) + +