From 7b623df087278a111a1fa26b08cfcae6b02e29a3 Mon Sep 17 00:00:00 2001 From: noembryo Date: Tue, 25 Aug 2026 18:33:50 +0300 Subject: [PATCH] 1.6.1 --- README.md | 4 +- load_image_from_path.py | 75 ++++++++++++++++++++++++++-------- nodes.py | 4 +- pyproject.toml | 4 +- web/js/load_image_from_path.js | 21 +++++++--- 5 files changed, 80 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 07e042f..231fc77 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ For the custom text integration, there is a variable (can be specified by the us --- ## Load Image (from path) ![LoadImageFromPath](https://raw.githubusercontent.com/noembryo/ComfyUI-noEmbryo/master/stuff/LoadImageFromPath.jpg) -Load an image from **any path on your computer**. Paste an absolute path, use an annotated path (`input/file.png`), or click the **Browse** dialog to pick a file from your drives. +Load an image from **any path on your computer or a URL**. Paste an absolute path or a link to an image, use an annotated path (`input/file.png`), or click the **Browse** dialog to pick a file from your drives. The file is read from its **original location**; it is **not** copied into ComfyUI’s `input` folder. Use a selection rectangle at the preview to crop it. Limit (downscale) the output's size in megapixels or pixels. @@ -78,7 +78,7 @@ Outputs match the stock Load Image node: **IMAGE**, **MASK** (from the alpha cha - **Controls** - - **image**: Paste an absolute path, (or a relative one with a prefix input/, or output/, or temp/), to an image file. + - **image**: Paste an absolute path, (or a relative one with a prefix input/, or output/, or temp/), or a URL to an image file. - **max_megapixels**: Cap the output (crop, or full image if uncropped) to this many megapixels, downscaling only if it's bigger. Smaller images are left untouched. 1.0 = 1024x1024 px. 0 disables the cap. - **Browse...**: to open an image file from your drives. diff --git a/load_image_from_path.py b/load_image_from_path.py index b3ca87e..c6efc01 100644 --- a/load_image_from_path.py +++ b/load_image_from_path.py @@ -1,9 +1,11 @@ import hashlib -from pathlib import Path -from typing import Iterable +import io import os import json import shutil +from pathlib import Path +from typing import Iterable +from urllib.request import urlopen, Request from PIL import (Image, ImageOps, ImageSequence, ImageFile, UnidentifiedImageError, ) import numpy as np @@ -14,6 +16,18 @@ from aiohttp import web from server import PromptServer +def _is_url(s) -> bool: + """Return True if s looks like an HTTP(S) URL.""" + return isinstance(s, str) and (s.startswith("http://") or s.startswith("https://")) + + +def _fetch_url_bytes(url: str, timeout: float = 30.0) -> bytes: + """Download the bytes of a URL. Raises on any network/HTTP problem.""" + req = Request(url, headers={"User-Agent": "ComfyUI-noEmbryo/1.0"}) + with urlopen(req, timeout=timeout) as resp: + return resp.read() + + def _pillow(fn, arg): prev_value = None try: @@ -174,9 +188,11 @@ class LoadImageFromPath: @staticmethod def load_image(image): - image_path = LoadImageFromPath._resolve_path(image) - - i = _pillow(Image.open, image_path) + if _is_url(image): + i = _pillow(Image.open, io.BytesIO(_fetch_url_bytes(image))) + else: + image_path = LoadImageFromPath._resolve_path(image) + i = _pillow(Image.open, image_path) image = [] mask = [] @@ -200,6 +216,16 @@ class LoadImageFromPath: @classmethod def IS_CHANGED(cls, image): + if _is_url(image): + # URLs: hash the fetched content to detect remote changes. If the + # network fails (transient blip etc.), fall back to hashing the URL + # string so a re-run isn't triggered spuriously. + try: + data = _fetch_url_bytes(image) + except Exception: + return hashlib.sha256(image.encode("utf-8")).hexdigest() + return hashlib.sha256(data).hexdigest() + image_path = LoadImageFromPath._resolve_path(image) m = hashlib.sha256() with open(image_path, 'rb') as f: @@ -210,6 +236,8 @@ class LoadImageFromPath: def VALIDATE_INPUTS(cls, image): if image is None: return True + if _is_url(image): + return True # URLs skip the filesystem checks try: image_path = LoadImageFromPath._resolve_path(image) except ValueError as e: @@ -231,7 +259,7 @@ class LoadImageFromPathEnhanced(LoadImageFromPath): def INPUT_TYPES(cls): return {"required": {"image": ("STRING", {"default": "", "tooltip": 'Paste an absolute path, (or a relative one with a prefix input/) ' - 'to an image file. Or use "Browse" to pick a file.', }), + 'to an image file, or a URL of an image. Or use "Browse" to pick a file.', }), "crop": ("STRING", {"default": "", "tooltip": "Managed by the crop editor on the node" @@ -261,7 +289,7 @@ class LoadImageFromPathEnhanced(LoadImageFromPath): RETURN_NAMES = ("IMAGE", "MASK", "path") FUNCTION = "load_image_enhanced" DESCRIPTION = ( - " Load an image from any path (paste or Browse). Drag on the preview to" + " Load an image from any path (paste or Browse) or URL. Drag on the preview to" " crop; drag inside to move; drag corners to resize; click outside the" " selection to clear. With no crop drawn, the full image is output.\n" " If max_megapixels is greater than 0, the output (crop or full image)" @@ -278,8 +306,8 @@ class LoadImageFromPathEnhanced(LoadImageFromPath): width = 0 if width is None else int(width) height = 0 if height is None else int(height) - # Get the full path - image_path = LoadImageFromPath._resolve_path(image) + # URLs don't map to a local filesystem path — resolve only real paths. + image_path = None if _is_url(image) else LoadImageFromPath._resolve_path(image) # Call the parent class's load_image method image_tensor, mask = super().load_image(image) @@ -336,19 +364,32 @@ class LoadImageFromPathEnhanced(LoadImageFromPath): 1).clamp(0.0, 1.0) # Register this image in our cache for mask editor support - filename = os.path.basename(str(image_path)) - _image_path_cache[filename] = str(image_path) - _image_path_cache[str(image_path)] = str(image_path) + # (local filesystem paths only — skip URLs). + if image_path is not None: + filename = os.path.basename(str(image_path)) + _image_path_cache[filename] = str(image_path) + _image_path_cache[str(image_path)] = str(image_path) - # Return image, mask, AND the original input path string + # Return image, mask, AND the original input string (path or URL) return image_tensor, mask, image @classmethod def IS_CHANGED(cls, image, crop="", max_megapixels=0.0, width=0, height=0): - image_path = LoadImageFromPath._resolve_path(image) - m = hashlib.sha256() - with open(image_path, 'rb') as f: - m.update(f.read()) + if _is_url(image): + # URLs: hash the fetched content to detect remote changes, falling + # back to a URL-string hash if the network fails (transient blip). + try: + base = hashlib.sha256(_fetch_url_bytes(image)).digest() + except Exception: + base = hashlib.sha256(image.encode("utf-8")).digest() + else: + image_path = LoadImageFromPath._resolve_path(image) + m = hashlib.sha256() + with open(image_path, 'rb') as f: + m.update(f.read()) + base = m.digest() + + m = hashlib.sha256(base) m.update(str(crop).encode("utf-8")) m.update(str(max_megapixels).encode("utf-8")) m.update(str(width).encode("utf-8")) diff --git a/nodes.py b/nodes.py index 0b8b394..244e332 100644 --- a/nodes.py +++ b/nodes.py @@ -7,14 +7,14 @@ from .load_image_from_path import LoadImageFromPathEnhanced from .stitcher import H3MotionContextClipStitcher MANIFEST = {"name": "noEmbryo Nodes", - "version": (1, 6, 0), + "version": (1, 6, 1), "author": "noEmbryo", "project": "https://github.com/noembryo/ComfyUI-noEmbryo", "description": "Nodes for ComfyUI", "license": "MIT", } __author__ = "noEmbryo" -__version__ = "1.6.0" +__version__ = "1.6.1" LISTS_PATH = join(dirname(realpath(__file__)), "TermLists") diff --git a/pyproject.toml b/pyproject.toml index 28790d5..790ef08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "comfyui-noembryo" description = """ A collection of nodes for ComyUI. \n -* "Load Image (from path)" is a node lets you load an image from any path in your computer, and a selection rectangle at the preview to crop it. It can also +* "Load Image (from path)" is a node lets you load an image from any path in your computer or a URL, and use a selection rectangle at the preview to crop it. It can also limit the output's size in megapixels. \n * "H3 Motion Context Clip Stitcher" is a node that concatenates H3 Motion Context clip archives. \n * "JsonPromptLoader" is a node that enhances a propt using a .json file with `item:prompt` pairs. \n @@ -11,7 +11,7 @@ limit the output's size in megapixels. \n * "PromptTermList (1-6)" nodes to help with the creation of Prompts. \n * "Auto Save Workflow" can save the current workflow to a .json file automatically. \n """ -version = "1.6.0" +version = "1.6.1" license = {text = "MIT License"} [project.urls] diff --git a/web/js/load_image_from_path.js b/web/js/load_image_from_path.js index 41684ac..312cec8 100644 --- a/web/js/load_image_from_path.js +++ b/web/js/load_image_from_path.js @@ -22,8 +22,13 @@ function dirnameOf(pathStr) { // File browser dialog class FileBrowserDialog { constructor(initialPath) { - // Prefer the path that is already in the node, then fall back to last browsed - const fromWidget = dirnameOf(initialPath); + // Prefer the path that is already in the node, then fall back to last browsed. + // A URL value (http/https) isn't a filesystem path — treating its host/path as a + // directory would produce an "Invalid path" error, so skip it and use the last + // browsed path (or the drive list) instead. + const fromWidget = /^https?:\/\//i.test(initialPath || '') + ? '' + : dirnameOf(initialPath); this.currentPath = fromWidget || lastBrowsedPath || ''; this.initialFilePath = initialPath || null; // ← keep the full path this.selectedFile = null; @@ -1052,10 +1057,16 @@ app.registerExtension({ editorWidget.triggerDraw?.(); return; } - const filename = path.split(/[\\/]/).pop(); + // URLs are loaded directly by the browser (cross-origin + // display + canvas draw works fine — no read-back is used, so + // there's no canvas taint issue). Local paths go through our + // serve proxy endpoint. const url = - `/noembryo/serve_image?path=${encodeURIComponent(path)}` + - `&filename=${encodeURIComponent(filename)}&t=${Date.now()}`; + /^https?:\/\//i.test(path) + ? path + : `/noembryo/serve_image?path=${encodeURIComponent(path)}` + + `&filename=${encodeURIComponent(path.split(/[\\/]/).pop())}` + + `&t=${Date.now()}`; const img = new Image(); img.onload = () => {