Adding the RS Load Images From Dir node

This commit is contained in:
Raykosan
2026-07-30 09:24:18 +03:00
parent c65da90888
commit 0c5f334995
5 changed files with 618 additions and 3 deletions
+98
View File
@@ -0,0 +1,98 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025-2026 Raykosan (RaykoStudio)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import glob
import torch
import numpy as np
import folder_paths
from PIL import Image
from typing import List, Dict, Any
class RSLoadImagesFromDir:
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Dict[str, Any]]:
return {
"required": {
"folder_path": ("STRING", {"default": "", "multiline": False}),
"filter_type": (["*.*", "*.png", "*.jpg", "*.jpeg", "*.webp", "*.bmp", "*.gif", "custom"], {"default": "*.png"}),
"start_index": ("INT", {"default": 1, "min": 1, "max": 999999, "step": 1}),
"end_index": ("INT", {"default": 1, "min": 1, "max": 999999, "step": 1}),
},
"optional": {
"custom_filter": ("STRING", {"default": "*.png", "multiline": False}),
}
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT")
RETURN_NAMES = ("image", "filename", "folder_path", "number_of_files")
OUTPUT_IS_LIST = (True, True, False, False)
FUNCTION = "load_images"
CATEGORY = "🦊 RaykoStudio"
DESCRIPTION = "Load images from directory. Preserves original size and alpha channel."
def load_images(
self,
folder_path: str,
filter_type: str,
start_index: int,
end_index: int,
custom_filter: str = "*.png"
):
if not folder_path or not os.path.isdir(folder_path):
raise ValueError(f"Invalid folder path: {folder_path}")
pattern = custom_filter if filter_type == "custom" else filter_type
search_pattern = os.path.join(folder_path, pattern)
file_list = sorted(
[f for f in glob.glob(search_pattern) if os.path.isfile(f)],
key=lambda x: os.path.basename(x).lower()
)
total_files = len(file_list)
images_list: List[torch.Tensor] = []
filenames_list: List[str] = []
if total_files == 0:
return (images_list, filenames_list, folder_path, total_files)
start_idx = max(0, min(start_index - 1, total_files - 1))
end_idx = max(start_idx, min(end_index - 1, total_files - 1))
for idx in range(start_idx, end_idx + 1):
filepath = file_list[idx]
filename = os.path.basename(filepath)
try:
img = Image.open(filepath)
img_np = np.array(img).astype(np.float32) / 255.0
if img_np.ndim == 2:
img_np = np.stack([img_np] * 3, axis=-1)
img_tensor = torch.from_numpy(img_np).unsqueeze(0)
images_list.append(img_tensor)
filenames_list.append(filename)
except Exception as e:
print(f"[RaykoStudio] Error loading {filename}: {e}")
return (images_list, filenames_list, folder_path, total_files)
NODE_CLASS_MAPPINGS = {"RSLoadImagesFromDir": RSLoadImagesFromDir}
NODE_DISPLAY_NAME_MAPPINGS = {"RSLoadImagesFromDir": "🦊 RS Load Images From Dir"}
+1 -1
View File
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
REQUIRED_MODULES: Set[str] = set()
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
__version__ = "0.33.2"
__version__ = "0.34.0"
def load_modules():
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ComfyUI_RaykoStudio",
"version": "0.33.2",
"version": "0.34.0",
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
"author": "Raykosan",
"license": "Apache License 2.0",
@@ -24,6 +24,7 @@
"Rayko_Insert_Crop.py"
"Rayko_Intermediate_Spline_Mask.py"
"Rayko_Last_Frame.py"
"Rayko_Load_Images_From_Dir.py"
"Rayko_Loop_Switch.py"
"Rayko_LoRA Loader.py"
"Rayko_Models_Loader.py"
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI_RaykoStudio"
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
version = "0.33.2"
version = "0.34.0"
license = { file = "LICENSE" }
dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
+516
View File
@@ -0,0 +1,516 @@
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "RaykoStudio.LoadImagesFromDir",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "RSLoadImagesFromDir") return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const node = this;
node.data = {
folder_path: "",
filter_type: "*.png",
start_index: 1,
end_index: 1,
custom_filter: "*.png"
};
const filters = ["*.*", "*.png", "*.jpg", "*.jpeg", "*.webp", "*.gif", "custom"];
const NODE_WIDTH = 260;
node.targetWidth = NODE_WIDTH;
const MIN_HEIGHT_NORMAL = 255;
const MIN_HEIGHT_CUSTOM = 280;
node.setSize([NODE_WIDTH, MIN_HEIGHT_NORMAL]);
node.min_size = [NODE_WIDTH, MIN_HEIGHT_NORMAL];
const wPath = node.widgets?.find(w => w.name === "folder_path");
const wFilter = node.widgets?.find(w => w.name === "filter_type");
const wStart = node.widgets?.find(w => w.name === "start_index");
const wEnd = node.widgets?.find(w => w.name === "end_index");
const wCustom = node.widgets?.find(w => w.name === "custom_filter");
[wPath, wFilter, wStart, wEnd, wCustom].forEach(w => {
if (w) {
w.hidden = true;
if (w.element) w.element.style.display = "none";
}
});
if (wPath) wPath.serializeValue = () => node.data.folder_path;
if (wFilter) wFilter.serializeValue = () => node.data.filter_type;
if (wStart) wStart.serializeValue = () => parseInt(node.data.start_index);
if (wEnd) wEnd.serializeValue = () => parseInt(node.data.end_index);
if (wCustom) wCustom.serializeValue = () => node.data.custom_filter;
if (wPath?.value) node.data.folder_path = wPath.value;
if (wFilter?.value) node.data.filter_type = wFilter.value;
if (wStart?.value) node.data.start_index = parseInt(wStart.value);
if (wEnd?.value) node.data.end_index = parseInt(wEnd.value);
if (wCustom?.value) node.data.custom_filter = wCustom.value;
node.uiElements = [];
node.dropdownMenu = null;
node.customModal = null;
const syncData = () => {
if (wPath) wPath.value = node.data.folder_path;
if (wFilter) wFilter.value = node.data.filter_type;
if (wStart) wStart.value = parseInt(node.data.start_index);
if (wEnd) wEnd.value = parseInt(node.data.end_index);
if (wCustom) wCustom.value = node.data.custom_filter;
if (node.graph) node.graph.changeTracker?.dispatchEvent(new Event("change"));
node.setDirtyCanvas(true, true);
};
const updateNodeSize = () => {
const targetHeight = node.data.filter_type === "custom" ? MIN_HEIGHT_CUSTOM : MIN_HEIGHT_NORMAL;
node.min_size = [NODE_WIDTH, targetHeight];
if (Math.abs(node.size[1] - targetHeight) > 1) {
node.setSize([node.size[0], targetHeight]);
node.setDirtyCanvas(true, true);
}
};
node.onResize = function() {
const minHeight = this.data.filter_type === "custom" ? MIN_HEIGHT_CUSTOM : MIN_HEIGHT_NORMAL;
if (this.size[0] < NODE_WIDTH) {
this.size[0] = NODE_WIDTH;
}
if (this.size[1] < minHeight) {
this.size[1] = minHeight;
}
this.setDirtyCanvas(true, true);
};
const closeDropdown = (e) => {
if (node.dropdownMenu && !node.dropdownMenu.contains(e.target)) {
node.dropdownMenu.remove();
node.dropdownMenu = null;
}
};
const showCustomPrompt = (title, defaultValue, callback) => {
if (node.customModal) {
node.customModal.remove();
node.customModal = null;
}
const backdrop = document.createElement("div");
backdrop.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5); z-index: 10002;
display: flex; align-items: center; justify-content: center;
`;
const modal = document.createElement("div");
modal.style.cssText = `
background: #1a1a1a; border: 1px solid #444; border-radius: 3px;
padding: 16px; min-width: 320px; max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.7);
font-family: sans-serif; font-size: 12px; color: #ddd;
`;
const titleEl = document.createElement("div");
titleEl.textContent = title;
titleEl.style.cssText = `
margin-bottom: 12px; font-size: 13px; color: #fff; font-weight: bold;
`;
modal.appendChild(titleEl);
const input = document.createElement("input");
input.type = "text";
input.value = defaultValue || "";
input.style.cssText = `
width: 100%; box-sizing: border-box; padding: 8px 10px;
background: #222; border: 1px solid #666; border-radius: 3px;
color: #fff; font-size: 12px; outline: none;
font-family: monospace;
`;
input.onfocus = () => { input.style.borderColor = "#2196F3"; };
input.onblur = () => { input.style.borderColor = "#666"; };
modal.appendChild(input);
const btnContainer = document.createElement("div");
btnContainer.style.cssText = `
display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px;
`;
const btnCancel = document.createElement("button");
btnCancel.textContent = "Cancel";
btnCancel.style.cssText = `
padding: 6px 16px; background: #2a2a2a; border: 1px solid #888;
border-radius: 3px; color: #ddd; font-size: 12px; cursor: pointer;
font-family: sans-serif;
`;
btnCancel.onmouseover = () => { btnCancel.style.background = "#3a3a3a"; };
btnCancel.onmouseout = () => { btnCancel.style.background = "#2a2a2a"; };
const btnOk = document.createElement("button");
btnOk.textContent = "OK";
btnOk.style.cssText = `
padding: 6px 16px; background: #2196F3; border: 1px solid #2196F3;
border-radius: 3px; color: #fff; font-size: 12px; cursor: pointer;
font-family: sans-serif; font-weight: bold;
`;
btnOk.onmouseover = () => { btnOk.style.background = "#1976D2"; };
btnOk.onmouseout = () => { btnOk.style.background = "#2196F3"; };
btnContainer.appendChild(btnCancel);
btnContainer.appendChild(btnOk);
modal.appendChild(btnContainer);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
node.customModal = backdrop;
const close = (result) => {
backdrop.remove();
node.customModal = null;
document.removeEventListener("keydown", keyHandler);
if (callback) callback(result);
};
const confirm = () => close(input.value);
const cancel = () => close(null);
btnOk.onclick = (e) => { e.stopPropagation(); confirm(); };
btnCancel.onclick = (e) => { e.stopPropagation(); cancel(); };
backdrop.onclick = (e) => {
if (e.target === backdrop) cancel();
};
const keyHandler = (e) => {
if (e.key === "Enter") {
e.preventDefault();
confirm();
} else if (e.key === "Escape") {
e.preventDefault();
cancel();
}
};
document.addEventListener("keydown", keyHandler);
setTimeout(() => {
input.focus();
input.select();
}, 10);
};
const truncatePath = (ctx, path, maxWidth) => {
if (!path) return "Folder not selected";
const ellipsis = "...";
if (ctx.measureText(path).width <= maxWidth) {
return path;
}
let truncated = path;
while (truncated.length > 0 && ctx.measureText(ellipsis + truncated).width > maxWidth) {
truncated = truncated.substring(1);
}
return ellipsis + truncated;
};
const drawRoundedRect = (ctx, x, y, w, h, r) => {
if (ctx.roundRect) {
ctx.beginPath();
ctx.roundRect(x, y, w, h, r);
ctx.fill();
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
};
node.onDrawForeground = function(ctx) {
if (this.flags.collapsed) return;
const [w, h] = this.size;
const padding = 10;
const rowHeight = 23;
const borderRadius = 3;
let currentY = 35 + 60;
ctx.font = "12px sans-serif";
ctx.textBaseline = "middle";
const pathDisplayHeight = 20;
const pathDisplayY = currentY;
ctx.fillStyle = "#1a1a1a";
ctx.strokeStyle = "#333";
drawRoundedRect(ctx, padding, pathDisplayY, w - padding * 2, pathDisplayHeight, borderRadius);
const pathText = node.data.folder_path || "Folder not selected";
const maxTextWidth = w - padding * 2 - 10;
const displayPath = truncatePath(ctx, pathText, maxTextWidth);
ctx.fillStyle = node.data.folder_path ? "#aaa" : "#666";
ctx.textAlign = "left";
ctx.fillText(displayPath, padding + 5, pathDisplayY + pathDisplayHeight / 2);
currentY += pathDisplayHeight + 7;
ctx.fillStyle = "#2a2a2a";
ctx.strokeStyle = "#2196F3";
drawRoundedRect(ctx, padding, currentY, w - padding * 2, rowHeight, borderRadius);
ctx.fillStyle = "#2196F3";
ctx.textAlign = "center";
ctx.fillText("📁 Select folder", w / 2, currentY + rowHeight / 2);
this.uiElements.push({ type: "btn_folder", x: padding, y: currentY, w: w - padding * 2, h: rowHeight });
currentY += rowHeight + 7;
const filterLabel = `▼ ${node.data.filter_type}`;
ctx.fillStyle = "#2a2a2a";
ctx.strokeStyle = "#4CAF50";
drawRoundedRect(ctx, padding, currentY, w - padding * 2, rowHeight, borderRadius);
ctx.fillStyle = "#4CAF50";
ctx.textAlign = "center";
ctx.fillText(filterLabel, w / 2, currentY + rowHeight / 2);
this.uiElements.push({ type: "btn_filter", x: padding, y: currentY, w: w - padding * 2, h: rowHeight });
currentY += rowHeight + 7;
const drawNumberInput = (label, value, yPos, dataType) => {
const labelW = 40;
const btnW = 20;
const gap = 4;
const totalW = w - padding * 2;
const valW = totalW - labelW - (btnW * 2) - (gap * 2);
ctx.fillStyle = "#aaa";
ctx.textAlign = "left";
ctx.fillText(label, padding, yPos + rowHeight / 2);
const btnX1 = padding + labelW + gap;
ctx.fillStyle = "#333";
ctx.strokeStyle = "#888";
drawRoundedRect(ctx, btnX1, yPos, btnW, rowHeight, borderRadius);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.fillText("-", btnX1 + btnW / 2, yPos + rowHeight / 2);
const valX = btnX1 + btnW + gap;
ctx.fillStyle = "#222";
ctx.strokeStyle = "#666";
drawRoundedRect(ctx, valX, yPos, valW, rowHeight, borderRadius);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.fillText(String(value), valX + valW / 2, yPos + rowHeight / 2);
const btnX2 = valX + valW + gap;
ctx.fillStyle = "#333";
ctx.strokeStyle = "#888";
drawRoundedRect(ctx, btnX2, yPos, btnW, rowHeight, borderRadius);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.fillText("+", btnX2 + btnW / 2, yPos + rowHeight / 2);
this.uiElements.push({ type: `btn_${dataType}_minus`, x: btnX1, y: yPos, w: btnW, h: rowHeight });
this.uiElements.push({ type: `val_${dataType}`, x: valX, y: yPos, w: valW, h: rowHeight });
this.uiElements.push({ type: `btn_${dataType}_plus`, x: btnX2, y: yPos, w: btnW, h: rowHeight });
};
drawNumberInput("Start", node.data.start_index, currentY, "start");
currentY += rowHeight + 5;
drawNumberInput("End", node.data.end_index, currentY, "end");
currentY += rowHeight + 12;
if (node.data.filter_type === "custom") {
ctx.fillStyle = "#aaa";
ctx.textAlign = "left";
ctx.fillText("Custom", padding, currentY + rowHeight / 2);
const custValW = w - padding * 2 - 45;
const custX = padding + 45;
ctx.fillStyle = "#222";
ctx.strokeStyle = "#FF9800";
drawRoundedRect(ctx, custX, currentY, custValW, rowHeight, borderRadius);
ctx.fillStyle = "#FF9800";
ctx.textAlign = "left";
ctx.fillText(node.data.custom_filter, custX + 5, currentY + rowHeight / 2);
this.uiElements.push({ type: "val_custom", x: custX, y: currentY, w: custValW, h: rowHeight });
}
};
node.onMouseDown = function(event, pos) {
const [x, y] = pos;
this.uiElements = [];
this.onDrawForeground(app.canvas.ctx);
for (const el of this.uiElements) {
if (x >= el.x && x <= el.x + el.w && y >= el.y && y <= el.y + el.h) {
if (el.type === "btn_folder") {
showCustomPrompt(
"Введите путь к папке",
this.data.folder_path,
(result) => {
if (result !== null) {
this.data.folder_path = result.trim();
syncData();
}
}
);
return true;
}
if (el.type === "btn_filter") {
if (this.dropdownMenu) this.dropdownMenu.remove();
const menu = document.createElement("div");
menu.style.cssText = `
position: fixed; background: #1a1a1a; border: 1px solid #444;
border-radius: 3px; max-height: 255px; overflow-y: auto;
z-index: 10001; box-shadow: 0 4px 20px rgba(0,0,0,0.5); min-width: 150px;
`;
filters.forEach(f => {
const item = document.createElement("div");
item.textContent = f;
item.style.cssText = `
padding: 8px 12px; cursor: pointer; color: #ddd; font-size: 12px;
border-bottom: 1px solid #333;
background: ${f === this.data.filter_type ? '#333' : '#1a1a1a'};
`;
item.onmouseover = () => item.style.background = "#444";
item.onmouseout = () => item.style.background = f === this.data.filter_type ? '#333' : "#1a1a1a";
item.onclick = (e) => {
e.stopPropagation();
this.data.filter_type = f;
syncData();
updateNodeSize();
menu.remove();
this.dropdownMenu = null;
};
menu.appendChild(item);
});
const canvasRect = app.canvas.canvas.getBoundingClientRect();
const ds = app.canvas.ds;
const nodeScreenX = canvasRect.left + ((this.pos[0] + ds.offset[0]) * ds.scale);
const nodeScreenY = canvasRect.top + ((this.pos[1] + ds.offset[1]) * ds.scale);
menu.style.left = (nodeScreenX + el.x) + "px";
menu.style.top = (nodeScreenY + el.y + el.h + 5) + "px";
document.body.appendChild(menu);
this.dropdownMenu = menu;
setTimeout(() => document.addEventListener("mousedown", closeDropdown), 100);
return true;
}
if (el.type === "btn_start_minus") {
this.data.start_index = Math.max(1, parseInt(this.data.start_index) - 1);
syncData();
return true;
}
if (el.type === "btn_start_plus") {
this.data.start_index = parseInt(this.data.start_index) + 1;
syncData();
return true;
}
if (el.type === "val_start") {
showCustomPrompt(
"Начальный индекс (мин. 1)",
String(this.data.start_index),
(result) => {
if (result !== null) {
this.data.start_index = Math.max(1, parseInt(result) || 1);
syncData();
}
}
);
return true;
}
if (el.type === "btn_end_minus") {
this.data.end_index = Math.max(1, parseInt(this.data.end_index) - 1);
syncData();
return true;
}
if (el.type === "btn_end_plus") {
this.data.end_index = parseInt(this.data.end_index) + 1;
syncData();
return true;
}
if (el.type === "val_end") {
showCustomPrompt(
"Конечный индекс",
String(this.data.end_index),
(result) => {
if (result !== null) {
this.data.end_index = Math.max(1, parseInt(result) || 1);
syncData();
}
}
);
return true;
}
if (el.type === "val_custom") {
showCustomPrompt(
"Пользовательский фильтр (например, *_mask.png)",
this.data.custom_filter,
(result) => {
if (result !== null) {
this.data.custom_filter = result.trim() || "*.png";
syncData();
}
}
);
return true;
}
}
}
return false;
};
const originalOnRemoved = node.onRemoved;
node.onRemoved = function() {
if (this.dropdownMenu) {
this.dropdownMenu.remove();
this.dropdownMenu = null;
}
if (this.customModal) {
this.customModal.remove();
this.customModal = null;
}
document.removeEventListener("mousedown", closeDropdown);
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
};
syncData();
updateNodeSize();
return result;
};
}
});