Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "basic_data_handling"
version = "0.4.6"
version = "0.4.7"
description = """Basic Python functions for manipulating data that every programmer is used to, lightweight with no additional dependencies.

Supported data types:
Expand Down
12 changes: 9 additions & 3 deletions src/basic_data_handling/control_flow_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ class ExecutionOrder(ComfyNodeABC):
the execution order of nodes in the workflow. You only need to chain this node
with the other execution order nodes in the desired order and add any
output of the nodes you want to force execution order on.

This node also passes through any input connected to "any node output" as
its second output.
"""
@classmethod
def INPUT_TYPES(cls):
Expand All @@ -318,13 +321,16 @@ def INPUT_TYPES(cls):
}
}

RETURN_TYPES = ("E/O",)
RETURN_TYPES = ("E/O", IO.ANY)
RETURN_NAMES = ("E/O", "passthrough")
FUNCTION = "execution_order"
CATEGORY = "Basic/flow control"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "execute"

def execute(self, **kwargs) -> tuple[Any]:
return (None,)
def execute(self, **kwargs: list[Any]) -> tuple[None, Any]:
any_node_output = kwargs.get('any node output', [])
return (None, any_node_output)


NODE_CLASS_MAPPINGS = {
Expand Down
145 changes: 115 additions & 30 deletions src/basic_data_handling/path_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ def load_image_helper(path: str):
pass

if not os.path.exists(path):
raise FileNotFoundError(f"Basic data handling: Image file not found: {path}")
return None

# Open and process the image
img = Image.open(path)
img = ImageOps.exif_transpose(img)

return img
try:
img = Image.open(path)
img = ImageOps.exif_transpose(img)
return img
except Exception:
return None


def extract_mask_from_alpha(img):
Expand Down Expand Up @@ -675,19 +677,33 @@ def INPUT_TYPES(cls):
},
}

RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("text",)
RETURN_TYPES = (IO.STRING, IO.BOOLEAN)
RETURN_NAMES = ("text", "exists")
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "load_text"

@classmethod
def IS_CHANGED(cls, path):
try:
if os.path.exists(path):
return os.path.getmtime(path)
except Exception:
pass
return float("NaN") # Return NaN if file doesn't exist or can't access modification time

def load_text(self, path: str):
if not os.path.exists(path):
raise FileNotFoundError(f"Basic data handling: String file not found: {path}")
exists = os.path.exists(path)

with open(path, "r", encoding="utf-8") as f:
text = f.read()
return (text,)
if not exists:
return ("", False)

try:
with open(path, "r", encoding="utf-8") as f:
text = f.read()
return (text, True)
except Exception:
return ("", False)


class PathLoadImageRGB(ComfyNodeABC):
Expand All @@ -705,26 +721,40 @@ def INPUT_TYPES(cls):
},
}

RETURN_TYPES = (IO.IMAGE,)
RETURN_NAMES = ("image",)
RETURN_TYPES = (IO.IMAGE, IO.BOOLEAN)
RETURN_NAMES = ("image", "exists")
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "load_image_rgb"

@classmethod
def IS_CHANGED(cls, path):
try:
if os.path.exists(path):
return os.path.getmtime(path)
except Exception:
pass
return float("NaN") # Return NaN if file doesn't exist or can't access modification time

def load_image_rgb(self, path: str):
import numpy as np
import torch

img = load_image_helper(path)

if img is None:
# Create an empty 1x1 image
empty_tensor = torch.zeros((1, 1, 1, 3), dtype=torch.float32)
return (empty_tensor, False)

# Convert to RGB (removing alpha if present)
img_rgb = img.convert("RGB")

# Convert to tensor format expected by ComfyUI
image_tensor = np.array(img_rgb).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image_tensor)[None,]

return (image_tensor,)
return (image_tensor, True)


class PathLoadImageRGBA(ComfyNodeABC):
Expand All @@ -743,18 +773,33 @@ def INPUT_TYPES(cls):
},
}

RETURN_TYPES = (IO.IMAGE, IO.MASK)
RETURN_NAMES = ("image", "mask")
RETURN_TYPES = (IO.IMAGE, IO.MASK, IO.BOOLEAN)
RETURN_NAMES = ("image", "mask", "exists")
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "load_image_rgba"

@classmethod
def IS_CHANGED(cls, path):
try:
if os.path.exists(path):
return os.path.getmtime(path)
except Exception:
pass
return float("NaN") # Return NaN if file doesn't exist or can't access modification time

def load_image_rgba(self, path: str):
import numpy as np
import torch

img = load_image_helper(path)

if img is None:
# Create empty 1x1 image and mask
empty_image = torch.zeros((1, 1, 1, 3), dtype=torch.float32)
empty_mask = torch.zeros((1, 1, 1), dtype=torch.float32)
return (empty_image, empty_mask, False)

# Convert to RGB for the image
img_rgb = img.convert("RGB")

Expand All @@ -765,7 +810,7 @@ def load_image_rgba(self, path: str):
# Extract alpha channel as mask
mask_tensor = extract_mask_from_alpha(img)

