Files
2026-04-17 01:11:22 +03:00

261 lines
8.0 KiB
Python

# -----------------------------------------------------------------------
#
# !\file CNP_random_resolution.py
# !\brief Generate random image resolutions based on a base unit and various modes.
# !\author Colin J.D. Stewart
#
# Copyright (c) 2026 Colin J.D. Stewart. All rights reserved.
#
# -----------------------------------------------------------------------
import random
class CNPRandomResolutionNode:
"""
Generate image width/height from multiples of a base unit.
Supports fixed, fully random, and ratio-based random modes.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mode": (["fixed", "random", "random_ratio"], {
"default": "random_ratio"
}),
"base_unit": ("INT", {
"default": 512,
"min": 64,
"max": 4096,
"step": 64
}),
"min_steps": ("INT", {
"default": 1,
"min": 1,
"max": 32,
"step": 1
}),
"max_steps": ("INT", {
"default": 4,
"min": 1,
"max": 32,
"step": 1
}),
"fixed_width_steps": ("INT", {
"default": 2,
"min": 1,
"max": 32,
"step": 1
}),
"fixed_height_steps": ("INT", {
"default": 2,
"min": 1,
"max": 32,
"step": 1
}),
"orientation": (["any", "square", "portrait", "landscape"], {
"default": "any"
}),
"ratio_mode": (["free", "common", "vertical_common", "horizontal_common"], {
"default": "common"
}),
"randomise_orientation": ("BOOLEAN", {
"default": True
}),
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff
}),
}
}
RETURN_TYPES = ("INT", "INT", "STRING")
RETURN_NAMES = ("width", "height", "resolution")
FUNCTION = "generate"
CATEGORY = "ComfyNodesPlus/Utils"
def generate(
self,
mode,
base_unit,
min_steps,
max_steps,
fixed_width_steps,
fixed_height_steps,
orientation,
ratio_mode,
randomise_orientation,
seed
):
if min_steps > max_steps:
min_steps, max_steps = max_steps, min_steps
rng = random.Random(seed)
if mode == "fixed":
width = fixed_width_steps * base_unit
height = fixed_height_steps * base_unit
width, height = self._apply_orientation_hint(
width,
height,
orientation,
rng,
randomise_orientation
)
return (width, height, f"{width} x {height}")
if mode == "random":
width_steps = rng.randint(min_steps, max_steps)
height_steps = rng.randint(min_steps, max_steps)
width = width_steps * base_unit
height = height_steps * base_unit
width, height = self._apply_orientation_hint(
width,
height,
orientation,
rng,
randomise_orientation
)
return (width, height, f"{width} x {height}")
# mode == "random_ratio"
width, height = self._generate_ratio_size(
rng=rng,
base_unit=base_unit,
min_steps=min_steps,
max_steps=max_steps,
orientation=orientation,
ratio_mode=ratio_mode,
randomise_orientation=randomise_orientation
)
return (width, height, f"{width} x {height}")
def _apply_orientation_hint(self, width, height, orientation, rng, randomise_orientation):
if orientation == "square":
side = min(width, height)
return side, side
if orientation == "portrait":
if width > height:
return height, width
return width, height
if orientation == "landscape":
if height > width:
return height, width
return width, height
# any
if randomise_orientation and width != height and rng.choice([True, False]):
return height, width
return width, height
def _generate_ratio_size(
self,
rng,
base_unit,
min_steps,
max_steps,
orientation,
ratio_mode,
randomise_orientation
):
presets = self._build_presets(
base_unit=base_unit,
min_steps=min_steps,
max_steps=max_steps,
orientation=orientation,
ratio_mode=ratio_mode,
randomise_orientation=randomise_orientation
)
if not presets:
# Safe fallback
steps = min_steps
width = steps * base_unit
height = steps * base_unit
return width, height
width, height = rng.choice(presets)
return width, height
def _build_presets(
self,
base_unit,
min_steps,
max_steps,
orientation,
ratio_mode,
randomise_orientation
):
presets = set()
# Always allow square unless specifically filtered to only vertical/horizontal common
if ratio_mode in ("free", "common"):
for steps in range(min_steps, max_steps + 1):
side = steps * base_unit
presets.add((side, side))
if ratio_mode == "free":
# Any combination within range
for w_steps in range(min_steps, max_steps + 1):
for h_steps in range(min_steps, max_steps + 1):
presets.add((w_steps * base_unit, h_steps * base_unit))
else:
# Common sensible shapes using only integer step pairs
common_pairs = [
(1, 2), # 512 x 1024
(2, 3), # 1024 x 1536
(2, 4), # 1024 x 2048
(3, 4), # 1536 x 2048
(2, 1), # 1024 x 512
(3, 2), # 1536 x 1024
(4, 2), # 2048 x 1024
(4, 3), # 2048 x 1536
]
filtered_pairs = []
for w_steps, h_steps in common_pairs:
if w_steps < min_steps or w_steps > max_steps:
continue
if h_steps < min_steps or h_steps > max_steps:
continue
if ratio_mode == "vertical_common" and not (h_steps > w_steps):
continue
if ratio_mode == "horizontal_common" and not (w_steps > h_steps):
continue
filtered_pairs.append((w_steps, h_steps))
for w_steps, h_steps in filtered_pairs:
presets.add((w_steps * base_unit, h_steps * base_unit))
# Orientation filter
filtered = set()
for width, height in presets:
if orientation == "square" and width != height:
continue
if orientation == "portrait" and not (height > width):
continue
if orientation == "landscape" and not (width > height):
continue
filtered.add((width, height))
presets = filtered
# If orientation is any and randomise_orientation is enabled,
# mirror non-square sizes so both vertical/horizontal can appear.
if orientation == "any" and randomise_orientation:
mirrored = set(presets)
for width, height in presets:
mirrored.add((height, width))
presets = mirrored
return sorted(presets)