Files
audioscavenger b6c8dbf543 WIP 1.34 pyproject.toml
modified:   LoadImageThumbnails.py
	modified:   README.md
	modified:   pyproject.toml
2025-09-01 19:49:42 -07:00

203 lines
7.0 KiB
Python

import torch
import os
import sys
import server
from aiohttp import web
import aiohttp
# import json
# import zipfile
import urllib.request
import pathlib
from urllib.parse import unquote
import hashlib
import folder_paths
from server import PromptServer
# import manager_core as core
# import cm_global
# import nodes
# import node_helpers ## node_helpers disappeared from Comfy in July 2024 and broke my node, then was re-added. Cannot depend on it.
from PIL import Image, ImageOps, ImageSequence, ImageFile, UnidentifiedImageError
import numpy as np
version = 1.34
debug = False
# debug = True
# # No need ot declare an empty class:
# class LoadImageThumbnails:
# RETURN_TYPES = ()
# OUTPUT_NODE = False
# def INPUT_TYPES():
# return None
def findFile(name, path):
for root, dirs, files in os.walk(path):
if name in files:
return os.path.join(root, name)
## node_helpers was briefly removed from Comfy in July 2024, then re-added. Mistake or not, it broke my node.
def myPillow(fn, arg):
prev_value = None
try:
x = fn(arg)
except (OSError, UnidentifiedImageError, ValueError): #PIL issues #4472 and #2445, also fixes ComfyUI issue #3416
prev_value = ImageFile.LOAD_TRUNCATED_IMAGES
ImageFile.LOAD_TRUNCATED_IMAGES = True
x = fn(arg)
finally:
if prev_value is not None:
ImageFile.LOAD_TRUNCATED_IMAGES = prev_value
return x
@PromptServer.instance.routes.get("/customnode/deleteImage")
async def deleteImage(request):
if debug: print(f"LoadImageThumbnails request: {request}")
if debug: print(f"LoadImageThumbnails request.rel_url: {request.rel_url}")
if debug: print(f"LoadImageThumbnails request.rel_url.query: {request.rel_url.query}") # <MultiDictProxy('value': '__revAnimated_v122-house.webp')>
input_dir = folder_paths.get_input_directory()
res = {'found': None, 'filename': None, 'status': 'missing'}
if "value" in request.rel_url.query:
filename = unquote(unquote(request.rel_url.query['value']))
res['filename'] = filename
if debug: print(f"LoadImageThumbnails filename: {filename}", file=sys.stderr)
found = findFile(filename, input_dir)
if not found:
if debug: print(f"LoadImageThumbnails 400 {found} not found", file=sys.stderr)
res['status'] = 'not found'
return web.json_response(res, status=400, content_type='application/json')
if debug: print(f"LoadImageThumbnails deleting {found}", file=sys.stderr)
res['found'] = found
try:
os.remove(found)
if debug: print(f"LoadImageThumbnails 201 deleted {found}", file=sys.stderr)
res['status'] = 'success'
# return web.Response(status=201)
return web.json_response(res, status=201, content_type='application/json')
except Exception as e:
print(f"LoadImageThumbnails 400 delete {found} fail: {e}", file=sys.stderr)
res['status'] = e
return web.json_response(res, status=400, content_type='application/json')
print(f"LoadImageThumbnails 400 no filename", file=sys.stderr)
return web.json_response(res, status=400, content_type='application/json')
# we override the original ComfyUI\nodes.py class LoadImage to list subfolders
class LoadImageThumbnails:
print(f"\033[92m[LoadImageThumbnails]\033[0m version: {version}\033[0m")
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
# input_dir = 'E:\\GPT\\ComfyUI'
# If we add a / at the end of each folder so Comfy.ContextMenuFilterThumbnails extension can identify them as such, they are removed by another class
# So instead, we will pass objects. It's curious how Comfy cleans up strings that have a / but let pass objects
folders = [f"{f}" for f in os.listdir(input_dir) if os.path.isdir(os.path.join(input_dir, f))]
#### folders = ['misc', 'pose', 'tmp']
if debug: print(f"LoadImage folders = {folders}", file=sys.stderr)
#### files = ['badgers.png', 'Chloe-01-1024-grey.jpg', 'Chloe-01-1024.jpg', ... ]
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
if debug: print(f"LoadImage files = {files}", file=sys.stderr)
# the js extension has no problem receiving a list of mixed strings and objects! Let's use it at our advantage haha
# This is the format we need for each folder found:
folderObjects = [{'name': F, 'files': [f for f in os.listdir(os.path.join(input_dir,F)) if os.path.isfile(os.path.join(os.path.join(input_dir,F), f))]} for F in folders]
# we put folder first, same order as Windows explorer
#### folderObjects = [{'name': 'misc', 'files': ['qr-nqzw-high-768.png', 'qr-with-error.png']}, {'name': 'pose', 'files': [...]}]
folderObjects.extend(sorted(files, key=str.upper))
if debug: print(f"LoadImage folderObjects= {folderObjects}", file=sys.stderr)
# sorted(files,key=str.upper).extend(folders)
return {
"required": {
"image": (folderObjects, {"image_upload": True})
},
}
CATEGORY = "image"
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "load_image"
def load_image(self, image):
image_path = folder_paths.get_annotated_filepath(image)
# img = node_helpers.pillow(Image.open, image_path) // cannot rely on node_helpers anymore
img = myPillow(Image.open, image_path)
output_images = []
output_masks = []
w, h = None, None
excluded_formats = ['MPO']
for i in ImageSequence.Iterator(img):
# i = node_helpers.pillow(ImageOps.exif_transpose, i) // cannot rely on node_helpers anymore
i = myPillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (output_image, output_mask)
@classmethod
def IS_CHANGED(s, image):
image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
return m.digest().hex()
@classmethod
def VALIDATE_INPUTS(s, image):
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True
NODE_CLASS_MAPPINGS = {
'LoadImage': LoadImageThumbnails,
}
NODE_DISPLAY_NAME_MAPPINGS = {
'LoadImage': 'Load Image+Thumbnails',
}