Files
MeeeyoAI-ComfyUI_StringOps/meyo_node_File.py
T
2025-05-01 14:52:51 +08:00

980 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 pathlib import Path
from openpyxl.drawing.image import Image as OpenpyxlImage
from PIL import Image as PILImage
from io import BytesIO
from . import any_typ, note
#======全能加载图像
class GenericImageLoader:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_input": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "load_image"
OUTPUT_NODE = False
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def load_image(self, image_input):
path = image_input.strip()
for ctrl in ['\u202a', '\u202b', '\u202c', '\u202d', '\u202e']:
while path.startswith(ctrl):
path = path.lstrip(ctrl)
source = None
lower = path.lower()
if lower.startswith('http://') or lower.startswith('https://'):
source = 'network'
elif os.path.isfile(path):
source = 'local'
else:
source = 'base64'
if source == 'local':
img = Image.open(path)
elif source == 'network':
resp = requests.get(path)
resp.raise_for_status()
img = Image.open(io.BytesIO(resp.content))
else: # 'base64'
if ',' in path and path.startswith('data:'):
_, data = path.split(',', 1)
else:
data = path
decoded = base64.b64decode(data)
img = Image.open(io.BytesIO(decoded))
img = img.convert('RGBA')
has_alpha = img.mode == 'RGBA'
if has_alpha:
alpha_channel = np.array(img)[:, :, 3]
mask = (alpha_channel > 0).astype(np.float32)
mask_tensor = torch.from_numpy(mask).unsqueeze(0).unsqueeze(0)
else:
mask_tensor = torch.zeros((1, 1, img.size[1], img.size[0]), dtype=torch.float32)
np_img = np.array(img).astype(np.float32) / 255.0
img_tensor = torch.from_numpy(np_img).unsqueeze(0)
return img_tensor, mask_tensor
#======加载重置图像
class LoadAndAdjustImage:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f.name for f in Path(input_dir).iterdir() if f.is_file()]
return {
"required": {
"image": (sorted(files), {"image_upload": True}),
"max_dimension": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 8}),
"size_option": (["No Change", "Custom", "Million Pixels", "Small", "Medium", "Large",
"480P-H(vid 4:3)", "480P-V(vid 3:4)", "720P-H(vid 16:9)", "720P-V(vid 9:16)", "832×480", "480×832"],
{"default": "No Change"})
}
}
RETURN_TYPES = ("IMAGE", "MASK", "STRING")
RETURN_NAMES = ("image", "mask", "info")
FUNCTION = "load_image"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def load_image(self, image, max_dimension, size_option):
image_path = folder_paths.get_annotated_filepath(image)
img = Image.open(image_path)
W, H = img.size
aspect_ratio = W / H
def get_target_size():
if size_option == "No Change":
# No resizing or cropping, just return the original size
return W, H
elif size_option == "Million Pixels":
return self._resize_to_million_pixels(W, H)
elif size_option == "Custom":
ratio = min(max_dimension / W, max_dimension / H)
return round(W * ratio), round(H * ratio)
size_options = {
"Small": (
(768, 512) if aspect_ratio >= 1.23 else
(512, 768) if aspect_ratio <= 0.82 else
(768, 768)
),
"Medium": (
(1216, 832) if aspect_ratio >= 1.23 else
(832, 1216) if aspect_ratio <= 0.82 else
(1216, 1216)
),
"Large": (
(1600, 1120) if aspect_ratio >= 1.23 else
(1120, 1600) if aspect_ratio <= 0.82 else
(1600, 1600)
),
"Million Pixels": self._resize_to_million_pixels(W, H), # Million Pixels option
"480P-H(vid 4:3)": (640, 480), # 480P-H, 640x480
"480P-V(vid 3:4)": (480, 640), # 480P-V, 480x640
"720P-H(vid 16:9)": (1280, 720), # 720P-H, 1280x720
"720P-V(vid 9:16)": (720, 1280), # 720P-V, 720x1280
"832×480": (832, 480), # 832x480
"480×832": (480, 832), # 480x832
}
return size_options[size_option]
target_width, target_height = get_target_size()
output_images = []
output_masks = []
for frame in ImageSequence.Iterator(img):
frame = ImageOps.exif_transpose(frame)
if frame.mode == 'P':
frame = frame.convert("RGBA")
elif 'A' in frame.getbands():
frame = frame.convert("RGBA")
if size_option == "No Change":
# No resizing, just use the original frame
image_frame = frame.convert("RGB")
else:
if size_option == "Custom" or size_option == "Million Pixels":
ratio = min(target_width / W, target_height / H)
adjusted_width = round(W * ratio)
adjusted_height = round(H * ratio)
image_frame = frame.convert("RGB").resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR)
else:
image_frame = frame.convert("RGB")
image_frame = ImageOps.fit(image_frame, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5))
image_array = np.array(image_frame).astype(np.float32) / 255.0
output_images.append(torch.from_numpy(image_array)[None,])
# Process the mask if available
if 'A' in frame.getbands():
mask_frame = frame.getchannel('A')
if size_option == "Custom" or size_option == "Million Pixels":
mask_frame = mask_frame.resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR)
else:
mask_frame = ImageOps.fit(mask_frame, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5))
mask = np.array(mask_frame).astype(np.float32) / 255.0
mask = 1. - mask
else:
if size_option == "Custom" or size_option == "Million Pixels":
mask = np.zeros((adjusted_height, adjusted_width), dtype=np.float32)
else:
mask = np.zeros((target_height, target_width), dtype=np.float32)
output_masks.append(torch.from_numpy(mask).unsqueeze(0))
output_image = torch.cat(output_images, dim=0) if len(output_images) > 1 else output_images[0]
output_mask = torch.cat(output_masks, dim=0) if len(output_masks) > 1 else output_masks[0]
info = f"Image Path: {image_path}\nOriginal Size: {W}x{H}\nAdjusted Size: {target_width}x{target_height}"
return (output_image, output_mask, info)
@classmethod
def VALIDATE_INPUTS(s, image):
if not folder_paths.exists_annotated_filepath(image):
return f"Invalid image file: {image}"
return True
def _resize_to_million_pixels(self, W, H):
aspect_ratio = W / H
target_area = 1000000 # 1 million pixels
if aspect_ratio > 1: # Landscape
width = int(np.sqrt(target_area * aspect_ratio))
height = int(target_area / width)
else: # Portrait
height = int(np.sqrt(target_area / aspect_ratio))
width = int(target_area / height)
width = (width + 7) // 8 * 8
height = (height + 7) // 8 * 8
return width, height
#======重置图像
class ImageAdjuster:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",), # 添加遮罩输入
"max_dimension": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 8}),
"size_option": ([
"Custom", "Million Pixels", "Small", "Medium", "Large",
"480P-H(vid 4:3)", "480P-V(vid 3:4)", "720P-H(vid 16:9)", "720P-V(vid 9:16)", "832×480", "480×832"],
{"default": "Million Pixels"}
)
}
}
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT")
RETURN_NAMES = ("image", "mask", "width", "height")
FUNCTION = "process_image"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def process_image(self, image, mask, max_dimension=1024, size_option="Custom"):
batch_size = image.shape[0]
processed_images = []
processed_masks = []
widths = []
heights = []
for i in range(batch_size):
current_image = image[i]
current_mask = mask[i]
input_pil_image = Image.fromarray((current_image.numpy() * 255).astype(np.uint8))
input_pil_mask = Image.fromarray((current_mask.numpy() * 255).astype(np.uint8))
W, H = input_pil_image.size
processed_image_pil = input_pil_image.copy()
processed_image_pil = ImageOps.exif_transpose(processed_image_pil)
processed_mask_pil = input_pil_mask.copy()
processed_mask_pil = ImageOps.exif_transpose(processed_mask_pil)
if processed_image_pil.mode == 'P':
processed_image_pil = processed_image_pil.convert("RGBA")
elif 'A' in processed_image_pil.getbands():
processed_image_pil = processed_image_pil.convert("RGBA")
if processed_mask_pil.mode != "L":
processed_mask_pil = processed_mask_pil.convert("L")
if size_option != "Custom":
aspect_ratio = W / H
size_options = {
"Small": (
(768, 512) if aspect_ratio >= 1.23 else
(512, 768) if aspect_ratio <= 0.82 else
(768, 768)
),
"Medium": (
(1216, 832) if aspect_ratio >= 1.23 else
(832, 1216) if aspect_ratio <= 0.82 else
(1216, 1216)
),
"Large": (
(1600, 1120) if aspect_ratio >= 1.23 else
(1120, 1600) if aspect_ratio <= 0.82 else
(1600, 1600)
),
"Million Pixels": self._resize_to_million_pixels(W, H),
"480P-H(vid 4:3)": (640, 480),
"480P-V(vid 3:4)": (480, 640),
"720P-H(vid 16:9)": (1280, 720),
"720P-V(vid 9:16)": (720, 1280),
"832×480": (832, 480),
"480×832": (480, 832)
}
target_width, target_height = size_options[size_option]
processed_image_pil = processed_image_pil.convert("RGB")
processed_image_pil = ImageOps.fit(processed_image_pil, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5))
processed_mask_pil = ImageOps.fit(processed_mask_pil, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5))
else:
ratio = min(max_dimension / W, max_dimension / H)
adjusted_width = round(W * ratio)
adjusted_height = round(H * ratio)
processed_image_pil = processed_image_pil.convert("RGB")
processed_image_pil = processed_image_pil.resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR)
processed_mask_pil = processed_mask_pil.resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR)
processed_image = np.array(processed_image_pil).astype(np.float32) / 255.0
processed_image = torch.from_numpy(processed_image)
processed_images.append(processed_image)
processed_mask = np.array(processed_mask_pil).astype(np.float32) / 255.0
processed_mask = torch.from_numpy(processed_mask)
processed_masks.append(processed_mask)
if size_option != "Custom":
widths.append(target_width)
heights.append(target_height)
else:
widths.append(adjusted_width)
heights.append(adjusted_height)
output_image = torch.stack(processed_images)
output_mask = torch.stack(processed_masks)
if all(w == widths[0] for w in widths) and all(h == heights[0] for h in heights):
return (output_image, output_mask, widths[0], heights[0])
else:
return (output_image, output_mask, widths[0], heights[0])
def _resize_to_million_pixels(self, W, H):
aspect_ratio = W / H
target_area = 1000000
if aspect_ratio > 1:
width = int(np.sqrt(target_area * aspect_ratio))
height = int(target_area / width)
else:
height = int(np.sqrt(target_area / aspect_ratio))
width = int(target_area / height)
width = (width + 7) // 8 * 8
height = (height + 7) // 8 * 8
return width, height
#======裁剪图像
class CustomCrop:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",), # 新增遮罩输入
"width": ("INT", {"default": 768, "min": 0, "max": 4096, "step": 8}),
"height": ("INT", {"default": 768, "min": 0, "max": 4096, "step": 8}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT") # 新增遮罩输出
RETURN_NAMES = ("image", "mask", "width", "height")
FUNCTION = "process_image"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def process_image(self, image, mask, width=768, height=768):
input_image = Image.fromarray((image.squeeze(0).numpy() * 255).astype(np.uint8))
input_mask = Image.fromarray((mask.squeeze(0).numpy() * 255).astype(np.uint8)) # 转换遮罩为PIL图像
W, H = input_image.size
processed_images = []
processed_masks = [] # 新增遮罩处理列表
for frame in [input_image]:
frame = ImageOps.exif_transpose(frame)
if frame.mode == 'P':
frame = frame.convert("RGBA")
elif 'A' in frame.getbands():
frame = frame.convert("RGBA")
processed_image = frame.convert("RGB")
processed_image = ImageOps.fit(processed_image, (width, height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5))
processed_image = np.array(processed_image).astype(np.float32) / 255.0
processed_image = torch.from_numpy(processed_image)[None,]
processed_images.append(processed_image)
# 处理遮罩
input_mask = ImageOps.exif_transpose(input_mask)
processed_mask = input_mask.convert("L") # 确保遮罩是灰度图像
processed_mask = ImageOps.fit(processed_mask, (width, height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5))
processed_mask = np.array(processed_mask).astype(np.float32) / 255.0
processed_mask = torch.from_numpy(processed_mask)[None,]
processed_masks.append(processed_mask)
output_image = torch.cat(processed_images, dim=0) if len(processed_images) > 1 else processed_images[0]
output_mask = torch.cat(processed_masks, dim=0) if len(processed_masks) > 1 else processed_masks[0]
return (output_image, output_mask, width, height)
#======保存图像
class SaveImagEX:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"save_path": ("STRING", {"default": "./output"}),
"image_name": ("STRING", {"default": "ComfyUI"}),
"image_format": (["PNG", "JPG"], {"default": "JPG"})
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "save_image"
OUTPUT_NODE = True
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def save_image(self, image, save_path, image_name, image_format):
if not isinstance(image, torch.Tensor):
raise ValueError("Invalid image tensor format")
if save_path == "./output":
save_path = self.output_dir
elif not os.path.isabs(save_path):
save_path = os.path.join(self.output_dir, save_path)
os.makedirs(save_path, exist_ok=True)
# 移除可能存在的扩展名,然后添加用户选择的格式对应的扩展名
base_name = os.path.splitext(image_name)[0]
batch_size = image.shape[0]
channel_to_mode = {1: "L", 3: "RGB", 4: "RGBA"}
for i in range(batch_size):
if image_format == "PNG":
filename = f"{base_name}.png" if batch_size == 1 else f"{base_name}_{i:05d}.png"
save_format = "PNG"
save_params = {"compress_level": 0}
else: # JPG
filename = f"{base_name}.jpg" if batch_size == 1 else f"{base_name}_{i:05d}.jpg"
save_format = "JPEG"
save_params = {"quality": 100}
full_path = os.path.join(save_path, filename)
single_image = image[i].cpu().numpy()
single_image = (single_image * 255).astype('uint8')
channels = single_image.shape[2]
if channels not in channel_to_mode:
raise ValueError(f"Unsupported channel number: {channels}")
mode = channel_to_mode[channels]
if channels == 1:
single_image = single_image[:, :, 0]
pil_image = Image.fromarray(single_image, mode)
if image_format == "JPG":
pil_image = pil_image.convert("RGB")
pil_image.save(full_path, format=save_format, **save_params)
return (image,)
#======文件操作
class FileCopyCutNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"source_path": ("STRING", {"default": "", "multiline": False}),
"destination_path": ("STRING", {"default": "", "multiline": False}),
"operation": (["copy", "cut"], {"default": "copy"}),
},
"optional": {"any": (any_typ,)} # 可以进一步简化成一行
}
RETURN_TYPES = ("STRING",) # 返回操作结果字符串
RETURN_NAMES = ("result",)
FUNCTION = "copy_cut_file"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def copy_cut_file(self, source_path, destination_path, operation, any=None):
result = "执行失败"
try:
if not os.path.isfile(source_path):
raise FileNotFoundError(f"源文件未找到: {source_path}")
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
if operation == "copy":
shutil.copy2(source_path, destination_path)
result = "执行成功: 文件已复制"
elif operation == "cut":
shutil.move(source_path, destination_path)
result = "执行成功: 文件已剪切"
else:
raise ValueError("操作类型无效,仅支持 'copy' 或 'cut'。")
except FileNotFoundError as e:
result = f"执行失败: {str(e)}"
except ValueError as e:
result = f"执行失败: {str(e)}"
except Exception as e:
result = f"执行失败: {str(e)}"
return (result,)
#======替换文件名
class FileNameReplacer:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"file_path": ("STRING", {"default": "path/to/your/file.jpg"}),
"new_file_name": ("STRING", {"default": ""}),
},
"optional": {"any": (any_typ,)}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "replace_file_name"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def replace_file_name(self, file_path, new_file_name, any=None):
dir_name = os.path.dirname(file_path)
_, file_ext = os.path.splitext(file_path)
new_file_name = self.sanitize_file_name(new_file_name)
new_file_path = os.path.join(dir_name, new_file_name + file_ext)
return (new_file_path,)
def sanitize_file_name(self, file_name):
invalid_chars = r'[\/:*?"<>|]'
return re.sub(invalid_chars, '_', file_name)
#======文本写入TXT
class WriteToTxtFile:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text_content": ("STRING", {"default": "", "multiline": True}),
"file_path": ("STRING", {"default": "path/to/your/file.txt"}),
},
"optional": {"any": (any_typ,)}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "write_to_txt"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def write_to_txt(self, text_content, file_path, any=None):
try:
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(text_content)
return ("Write successful: " + text_content,)
except Exception as e:
return (f"Error: {str(e)}",)
#======清理文件
class FileDeleteNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"items_to_delete": ("STRING", {"default": "33.png\ncs1/01.png\ncs1", "multiline": True}),
},
"optional": {"any": (any_typ,)}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("result",)
FUNCTION = "delete_files"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def delete_files(self, items_to_delete, any=None):
result = "执行成功: 所有指定项已从output目录删除"
error_messages = []
base_output_dir = Path.cwd() / COMFYUI_OUTPUT_DIR
items = items_to_delete.strip().split('\n')
for item in items:
item = item.strip()
if not item:
continue
if item == "[DeleteAll]":
try:
for file_or_dir in base_output_dir.glob('*'):
if file_or_dir.is_file() or file_or_dir.is_symlink():
file_or_dir.unlink()
elif file_or_dir.is_dir():
shutil.rmtree(file_or_dir)
continue
except Exception as e:
error_messages.append(f"从output目录删除全部失败: {str(e)}")
continue
target_path = base_output_dir / item
try:
target_path.relative_to(base_output_dir)
except ValueError:
error_messages.append(f"{item} 不在output目录范围内,无法删除")
continue
if not target_path.exists():
error_messages.append(f"在output目录下找不到 {item}")
continue
try:
if target_path.is_file() or target_path.is_symlink():
target_path.unlink()
elif target_path.is_dir():
shutil.rmtree(target_path)
except Exception as e:
error_messages.append(f"从output目录删除 {item} 失败: {str(e)}")
if error_messages:
result = "部分执行失败:\n" + "\n".join(error_messages)
return (result,)
#======文件路径和后缀统计
class FileListAndSuffix:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"folder_path": ("STRING",),
"file_extension": (["jpg", "png", "jpg&png", "txt", "csv", "all"], {"default": "jpg"}),
},
"optional": {"any": (any_typ,)}
}
RETURN_TYPES = ("STRING", "INT", "LIST")
FUNCTION = "file_list_and_suffix"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def file_list_and_suffix(self, folder_path, file_extension, any=None):
try:
if not os.path.isdir(folder_path):
return ("", 0, [])
if file_extension == "all":
file_paths = [os.path.join(folder_path, f) for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
elif file_extension == "jpg&png":
file_paths = [os.path.join(folder_path, f) for f in os.listdir(folder_path) if f.lower().endswith(('.jpg', '.png'))]
else:
file_paths = [os.path.join(folder_path, f) for f in os.listdir(folder_path) if f.lower().endswith('.' + file_extension)]
return ('\n'.join(file_paths), len(file_paths), file_paths)
except Exception as e:
return ("", 0, [])
#======读取表格数据
class ReadExcelData:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}),
"sheet_name": ("STRING", {"default": "Sheet1"}),
"row_range": ("STRING", {"default": "2-3"}),
"col_range": ("STRING", {"default": "1-4"}),
},
"optional": {"any": (any_typ,)}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "read_excel_data"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def read_excel_data(self, excel_path, sheet_name, row_range, col_range, any=None):
try:
if "-" in row_range:
start_row, end_row = map(int, row_range.split("-"))
else:
start_row = end_row = int(row_range)
if "-" in col_range:
start_col, end_col = map(int, col_range.split("-"))
else:
start_col = end_col = int(col_range)
start_row = max(1, start_row)
start_col = max(1, start_col)
workbook = openpyxl.load_workbook(excel_path, read_only=True, data_only=True)
sheet = workbook[sheet_name]
output_lines = []
for row in range(start_row, end_row + 1):
row_data = []
for col in range(start_col, end_col + 1):
cell_value = sheet.cell(row=row, column=col).value
row_data.append(str(cell_value) if cell_value is not None else "")
output_lines.append("|".join(row_data))
workbook.close()
del workbook
return ("\n".join(output_lines),)
except Exception as e:
return (f"Error: {str(e)}",)
#======写入表格数据
class WriteExcelData:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}),
"sheet_name": ("STRING", {"default": "Sheet1"}),
"row_range": ("STRING", {"default": "2-3"}),
"col_range": ("STRING", {"default": "1-4"}),
"data": ("STRING", {"default": "", "multiline": True}),
},
"optional": {"any": (any_typ,)}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "write_excel_data"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def write_excel_data(self, excel_path, sheet_name, row_range, col_range, data, any=None):
try:
if not os.path.exists(excel_path):
return (f"Error: File does not exist at path: {excel_path}",)
if not os.access(excel_path, os.W_OK):
return (f"Error: No write permission for file at path: {excel_path}",)
if "-" in row_range:
start_row, end_row = map(int, row_range.split("-"))
else:
start_row = end_row = int(row_range)
if "-" in col_range:
start_col, end_col = map(int, col_range.split("-"))
else:
start_col = end_col = int(col_range)
start_row = max(1, start_row)
start_col = max(1, start_col)
workbook = openpyxl.load_workbook(excel_path, read_only=False, data_only=True)
sheet = workbook[sheet_name]
data_lines = data.strip().split("\n")
for row_index, line in enumerate(data_lines, start=start_row):
if row_index > end_row:
break
cell_values = line.split("|")
for col_index, cell_value in enumerate(cell_values, start=start_col):
if col_index > end_col:
break
if cell_value.strip():
sheet.cell(row=row_index, column=col_index).value = cell_value.strip()
workbook.save(excel_path)
workbook.close()
del workbook
return ("Data written successfully!",)
except PermissionError as pe:
return (f"Permission Error: {str(pe)}",)
except Exception as e:
return (f"Error: {str(e)}",)
#======图片插入表格
class WriteExcelImage:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}),
"sheet_name": ("STRING", {"default": "Sheet1"}),
"row_range": ("STRING", {"default": "1"}),
"col_range": ("STRING", {"default": "1"}),
"image_path": ("STRING", {"default": "path/to/your/image.png"}),
},
"optional": {"any": (any_typ,)}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "write_image"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def write_image(self, excel_path, sheet_name, row_range, col_range, image_path, any=None):
try:
if not os.path.exists(excel_path):
return (f"Error: Excel file does not exist at path: {excel_path}",)
if not os.access(excel_path, os.W_OK):
return (f"Error: No write permission for Excel file at path: {excel_path}",)
if not os.path.exists(image_path):
return (f"Error: Image file does not exist at path: {image_path}",)
if not os.access(image_path, os.R_OK):
return (f"Error: No read permission for image file at path: {image_path}",)
if "-" in row_range:
start_row, end_row = map(int, row_range.split("-"))
else:
start_row = end_row = int(row_range)
if "-" in col_range:
start_col, end_col = map(int, col_range.split("-"))
else:
start_col = end_col = int(col_range)
start_row = max(1, start_row)
start_col = max(1, start_col)
workbook = openpyxl.load_workbook(excel_path, read_only=False, data_only=True)
sheet = workbook[sheet_name]
thumbnail_size = (128, 128)
with PILImage.open(image_path) as img:
img = img.resize(thumbnail_size)
img_byte_array = BytesIO()
img.save(img_byte_array, format='PNG')
img_byte_array.seek(0)
openpyxl_img = OpenpyxlImage(img_byte_array)
cell_address = openpyxl.utils.get_column_letter(start_col) + str(start_row)
sheet.add_image(openpyxl_img, cell_address)
workbook.save(excel_path)
workbook.close()
return ("Image inserted successfully!",)
except PermissionError as pe:
return (f"Permission Error: {str(pe)}",)
except Exception as e:
return (f"Error: {str(e)}",)
#======查找表格数据
class FindExcelData:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}),
"sheet_name": ("STRING", {"default": "Sheet1"}),
"search_content": ("STRING", {"default": "查找内容"}),
"search_mode": (["精确查找", "模糊查找"], {"default": "精确查找"}),
},
"optional": {"any": (any_typ,)}
}
RETURN_TYPES = ("STRING", "INT", "INT")
FUNCTION = "find_excel_data"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def find_excel_data(self, excel_path, sheet_name, search_content, search_mode, any=None):
try:
if not os.path.exists(excel_path):
return (f"Error: File does not exist at path: {excel_path}", None, None)
if not os.access(excel_path, os.R_OK):
return (f"Error: No read permission for file at path: {excel_path}", None, None)
workbook = openpyxl.load_workbook(excel_path, read_only=True, data_only=True)
sheet = workbook[sheet_name]
results = []
found_row = None
found_col = None
for row in range(1, sheet.max_row + 1):
for col in range(1, sheet.max_column + 1):
cell = sheet.cell(row=row, column=col)
cell_value = cell.value if cell.value is not None else ""
cell_value_str = str(cell_value)
if (search_mode == "精确查找" and cell_value_str == search_content) or \
(search_mode == "模糊查找" and search_content in cell_value_str):
results.append(f"{sheet_name}|{row}|{col}|{cell_value}")
found_row = row
found_col = col
workbook.close()
del workbook
if not results:
return ("No results found.", None, None)
return ("\n".join(results), found_row, found_col)
except Exception as e:
return (f"Error: {str(e)}", None, None)
#======读取表格数量差
class ReadExcelRowOrColumnDiff:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}),
"sheet_name": ("STRING", {"default": "Sheet1"}),
"read_mode": (["读行", "读列"], {"default": "读行"}),
"indices": ("STRING", {"default": "1,3"}),
},
"optional": {"any": (any_typ,)}
}
RETURN_TYPES = ("INT",)
FUNCTION = "read_excel_row_or_column_diff"
CATEGORY = "Meeeyo/File"
DESCRIPTION = note
def IS_CHANGED(): return float("NaN")
def read_excel_row_or_column_diff(self, excel_path, sheet_name, read_mode, indices, any=None):
try:
if not os.path.exists(excel_path):
return (f"Error: File does not exist at path: {excel_path}",)
if not os.access(excel_path, os.R_OK):
return (f"Error: No read permission for file at path: {excel_path}",)
workbook = openpyxl.load_workbook(excel_path, read_only=True, data_only=True)
sheet = workbook[sheet_name]
def count_cells(mode, index):
count = 0
if mode == "读行":
for col in range(1, sheet.max_column + 1):
cell_value = sheet.cell(row=index, column=col).value
if cell_value is not None:
count += 1
else:
break
elif mode == "读列":
for row in range(1, sheet.max_row + 1):
cell_value = sheet.cell(row=row, column=index).value
if cell_value is not None:
count += 1
else:
break
return count
indices = indices.strip()
if "," in indices:
try:
index1, index2 = map(int, indices.split(","))
except ValueError:
return (f"Error: Invalid indices format. Please use 'number,number' format.",)
count1 = count_cells(read_mode, index1)
count2 = count_cells(read_mode, index2)
result = count1 - count2
else:
try:
index = int(indices)
except ValueError:
return (f"Error: Invalid index format. Please enter a valid number.",)
result = count_cells(read_mode, index)
workbook.close()
del workbook
return (result,)
except Exception as e:
return (f"Error: {str(e)}",)