Files
2026-08-15 23:06:17 -07:00

172 lines
5.3 KiB
Python

import re
from comfy_api.latest import io
import torch
from .utils import SCALE_METHODS, scale_image
class SuperPadImage(io.ComfyNode):
"""
Places an image onto an exact target canvas size and returns:
IMAGE: Final image at (target_width, target_height)
MASK: 1.0 = padded pixels, 0.0 = image pixels
Behavior:
- Image is first scaled to "contain" inside target (aspect preserved).
- Then scaled again by scale_factor (0.1-1.0).
* 1.0 = normal fill/letterbox
* <1.0 = shrink image to introduce padding on both axes
- Image is placed using horizontal/vertical shift.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="SuperPadImage",
display_name="🐧 Pad Image Scaled",
category="SuperNodes/Image",
inputs=[
io.Image.Input("image", tooltip="The input image."),
io.Int.Input(
"target_width",
default=1024,
min=1,
max=16384,
step=1,
tooltip="Final output width in pixels.",
),
io.Int.Input(
"target_height",
default=1024,
min=1,
max=16384,
step=1,
tooltip="Final output height in pixels.",
),
io.Float.Input(
"shift_horizontal",
default=0.0,
min=-1.0,
max=1.0,
step=0.01,
tooltip="Horizontal placement: -1 = far left, 0 = center, 1 = far right.",
),
io.Float.Input(
"shift_vertical",
default=0.0,
min=-1.0,
max=1.0,
step=0.01,
tooltip="Vertical placement: -1 = bottom, 0 = center, 1 = top.",
),
io.Float.Input(
"scale_factor",
default=1.0,
min=0.1,
max=1.0,
step=0.01,
tooltip="Additional scale after fitting. 1.0 = normal fill. Smaller values shrink image to create padding.",
),
io.Combo.Input(
"scale_method",
options=SCALE_METHODS,
default="nearest-exact",
tooltip="Resampling method used for resizing.",
),
io.String.Input(
"color",
default="#808080",
multiline=False,
tooltip="Padding color as hex (#RRGGBB, RRGGBB, #RGB, RGB). Invalid values default to white.",
),
],
outputs=[
io.Image.Output(display_name="IMAGE"),
io.Mask.Output(display_name="MASK"),
],
)
@classmethod
def execute(
cls,
image,
target_width,
target_height,
shift_horizontal,
shift_vertical,
scale_factor,
scale_method,
color,
) -> io.NodeOutput:
shift_horizontal = float(max(-1.0, min(1.0, shift_horizontal)))
shift_vertical = float(max(-1.0, min(1.0, shift_vertical)))
scale_factor = float(max(0.1, min(1.0, scale_factor)))
b, h, w, c = image.shape
device = image.device
dtype = image.dtype
pad_rgb = cls._parse_hex_color(color)
# Contain scale
contain_scale = min(target_width / w, target_height / h)
cw = max(1, int(round(w * contain_scale)))
ch = max(1, int(round(h * contain_scale)))
# Extra scale
fw = min(target_width, max(1, int(round(cw * scale_factor))))
fh = min(target_height, max(1, int(round(ch * scale_factor))))
resized = scale_image(image[..., :3], fw, fh, scale_method)
canvas = torch.empty(
(b, target_height, target_width, 3), device=device, dtype=dtype
)
canvas[..., 0].fill_(pad_rgb[0])
canvas[..., 1].fill_(pad_rgb[1])
canvas[..., 2].fill_(pad_rgb[2])
mask = torch.ones(
(b, target_height, target_width), device=device, dtype=dtype
)
dx = target_width - fw
dy = target_height - fh
x0 = int(round(((shift_horizontal + 1.0) * 0.5) * dx)) if dx > 0 else 0
y0 = int(round(((1.0 - shift_vertical) * 0.5) * dy)) if dy > 0 else 0
x1, y1 = x0, y0
x2, y2 = x0 + fw, y0 + fh
canvas[:, y1:y2, x1:x2] = resized
mask[:, y1:y2, x1:x2] = 0.0
return io.NodeOutput(canvas, mask)
@classmethod
def _parse_hex_color(cls, value):
if not isinstance(value, str):
return (1.0, 1.0, 1.0)
s = value.strip()
if s.startswith("#"):
s = s[1:]
if re.fullmatch(r"[0-9a-fA-F]{3}", s):
s = "".join([c * 2 for c in s])
if not re.fullmatch(r"[0-9a-fA-F]{6}", s):
return (1.0, 1.0, 1.0)
return (
int(s[0:2], 16) / 255.0,
int(s[2:4], 16) / 255.0,
int(s[4:6], 16) / 255.0,
)
NODE = [SuperPadImage]