1.6.1
This commit is contained in:
@@ -55,7 +55,7 @@ For the custom text integration, there is a variable (can be specified by the us
|
||||
---
|
||||
## Load Image (from path)
|
||||

|
||||
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
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user