return (image_tensor, mask_tensor)
return (image_tensor, mask_tensor, True)


class PathLoadMaskFromAlpha(ComfyNodeABC):
Expand All @@ -784,16 +829,33 @@ def INPUT_TYPES(cls):
},
}

RETURN_TYPES = (IO.MASK,)
RETURN_NAMES = ("mask",)
RETURN_TYPES = (IO.MASK, IO.BOOLEAN)
RETURN_NAMES = ("mask", "exists")
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "load_mask_from_alpha"

@classmethod
def IS_CHANGED(cls, path):
try:
if os.path.exists(path):
return os.path.getmtime(path)
except Exception:
pass
return float("NaN") # Return NaN if file doesn't exist or can't access modification time

def load_mask_from_alpha(self, path: str):
import torch

img = load_image_helper(path)

if img is None:
# Return empty 1x1 mask
empty_mask = torch.zeros((1, 1, 1), dtype=torch.float32)
return (empty_mask, False)

mask_tensor = extract_mask_from_alpha(img)
return (mask_tensor,)
return (mask_tensor, True)


class PathLoadMaskFromGreyscale(ComfyNodeABC):
Expand All @@ -815,21 +877,38 @@ def INPUT_TYPES(cls):
},
}

RETURN_TYPES = (IO.MASK,)
RETURN_NAMES = ("mask",)
RETURN_TYPES = (IO.MASK, IO.BOOLEAN)
RETURN_NAMES = ("mask", "exists")
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "load_mask_from_greyscale"

@classmethod
def IS_CHANGED(cls, path):
try:
if os.path.exists(path):
return os.path.getmtime(path)
except Exception:
pass
return float("NaN") # Return NaN if file doesn't exist or can't access modification time

def load_mask_from_greyscale(self, path: str, invert: bool = False):
import torch

img = load_image_helper(path)

if img is None:
# Return empty 1x1 mask
empty_mask = torch.zeros((1, 1, 1), dtype=torch.float32)
return (empty_mask, False)

mask_tensor = extract_mask_from_greyscale(img)

# Optionally invert the mask (1.0 - mask)
if invert:
mask_tensor = 1.0 - mask_tensor

return (mask_tensor,)
return (mask_tensor, True)


class PathSaveStringFile(ComfyNodeABC):
Expand All @@ -852,15 +931,16 @@ def INPUT_TYPES(cls):
}
}

RETURN_TYPES = (IO.BOOLEAN)
RETURN_NAMES = ("success")
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("success",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "save_text"
OUTPUT_NODE = True

def save_text(self, text: str, path: str, create_dirs: bool = True, encoding: str = "utf-8"):
if not path:
print("Basic data handling: Save failed - no path specified")
return (False,)

try:
Expand All @@ -872,6 +952,7 @@ def save_text(self, text: str, path: str, create_dirs: bool = True, encoding: st
with open(path, "w", encoding=encoding) as f:
f.write(text)

print(f"Basic data handling: Successfully saved text to {path}")
return (True,)
except Exception as e:
print(f"Basic data handling: Error saving text file: {e}")
Expand Down Expand Up @@ -899,15 +980,16 @@ def INPUT_TYPES(cls):
}
}

RETURN_TYPES = (IO.BOOLEAN)
RETURN_NAMES = ("success")
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("success",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "save_image"
OUTPUT_NODE = True

def save_image(self, images, path: str, format: str = "png", quality: int = 95, create_dirs: bool = True):
if not path:
print("Basic data handling: Save failed - no path specified")
return (False,)

# If the path doesn't have an extension or it doesn't match the format, add it
Expand Down Expand Up @@ -957,6 +1039,7 @@ def save_image(self, images, path: str, format: str = "png", quality: int = 95,
else:
pil_img.save(path, format=format.upper())

print(f"Basic data handling: Successfully saved image to {path}")
return (True,)
except Exception as e:
print(f"Basic data handling: Error saving image: {e}")
Expand Down Expand Up @@ -987,8 +1070,8 @@ def INPUT_TYPES(cls):
}
}

RETURN_TYPES = (IO.BOOLEAN)
RETURN_NAMES = ("success")
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("success",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "save_image_with_mask"
Expand All @@ -998,6 +1081,7 @@ def save_image_with_mask(self, images, mask, path: str, format: str = "png",
quality: int = 95, invert_mask: bool = False,
create_dirs: bool = True):
if not path:
print("Basic data handling: Save failed - no path specified")
return (False,)

# Check format compatibility - needs to support alpha channel
Expand Down Expand Up @@ -1065,6 +1149,7 @@ def save_image_with_mask(self, images, mask, path: str, format: str = "png",
else:
pil_img_rgba.save(path, format=format.upper())

print(f"Basic data handling: Successfully saved image with mask to {path}")
return (True,)
except Exception as e:
print(f"Basic data handling: Error saving image with mask: {e}")
Expand Down
Loading