diff --git a/README.md b/README.md index 3a51681..40913cc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # ComfyUI_Ib_CustomNodes +## Installation +Install via the Custom Nodes Manager in [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager). + ## Load Image From Path ```python def LoadImageFromPath( @@ -27,11 +30,21 @@ The image path can be in the following format: `ComfyUI_00001_-assets\ComfyUI_00001_.png [temp]` -## Installation -Install via the Custom Nodes Manager in [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager). +### Usage Example + -## Usage Example - +## Load Image From Path (Enhanced) +Enhanced with image preview, file browser and a `path` output, made by @alFrame: + + + +File browser: + + + +Mask: + + ## PIL.Image ```python diff --git a/__init__.py b/__init__.py index c79c5d8..25f414c 100644 --- a/__init__.py +++ b/__init__.py @@ -1,9 +1,10 @@ -from .image import * +from .image import LoadImageFromPath, LoadImageFromPathEnhanced, PILToImage, PILToMask, ImageToPIL __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] NODE_CLASS_MAPPINGS = { 'LoadImageFromPath': LoadImageFromPath, + 'LoadImageFromPathEnhanced': LoadImageFromPathEnhanced, 'PILToImage': PILToImage, 'PILToMask': PILToMask, 'ImageToPIL': ImageToPIL, @@ -11,9 +12,12 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { 'LoadImageFromPath': 'Load Image From Path', + 'LoadImageFromPathEnhanced': 'Load Image From Path (Enhanced)', 'PILToImage': 'PIL To Image', 'PILToMask': 'PIL To Mask', 'ImageToPIL': 'Image To PIL', } +WEB_DIRECTORY = "./web" + print("\033[34mIb Custom Nodes: \033[92mLoaded\033[0m") diff --git a/image.py b/image.py index fc49f98..b055060 100644 --- a/image.py +++ b/image.py @@ -3,11 +3,19 @@ import torch import hashlib from pathlib import Path from typing import Iterable +import os +import json from PIL import Image, ImageOps import numpy as np import folder_paths +from aiohttp import web +from server import PromptServer + +# =================================================================================== +# ORIGINAL VERSION OF THE NODE +# =================================================================================== class LoadImageFromPath: @classmethod @@ -145,4 +153,223 @@ class ImageToPIL: i = 255. * image.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) pil_images.append(img) - return (pil_images,) \ No newline at end of file + return (pil_images,) + +# =================================================================================== +# ENHANCED VERSION WITH FILE BROWSER +# Features: Full file browser, sorting, preview, any path access, path output added +# =================================================================================== + +class LoadImageFromPathEnhanced: + @classmethod + def INPUT_TYPES(s): + return {"required": + {"image": ("STRING", {"default": r"ComfyUI_00001_-assets\ComfyUI_00001_.png [output]"})}, + } + + CATEGORY = "image" + + RETURN_TYPES = ("IMAGE", "MASK", "STRING") + RETURN_NAMES = ("IMAGE", "MASK", "path") + FUNCTION = "load_image" + + def load_image(self, image): + # Use the static method correctly + image_path = self._resolve_path(image) + + i = Image.open(image_path) + i = ImageOps.exif_transpose(i) + image_out = i.convert("RGB") + image_out = np.array(image_out).astype(np.float32) / 255.0 + image_out = torch.from_numpy(image_out)[None,] + if 'A' in i.getbands(): + mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 + mask = 1. - torch.from_numpy(mask) + + # Ensure mask has 3 dimensions: + mask = mask.unsqueeze(0) + else: + mask = torch.zeros((64,64), dtype=torch.float32, device="cpu") + + return (image_out, mask, image) + + def _resolve_path(self, image) -> Path: + image_path = Path(folder_paths.get_annotated_filepath(image)) + return image_path + + @classmethod + def IS_CHANGED(s, image): + # Need to handle this differently since it's a class method + try: + # Create a temporary instance to resolve the path + temp_instance = s() + image_path = temp_instance._resolve_path(image) + m = hashlib.sha256() + with open(image_path, 'rb') as f: + m.update(f.read()) + return m.digest().hex() + except: + return image # Fallback + + @classmethod + def VALIDATE_INPUTS(s, image): + if image is None: + return True + + try: + # Create a temporary instance to resolve the path + temp_instance = s() + image_path = temp_instance._resolve_path(image) + if not image_path.exists(): + return "Invalid image path: {}".format(image_path) + return True + except: + return "Error validating image path" + +# Server endpoints for file browsing +@PromptServer.instance.routes.get("/ib_custom_nodes/browse_directory") +async def browse_directory(request): + """Browse directories and return file listings""" + try: + path = request.query.get('path', '') + sort_method = request.query.get('sort', 'name_asc') # Default to name ascending + + if not path: + # Return some common starting points + if os.name == 'nt': # Windows + drives = [f"{d}:\\" for d in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' if os.path.exists(f"{d}:\\")] + return web.json_response({ + 'directories': drives, + 'files': [], + 'current_path': '' + }) + else: # Unix-like + path = os.path.expanduser('~') + + path = os.path.abspath(path) + + if not os.path.exists(path) or not os.path.isdir(path): + return web.json_response({'error': 'Invalid path'}, status=400) + + directories = [] + files = [] + + try: + items = [] + for item in os.listdir(path): + # Skip hidden files/folders (starting with .) + if item.startswith('.'): + continue + + item_path = os.path.join(path, item) + try: + if os.path.isdir(item_path): + item_type = 'directory' + stat = os.stat(item_path) + elif os.path.isfile(item_path): + # Check if it's an image file + ext = os.path.splitext(item)[1].lower() + if ext in ['.png', '.jpg', '.jpeg', '.bmp', '.gif', '.webp', '.tiff', '.tif']: + item_type = 'file' + stat = os.stat(item_path) + else: + continue + else: + continue + except (PermissionError, OSError): + # Skip items we can't access + continue + + items.append({ + 'name': item, + 'type': item_type, + 'path': item_path, + 'modified': stat.st_mtime + }) + + # Apply sorting + if sort_method == 'name_asc': + items.sort(key=lambda x: x['name'].lower()) + elif sort_method == 'name_desc': + items.sort(key=lambda x: x['name'].lower(), reverse=True) + elif sort_method == 'date_desc': + items.sort(key=lambda x: x['modified'], reverse=True) + elif sort_method == 'date_asc': + items.sort(key=lambda x: x['modified']) + + # Separate back into directories and files + for item in items: + if item['type'] == 'directory': + directories.append(item['name']) + else: + files.append(item['name']) + + except PermissionError: + return web.json_response({'error': 'Permission denied'}, status=403) + + parent_path = os.path.dirname(path) if path != os.path.dirname(path) else None + + return web.json_response({ + 'directories': directories, + 'files': files, + 'current_path': path, + 'parent_path': parent_path, + 'sort_method': sort_method + }) + + except Exception as e: + return web.json_response({'error': str(e)}, status=500) + + +@PromptServer.instance.routes.get("/ib_custom_nodes/get_image_preview") +async def get_image_preview(request): + """Get a preview of an image at the given path""" + try: + image_path = request.query.get('path', '') + + if not image_path or not os.path.exists(image_path): + return web.json_response({'error': 'Invalid image path'}, status=400) + + # Open and resize image for preview + img = Image.open(image_path) + img = ImageOps.exif_transpose(img) + + # Create thumbnail + max_size = (512, 512) + img.thumbnail(max_size, Image.Resampling.LANCZOS) + + # Convert to base64 + from io import BytesIO + import base64 + + buffer = BytesIO() + img.save(buffer, format='PNG') + img_str = base64.b64encode(buffer.getvalue()).decode() + + return web.json_response({ + 'preview': f'data:image/png;base64,{img_str}', + 'width': img.width, + 'height': img.height + }) + + except Exception as e: + return web.json_response({'error': str(e)}, status=500) + +@PromptServer.instance.routes.get("/ib_custom_nodes/serve_image") +async def serve_image(request): + """Serve image file directly""" + try: + image_path = request.query.get('path', '') + + if not image_path or not os.path.exists(image_path): + return web.Response(status=404, text='Image not found') + + # Serve the file directly with CORS headers + response = web.FileResponse(image_path) + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS' + response.headers['Access-Control-Allow-Headers'] = '*' + return response + + except Exception as e: + return web.Response(status=500, text=f'Error serving image: {str(e)}') \ No newline at end of file diff --git a/images/LoadImageFromPath-FileBrowser.jpg b/images/LoadImageFromPath-FileBrowser.jpg new file mode 100644 index 0000000..6a116b5 Binary files /dev/null and b/images/LoadImageFromPath-FileBrowser.jpg differ diff --git a/images/LoadImageFromPath-ImageAndPath.jpg b/images/LoadImageFromPath-ImageAndPath.jpg new file mode 100644 index 0000000..b6d0ec0 Binary files /dev/null and b/images/LoadImageFromPath-ImageAndPath.jpg differ diff --git a/images/LoadImageFromPath-Mask.jpg b/images/LoadImageFromPath-Mask.jpg new file mode 100644 index 0000000..c34112e Binary files /dev/null and b/images/LoadImageFromPath-Mask.jpg differ diff --git a/usage.jpg b/images/LoadImageFromPath.jpg similarity index 100% rename from usage.jpg rename to images/LoadImageFromPath.jpg diff --git a/pyproject.toml b/pyproject.toml index f2a77aa..9854e04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,11 @@ [project] name = "ComfyUI_Ib_CustomNodes" -version = "0.2.3" +version = "0.3.0" +authors = [ + {name = "Chaoses-Ib"}, + {name = "alFrame"}, + {name = "gibiee"}, +] description = 'Load Image From Path, PIL.Image' readme = "README.md" # requires-python = ">=3.8" diff --git a/web/ib_image_loader.js b/web/ib_image_loader.js new file mode 100644 index 0000000..dffe611 --- /dev/null +++ b/web/ib_image_loader.js @@ -0,0 +1,475 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +// Store last browsed path +let lastBrowsedPath = ''; + +// File browser dialog +class FileBrowserDialog { + constructor() { + this.currentPath = lastBrowsedPath || ''; + this.selectedFile = null; + this.callback = null; + } + + async show(callback) { + this.callback = callback; + + // Create dialog overlay + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + `; + + // Make overlay focusable + overlay.tabIndex = -1; + overlay.focus(); + + // Create dialog container + const dialog = document.createElement('div'); + dialog.style.cssText = ` + background: #2a2a2a; + border: 1px solid #555; + border-radius: 6px; + width: 85%; + max-width: 900px; + height: 75%; + display: flex; + flex-direction: column; + color: #eee; + font-family: system-ui, sans-serif; + font-size: 13px; + box-shadow: 0 10px 30px rgba(0,0,0,0.5); + `; + + // Create header with view controls + const header = document.createElement('div'); + header.style.cssText = ` + padding: 12px 15px; + border-bottom: 1px solid #444; + display: flex; + justify-content: space-between; + align-items: center; + background: #333; + `; + header.innerHTML = ` +