Files
ComfyUI/custom_nodes/comfyui-image-saver/nodes_literals.py
jaidaken f09734b0ee
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Includes 30 custom nodes committed directly, 7 Civitai-exclusive
loras stored via Git LFS, and a setup script that installs all
dependencies and downloads HuggingFace-hosted models on vast.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 00:56:42 +00:00

359 lines
14 KiB
Python

from sys import float_info
from typing import Any
from nodes import MAX_RESOLUTION
import torch
import numpy as np
from PIL import Image, ImageDraw
import random
import math
class SeedGenerator:
RETURN_TYPES = ("INT",)
OUTPUT_TOOLTIPS = ("seed (INT)",)
FUNCTION = "get_seed"
CATEGORY = "ImageSaver/utils"
DESCRIPTION = "Provides seed as integer"
@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "The random seed used for creating the noise."}),
"increment": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "tooltip": "number to add to the final seed value"}),
}
}
def get_seed(self, seed: int, increment: int) -> tuple[int,]:
return (seed + increment,)
class StringLiteral:
RETURN_TYPES = ("STRING",)
OUTPUT_TOOLTIPS = ("string (STRING)",)
FUNCTION = "get_string"
CATEGORY = "ImageSaver/utils"
DESCRIPTION = "Provides a string"
@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"string": ("STRING", {"default": "", "multiline": True, "tooltip": "string"}),
}
}
def get_string(self, string: str) -> tuple[str,] :
return (string,)
class SizeLiteral:
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("size",)
OUTPUT_TOOLTIPS = ("size (INT)",)
FUNCTION = "get_int"
CATEGORY = "ImageSaver/utils"
DESCRIPTION = f"Provides integer number between 0 and {MAX_RESOLUTION} (step=8)"
@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"size": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, "tooltip": "size as integer (in steps of 8)"}),
}
}
def get_int(self, size: int) -> tuple[int,]:
return (size,)
class IntLiteral:
RETURN_TYPES = ("INT",)
OUTPUT_TOOLTIPS = ("int (INT)",)
FUNCTION = "get_int"
CATEGORY = "ImageSaver/utils"
DESCRIPTION = "Provides integer number between 0 and 1000000"
@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"int": ("INT", {"default": 0, "min": 0, "max": 1000000, "tooltip": "integer number"}),
}
}
def get_int(self, int: int) -> tuple[int,]:
return (int,)
class FloatLiteral:
RETURN_TYPES = ("FLOAT",)
OUTPUT_TOOLTIPS = ("float (FLOAT)",)
FUNCTION = "get_float"
CATEGORY = "ImageSaver/utils"
DESCRIPTION = f"Provides a floating point number between {float_info.min} and {float_info.max} (step=0.01)"
@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"float": ("FLOAT", {"default": 1.0, "min": float_info.min, "max": float_info.max, "step": 0.01, "tooltip": "floating point number"}),
}
}
def get_float(self, float: float):
return (float,)
class CfgLiteral:
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("value",)
OUTPUT_TOOLTIPS = ("cfg (FLOAT)",)
FUNCTION = "get_float"
CATEGORY = "ImageSaver/utils"
DESCRIPTION = "Provides CFG value between 0.0 and 100.0"
@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "tooltip": "CFG as a floating point number"}),
}
}
def get_float(self, cfg: float) -> tuple[float,]:
return (cfg,)
class ConditioningConcatOptional:
@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"conditioning_to": ("CONDITIONING", {"tooltip": "base conditioning to concat to (or pass through, if second is empty)"}),
},
"optional": {
"conditioning_from": ("CONDITIONING", {"tooltip": "conditioning to concat to conditioning_to, if empty, then conditioning_to is passed through unchanged"}),
}
}
RETURN_TYPES = ("CONDITIONING",)
FUNCTION = "concat"
CATEGORY = "conditioning"
def concat(self, conditioning_to, conditioning_from=None):
if conditioning_from is None:
return (conditioning_to,)
out = []
if len(conditioning_from) > 1:
print("Warning: ConditioningConcat conditioning_from contains more than 1 cond, only the first one will actually be applied to conditioning_to.")
cond_from = conditioning_from[0][0]
for i in range(len(conditioning_to)):
t1 = conditioning_to[i][0]
tw = torch.cat((t1, cond_from), 1)
n = [tw, conditioning_to[i][1].copy()]
out.append(n)
return (out,)
class RandomShapeGenerator:
"""
A ComfyUI node that generates images with random shapes.
"""
@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"width": ("INT", { "default": 512, "min": 64, "max": 4096, "step": 64, "tooltip": "Width of the generated image in pixels" }),
"height": ("INT", { "default": 512, "min": 64, "max": 4096, "step": 64, "tooltip": "Height of the generated image in pixels" }),
"bg_color": (["random", "white", "black", "red", "green", "blue", "yellow", "cyan", "magenta"], { "tooltip": "Background color preset or random" }),
"fg_color": (["random", "black", "white", "red", "green", "blue", "yellow", "cyan", "magenta"], { "tooltip": "Foreground shape color preset or random" }),
"shape_type": (["random", "circle", "oval", "triangle", "square", "rectangle", "rhombus", "pentagon", "hexagon"], { "tooltip": "Type of shape to generate or random" }),
"seed": ("INT", { "default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "Random seed for reproducible shape generation" }),
},
"optional": {
"bg_color_override": ("STRING", { "default": "", "multiline": False, "tooltip": "Override background color with hex (#AABBCC) or RGB(r, g, b) format" }),
"fg_color_override": ("STRING", { "default": "", "multiline": False, "tooltip": "Override foreground color with hex (#AABBCC) or RGB(r, g, b) format" }),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING")
RETURN_NAMES = ("image", "bg_rgb", "fg_rgb")
OUTPUT_TOOLTIPS = ("Generated image with random shape", "Background color as RGB/hex", "Foreground color as RGB/hex")
FUNCTION = "generate_shape"
CATEGORY = "image/generators"
DESCRIPTION = "Generates images with random shapes for testing and prototyping"
def __init__(self):
self.color_map = {
"white": (255, 255, 255),
"black": (0, 0, 0),
"red": (255, 0, 0),
"green": (0, 255, 0),
"blue": (0, 0, 255),
"yellow": (255, 255, 0),
"cyan": (0, 255, 255),
"magenta": (255, 0, 255),
}
def parse_rgb_string(self, rgb_str: str) -> tuple[int, int, int] | None:
"""Parse RGB string like 'RGB(123, 45, 67)' or '#AABBCC' into tuple (123, 45, 67)"""
if not rgb_str or rgb_str.strip() == "":
return None
rgb_str = rgb_str.strip()
try:
# Try hex format first (#AABBCC or AABBCC)
if rgb_str.startswith("#"):
hex_str = rgb_str[1:]
else:
hex_str = rgb_str
# Check if it's a valid hex string (6 characters)
if len(hex_str) == 6 and all(c in '0123456789ABCDEFabcdef' for c in hex_str):
r = int(hex_str[0:2], 16)
g = int(hex_str[2:4], 16)
b = int(hex_str[4:6], 16)
return (r, g, b)
# Try RGB(r, g, b) format
rgb_str_upper = rgb_str.upper()
if rgb_str_upper.startswith("RGB(") and rgb_str_upper.endswith(")"):
values = rgb_str[4:-1].split(",")
r, g, b = [int(v.strip()) for v in values]
# Validate range
if all(0 <= val <= 255 for val in [r, g, b]):
return (r, g, b)
except (ValueError, IndexError):
return None
return None
def draw_shape(self, draw: ImageDraw.ImageDraw, img_width: int, img_height: int, shape_type: str, shape_color: tuple[int, int, int]) -> None:
"""Draw a random shape on the image."""
# Random size - prefer larger sizes (40-70% of image dimensions)
size_factor = random.uniform(0.4, 0.7)
shape_width = int(img_width * size_factor)
shape_height = int(img_height * size_factor)
# Random position (ensure shape stays fully within bounds)
x = random.randint(0, max(0, img_width - shape_width))
y = random.randint(0, max(0, img_height - shape_height))
# Draw the shape based on type
if shape_type == 'circle':
# Make it a perfect circle using the minimum dimension
radius = min(shape_width, shape_height) // 2
draw.ellipse([x, y, x + radius * 2, y + radius * 2], fill=shape_color)
elif shape_type == 'oval':
draw.ellipse([x, y, x + shape_width, y + shape_height], fill=shape_color)
elif shape_type == 'square':
# Make it a perfect square
side = min(shape_width, shape_height)
draw.rectangle([x, y, x + side, y + side], fill=shape_color)
elif shape_type == 'rectangle':
draw.rectangle([x, y, x + shape_width, y + shape_height], fill=shape_color)
elif shape_type == 'triangle':
# Equilateral-ish triangle
points = [
(x + shape_width // 2, y), # top
(x, y + shape_height), # bottom left
(x + shape_width, y + shape_height) # bottom right
]
draw.polygon(points, fill=shape_color)
elif shape_type == 'rhombus':
# Diamond shape
points = [
(x + shape_width // 2, y), # top
(x + shape_width, y + shape_height // 2), # right
(x + shape_width // 2, y + shape_height), # bottom
(x, y + shape_height // 2) # left
]
draw.polygon(points, fill=shape_color)
elif shape_type == 'pentagon':
# Regular pentagon
cx, cy = x + shape_width // 2, y + shape_height // 2
radius = min(shape_width, shape_height) // 2
points = []
for i in range(5):
angle = i * 2 * math.pi / 5 - math.pi / 2
px = cx + radius * math.cos(angle)
py = cy + radius * math.sin(angle)
points.append((px, py))
draw.polygon(points, fill=shape_color)
elif shape_type == 'hexagon':
# Regular hexagon
cx, cy = x + shape_width // 2, y + shape_height // 2
radius = min(shape_width, shape_height) // 2
points = []
for i in range(6):
angle = i * 2 * math.pi / 6
px = cx + radius * math.cos(angle)
py = cy + radius * math.sin(angle)
points.append((px, py))
draw.polygon(points, fill=shape_color)
def generate_shape(self, width: int, height: int, bg_color: str, fg_color: str, shape_type: str, seed: int, bg_color_override: str = "", fg_color_override: str = "") -> tuple[torch.Tensor, str, str]:
"""Generate an image with a random shape."""
# Set random seed for reproducibility
random.seed(seed)
# Get colors from map or generate random RGB values
# Check for override first
bg_override = self.parse_rgb_string(bg_color_override)
if bg_override is not None:
bg_rgb = bg_override
elif bg_color == "random":
bg_rgb = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
else:
bg_rgb = self.color_map.get(bg_color, (255, 255, 255))
fg_override = self.parse_rgb_string(fg_color_override)
if fg_override is not None:
fg_rgb = fg_override
elif fg_color == "random":
fg_rgb = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
else:
fg_rgb = self.color_map.get(fg_color, (0, 0, 0))
# Create image
img = Image.new('RGB', (width, height), bg_rgb)
draw = ImageDraw.Draw(img)
# Select shape type
if shape_type == "random":
shapes = ['circle', 'oval', 'triangle', 'square', 'rectangle', 'rhombus', 'pentagon', 'hexagon']
selected_shape = random.choice(shapes)
else:
selected_shape = shape_type
# Draw the shape
self.draw_shape(draw, width, height, selected_shape, fg_rgb)
# Convert PIL Image to torch tensor (ComfyUI format)
# ComfyUI expects images in format [batch, height, width, channels] with values 0-1
img_array = np.array(img).astype(np.float32) / 255.0
img_tensor = torch.from_numpy(img_array)[None,]
# Format RGB values as strings for output (both formats)
bg_hex = f"#{bg_rgb[0]:02X}{bg_rgb[1]:02X}{bg_rgb[2]:02X}"
fg_hex = f"#{fg_rgb[0]:02X}{fg_rgb[1]:02X}{fg_rgb[2]:02X}"
bg_rgb_str = f"RGB({bg_rgb[0]}, {bg_rgb[1]}, {bg_rgb[2]}) / {bg_hex}"
fg_rgb_str = f"RGB({fg_rgb[0]}, {fg_rgb[1]}, {fg_rgb[2]}) / {fg_hex}"
return (img_tensor, bg_rgb_str, fg_rgb_str)