This commit is contained in:
noembryo
2026-08-25 18:33:50 +03:00
parent 40f8fb8fe0
commit 7b623df087
5 changed files with 80 additions and 28 deletions
+2 -2
View File
@@ -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.
+58 -17
View File
@@ -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"))
+2 -2
View File
@@ -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")
+2 -2
View File
@@ -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]
+16 -5
View File
@@ -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 <img>
// 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 = () => {