Files
Jared Therriault 71a89500bb Improve sorting by date in ImageDrawer
This ensures that, if available, we'll sort using the date the file was created rather than modified date. Modified will be used as a fallback.
2025-12-16 19:43:52 -08:00

400 lines
12 KiB
Python

import os
import platform
import subprocess
import json
import re
import folder_paths
from .logger import logger
import torch
import random
import numpy as np
from PIL import Image
from typing import Dict, List, Optional, Union
import mimetypes
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss and rgthree"""
def __ne__(self, __value: object) -> bool:
return False
any = AnyType("*")
VIDEO_FORMATS_DIRECTORY = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "video_formats"
)
VIDEO_FORMATS = []
# Iterate over each file in the directory
for filename in os.listdir(VIDEO_FORMATS_DIRECTORY):
filepath = os.path.join(VIDEO_FORMATS_DIRECTORY, filename)
with open(filepath, "r") as file:
# Parse the JSON content
data = json.load(file)
# Get the value of the "extension" key
extension = data.get("extension")
# Add the extension value to the set
if extension not in VIDEO_FORMATS:
VIDEO_FORMATS.append(extension)
JNODES_IMAGE_FORMAT_TYPES = [
"jpg",
"jpeg",
"jfif",
"png",
"gif",
"webp",
"apng",
"mjpeg",
] + VIDEO_FORMATS
JNODES_VAE_LIST = ["Baked VAE"] + folder_paths.get_filename_list("vae")
ACCEPTED_UPLOAD_VIDEO_EXTENSIONS = ["webm", "mp4", "mkv", "ogg"] + VIDEO_FORMATS
ACCEPTED_BROWSER_VIDEO_EXTENSIONS = [
"webm",
"mp4",
"ogg",
] # Extensions of videos that will play in most browsers
ACCEPTED_ANIMATED_IMAGE_EXTENSIONS = ["gif", "webp", "apng", "mjpeg"]
ACCEPTED_STILL_IMAGE_EXTENSIONS = ["gif", "webp", "png", "jpg", "jpeg", "jfif"]
ALL_ACCEPTED_IMAGE_EXTENSIONS = (
ACCEPTED_STILL_IMAGE_EXTENSIONS + ACCEPTED_ANIMATED_IMAGE_EXTENSIONS
)
ALL_ACCEPTED_UPLOAD_VISUAL_EXTENSIONS = (
ACCEPTED_UPLOAD_VIDEO_EXTENSIONS + ALL_ACCEPTED_IMAGE_EXTENSIONS
)
ALL_ACCEPTED_BROWSER_VISUAL_EXTENSIONS = (
ACCEPTED_BROWSER_VIDEO_EXTENSIONS + ALL_ACCEPTED_IMAGE_EXTENSIONS
)
ACCEPTED_IMAGE_AND_VIDEO_EXTENSIONS_COMPENDIUM = (
ALL_ACCEPTED_IMAGE_EXTENSIONS + ALL_ACCEPTED_UPLOAD_VISUAL_EXTENSIONS + ALL_ACCEPTED_BROWSER_VISUAL_EXTENSIONS
)
mimetypes.add_type ('image/webp', '.webp')
@staticmethod
def return_random_int(min=1, max=100000):
return random.randint(min, max)
@staticmethod
def clamp(value, min_val, max_val):
"""
Clamp the 'value' between 'min_val' and 'max_val'.
"""
return max(min(value, max_val), min_val)
@staticmethod
def map_to_range(value, input_min, input_max, output_min, output_max):
# Calculate input range
input_range = input_max - input_min
# Handle cases where input_min and input_max are reversed
if input_range == 0:
return output_min # Avoid division by zero
# Calculate normalized value within the input range
normalized_value = (value - input_min) / input_range
# Determine output range direction
if output_min <= output_max:
# Regular output mapping (output_min to output_max)
output_range = output_max - output_min
mapped_value = output_min + (normalized_value * output_range)
else:
# Inverted output mapping (output_max to output_min)
output_range = output_min - output_max
mapped_value = output_min - (normalized_value * output_range)
# Ensure mapped value is within the output range
if output_min <= output_max:
return max(min(mapped_value, output_max), output_min)
else:
return min(max(mapped_value, output_max), output_min)
def convert_relative_comfyui_path_to_full_path(relative_path="output"):
try:
path = folder_paths.get_directory_by_type(relative_path)
if path:
return path
else:
paths = folder_paths.get_folder_paths(relative_path)
if len(paths) > 0:
return paths[0]
except:
pass
return os.path.join(folder_paths.base_path, relative_path)
def replace_all(original_string, to_replace, replacement):
original_string = str(original_string)
to_replace = str(to_replace)
replacement = str(replacement)
while to_replace in original_string:
original_string = original_string.replace(to_replace, replacement)
return original_string
def replace_all_but_first_in_string(original_string, to_replace, replacement):
original_string = str(original_string)
to_replace = str(to_replace)
replacement = str(replacement)
# Find the first occurrence
first_occurrence_index = original_string.find(to_replace)
# If the substring to replace is not found, return the original string
if first_occurrence_index == -1:
return original_string
# Split the string into two parts: before and after the first occurrence
before_first = original_string[:first_occurrence_index + len(to_replace)]
after_first = original_string[first_occurrence_index + len(to_replace):]
# Replace all occurrences of to_replace in the part after the first occurrence
after_first_replaced = after_first.replace(to_replace, replacement)
# Combine the two parts
result = before_first + after_first_replaced
return result
def resolve_file_path(in_file_path):
if os.path.isabs(in_file_path):
return in_file_path
else: # Relative path
return convert_relative_comfyui_path_to_full_path(in_file_path)
def get_clean_filename(file_path):
# Get the base name of the file
base_name = os.path.basename(file_path)
# Split the base name into name and extension
name, _ = os.path.splitext(base_name)
return name
def get_leaf_directory(path):
# Ensure the path is a directory
if os.path.isdir(path):
return os.path.basename(os.path.normpath(path))
else:
# If it's a file, get the parent directory and return the basename of that
return os.path.basename(os.path.dirname(path))
def insert_before_extension(file_name, insert_string):
# Split the file name into base name and extension
base_name, ext = os.path.splitext(file_name)
# Insert the string before the extension
new_base_name = base_name + insert_string
# Reassemble the new file name with the original extension
new_file_name = new_base_name + ext
return new_file_name
def highest_common_folder(path1, path2):
# Split the paths into their components
path1_parts = path1.replace("\\", "/").split(os.path.sep)
path2_parts = path2.replace("\\", "/").split(os.path.sep)
# Find the minimum length of the two paths
min_length = min(len(path1_parts), len(path2_parts))
# Initialize the highest common folder
common_folder = ""
# Iterate over the components of both paths
for i in range(min_length):
if path1_parts[i] == path2_parts[i]:
# If the components match, add them to the common folder
common_folder = os.path.join(common_folder, path1_parts[i])
else:
# If the components don't match, stop the iteration
break
return common_folder
def make_exclusive_list(original_list, items_to_remove):
return [item for item in original_list if item not in items_to_remove]
def get_file_extension(filename):
_, extension = os.path.splitext(filename)
return extension.lower()
def get_file_extension_without_dot(filename):
_, extension = os.path.splitext(filename)
return extension[1:].lower()
def get_next_base_filename(base_directory, base_name, fill_in_missing_numbers = False):
"""
Finds the next available numbered filename in the format base_name_<number>, ignoring extensions.
"""
existing_numbers = []
pattern = re.compile(rf"^{re.escape(base_name)}_(\d+)(?:\..+)?$") # Allow optional extensions
# Scan directory for matching files
for filename in os.listdir(base_directory):
match = pattern.match(filename)
if match:
existing_numbers.append(int(match.group(1)))
if not existing_numbers:
return f"{base_name}_0"
existing_numbers.sort()
# Find the first missing number
if fill_in_missing_numbers:
for i in range(len(existing_numbers)):
if existing_numbers[i] != i:
return f"{base_name}_{i}"
# Otherwise, use the next highest number
return f"{base_name}_{existing_numbers[-1] + 1}"
def is_media_url(url):
"""Check if the URL points to an image or video."""
parsed_url = urlparse(url)
ext = os.path.splitext(parsed_url.path)[-1].lower()
return ext in ALL_ACCEPTED_BROWSER_VISUAL_EXTENSIONS
def is_webp(filename):
return get_file_extension_without_dot(filename).lower() == "webp"
def is_gif(filename):
return get_file_extension_without_dot(filename).lower() == "gif"
def is_video(filename):
mime_type, _ = mimetypes.guess_type(filename)
return mime_type and mime_type.startswith("video")
def is_image(filename):
mime_type, _ = mimetypes.guess_type(filename)
return mime_type and mime_type.startswith("image")
def is_acceptable_image_or_video_for_upload(filename):
return (
is_image(filename) or is_video(filename)
) and get_file_extension_without_dot(
filename
) in ALL_ACCEPTED_UPLOAD_VISUAL_EXTENSIONS
def is_acceptable_image_or_video_for_browser_display(filename):
return (
is_image(filename) or is_video(filename)
) and get_file_extension_without_dot(
filename
) in ALL_ACCEPTED_BROWSER_VISUAL_EXTENSIONS
def tensor2pil(image: torch.Tensor) -> List[Image.Image]:
batch_count = image.size(0) if len(image.shape) > 3 else 1
if batch_count > 1:
out = []
for i in range(batch_count):
out.extend(tensor2pil(image[i]))
return out
return [
Image.fromarray(
np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
)
]
def pil2tensor(image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor:
if isinstance(image, list):
return torch.cat([pil2tensor(img) for img in image], dim=0)
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def open_file_manager(path):
print(f"JNodes: opening file manager on {platform.system()} at path: {path}")
if not os.path.exists(path):
raise FileNotFoundError(f"The path '{path}' does not exist.")
if platform.system() == 'Windows':
# Windows uses the 'explorer' command
if os.path.isfile(path):
subprocess.run(['explorer', '/select,', path])
else:
subprocess.run(['explorer', path])
elif platform.system() == 'Darwin':
# macOS uses the 'open' command
if os.path.isfile(path):
subprocess.run(['open', '-R', path])
else:
subprocess.run(['open', path])
elif platform.system() == 'Linux':
# Linux uses the 'xdg-open' command
if os.path.isfile(path):
subprocess.run(['xdg-open', os.path.dirname(path)])
else:
subprocess.run(['xdg-open', path])
else:
raise OSError("Unsupported operating system")
def search_and_replace_from_dict(
text, replacement_dict: Dict, consider_special_characters=True
):
def replace(match_text):
return replacement_dict[match_text.group(0)]
if consider_special_characters:
return re.sub(
"|".join(map(re.escape, replacement_dict.keys())), replace, text
).strip()
else:
return re.sub(
"|".join(r"\b%s\b" % re.escape(s) for s in replacement_dict.keys()),
replace,
text,
).strip()
# Check if an object is a PIL Image
def is_pil_image(obj):
return isinstance(obj, Image.Image)
# Check if an object is a PyTorch tensor
def is_torch_tensor(obj):
return isinstance(obj, torch.Tensor)
def get_creation_time(file_path):
"""Get file creation time if available, otherwise fallback to modification time."""
try:
stat = os.stat(file_path)
return stat.st_birthtime # Works on some Linux filesystems
except AttributeError:
return os.path.getmtime(file_path)