Files
malkuthro-ComfyUI-Koolook/k_easy_pattern.py
T
malkuthroandClaude Opus 4.7 9dab85c695 feat(easy_pattern): add test-pattern node + auto-restart dev-sync (#135)
* feat(easy_pattern): add test-pattern node + auto-restart dev-sync

Adds an `Easy_Pattern` node (Koolook/Testing) for solid-color frame
batches with optional per-frame index overlays — verifies sequence
merging, frame insertion, batch indexing, and video-pipeline frame
order at a glance. Color inputs use the same
`White/Black/Gray/Custom` dropdown idiom `EasyResize_Koolook` already
exposes for pad/panel colors, so "Gray" means the same 50% sRGB across
nodes.

`scripts/sync_to_dev.py` now restarts the live ComfyUI server after a
successful sync (via ComfyUI-Manager's `/manager/reboot`). Default-on
because dev-sync is only ever invoked to see new code, and Python
custom-node files load once at server start — `--no-restart` is the
escape hatch. The `RUNTIME_PATHS` manifest also picks up
`k_easy_pattern.py` so adding a new top-level node doesn't silently
break the pack at import time on the next sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync-dev): restrict restart URL to http(s) for Bandit B310

`urllib.request.urlopen` accepts arbitrary schemes including `file://`,
which Bandit B310 flags as a path-traversal vector. The script's typical
caller is the maintainer's own shell, but a typo in `--restart-url`
could surprise — and the static gate is right to flag it. Validate the
scheme is http/https before the urlopen call and annotate it so Bandit
accepts the now-truthful narrowing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:00:34 +02:00

193 lines
6.6 KiB
Python

# SPDX-License-Identifier: GPL-3.0-or-later
#
# ComfyUI-Koolook — Easy Pattern (Koolook variant)
# Copyright (C) 2026 ComfyUI-Koolook contributors (kforgelabs).
#
# This file is part of ComfyUI-Koolook, licensed under GPL-3.0-or-later.
# See the LICENSE file at the repo root for the full text.
#
# Originally drafted in an external AI-assisted chat session as a small
# standalone MIT-licensed test node, then ingested into ComfyUI-Koolook
# and relicensed under the project's GPL-3.0-or-later. No third-party
# upstream repository — this is in-house code.
"""
Easy Pattern node for ComfyUI-Koolook.
Generates a batch of solid-color images, optionally stamped with a per-frame
index number. Useful for testing sequence merging, frame insertion, batch
indexing, video pipeline order — anywhere a more flexible alternative to
KJNodes' `ImageBatchTestPattern` is needed.
"""
import os
import numpy as np
import torch
from PIL import Image, ImageDraw, ImageFont
def _hex_to_rgb(hex_str):
"""Accepts '#RRGGBB', 'RRGGBB', '#RGB', or a 3-tuple-like string. Falls back to black."""
if not isinstance(hex_str, str):
return (0, 0, 0)
s = hex_str.strip().lstrip("#")
try:
if len(s) == 3:
return tuple(int(c * 2, 16) for c in s)
if len(s) == 6:
return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16))
except ValueError:
pass
return (0, 0, 0)
# 50% Gray = sRGB (128, 128, 128); matches the `Gray` option in
# `EasyResize_Koolook`'s `pad_color_mode` and `easy_ImageBatch`'s
# `placeholder_color`, so the meaning of "Gray" is consistent across nodes.
PRESET_RGB = {
"White": (255, 255, 255),
"Black": (0, 0, 0),
"Gray": (128, 128, 128),
}
def _resolve_color(mode, custom_hex):
"""Mode wins; custom hex is only consulted when mode == 'Custom'."""
if mode in PRESET_RGB:
return PRESET_RGB[mode]
return _hex_to_rgb(custom_hex)
def _load_font(size):
"""Try a few common font paths; fall back to PIL default if none work."""
candidates = [
# Linux
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
# macOS
"/System/Library/Fonts/Helvetica.ttc",
"/Library/Fonts/Arial.ttf",
# Windows
"C:/Windows/Fonts/arialbd.ttf",
"C:/Windows/Fonts/arial.ttf",
]
for path in candidates:
if os.path.exists(path):
try:
return ImageFont.truetype(path, size)
except OSError:
continue
return ImageFont.load_default()
class EasyPattern:
"""
Configurable test-pattern batch.
Outputs: IMAGE tensor of shape [B, H, W, 3] with values in [0, 1].
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"batch_size": ("INT", {"default": 81, "min": 1, "max": 4096, "step": 1}),
"width": ("INT", {"default": 512, "min": 16, "max": 8192, "step": 8}),
"height": ("INT", {"default": 512, "min": 16, "max": 8192, "step": 8}),
"bg_color_mode": (["White", "Black", "Gray", "Custom"], {"default": "Custom"}),
"show_text": ("BOOLEAN", {"default": True}),
"text_color_mode": (["White", "Black", "Gray", "Custom"], {"default": "White"}),
"start_from": ("INT", {"default": 1, "min": 0, "max": 999999, "step": 1}),
"step": ("INT", {"default": 1, "min": -1000, "max": 1000, "step": 1}),
"font_size": ("INT", {"default": 256, "min": 8, "max": 1024, "step": 1}),
"position": (
["center", "top-left", "top-right", "bottom-left", "bottom-right"],
{"default": "center"},
),
"zero_pad": ("INT", {"default": 0, "min": 0, "max": 8, "step": 1}),
},
"optional": {
"bg_color": ("STRING", {"default": "#C71585"}),
"text_color": ("STRING", {"default": "#FFFFFF"}),
"prefix": ("STRING", {"default": "", "multiline": False}),
"suffix": ("STRING", {"default": "", "multiline": False}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "generate"
CATEGORY = "Koolook/Testing"
def generate(
self,
batch_size,
width,
height,
bg_color_mode,
show_text,
text_color_mode,
start_from,
step,
font_size,
position,
zero_pad=0,
bg_color="#C71585",
text_color="#FFFFFF",
prefix="",
suffix="",
):
bg_rgb = _resolve_color(bg_color_mode, bg_color)
fg_rgb = _resolve_color(text_color_mode, text_color)
font = _load_font(font_size) if show_text else None
frames = []
for i in range(batch_size):
img = Image.new("RGB", (width, height), bg_rgb)
if show_text:
n = start_from + i * step
num_str = f"{n:0{zero_pad}d}" if zero_pad > 0 else str(n)
text = f"{prefix}{num_str}{suffix}"
draw = ImageDraw.Draw(img)
# Measure with textbbox so it works on modern Pillow; fall back
# to the deprecated textsize on very old installs.
try:
bbox = draw.textbbox((0, 0), text, font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
bx, by = bbox[0], bbox[1]
except AttributeError:
tw, th = draw.textsize(text, font=font)
bx, by = 0, 0
pad = max(8, font_size // 8)
if position == "center":
x = (width - tw) // 2 - bx
y = (height - th) // 2 - by
elif position == "top-left":
x, y = pad - bx, pad - by
elif position == "top-right":
x, y = width - tw - pad - bx, pad - by
elif position == "bottom-left":
x, y = pad - bx, height - th - pad - by
else: # bottom-right
x, y = width - tw - pad - bx, height - th - pad - by
draw.text((x, y), text, font=font, fill=fg_rgb)
arr = np.array(img, dtype=np.float32) / 255.0
frames.append(arr)
batch = np.stack(frames, axis=0) # [B, H, W, 3]
return (torch.from_numpy(batch),)
NODE_CLASS_MAPPINGS = {
"Easy_Pattern": EasyPattern,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Easy_Pattern": "Easy Pattern (Koolook)",
}