Files
rubz c6f6914488 fix: batch collapse, memory blowup, resize rounding, and 8 more Should-Fix issues from code review
- Reject use_original_name=False + use_counter=False (all files collapsed to one output name)
- Add preview_limit to cap in-memory preview tensors (was holding entire batch as float32)
- Recover from per-file failures instead of aborting the whole batch
- Fix _calc_new_size overshoot past longest_side and off-by-one truncation
- Add upscale_if_smaller to SmartBatchResize to match SmartResize
- Clip pixel values before uint8 cast (was wrapping on overflow)
- Support CUDA tensors and RGBA/grayscale images in SmartResize
- Handle empty batches; derive width/height from the actual output tensor
- Require POST + loopback/origin check on the browse_folder endpoint (CSRF)
- Add subprocess timeout to the folder picker dialog
- Isolate the browse_folder route registration so import failures don't take down all nodes
- Validate folder_custom against path traversal and self-overwrite
- Replace IS_CHANGED=NaN with a folder-listing hash so unrelated graph runs can still cache
2026-07-27 18:42:54 +02:00

107 lines
4.1 KiB
Python

from PIL import Image as PILImage
import numpy as np
import torch
INTERP_MAP = {
"lanczos": PILImage.LANCZOS,
"bicubic": PILImage.BICUBIC,
"bilinear": PILImage.BILINEAR,
"nearest": PILImage.NEAREST,
}
def _calc_new_size(w, h, longest_side, multiple_of, upscale_if_smaller):
longest = max(w, h)
scaled = longest > longest_side or upscale_if_smaller
if not scaled:
new_w, new_h = w, h
else:
scale = longest_side / longest
new_w = round(w * scale)
new_h = round(h * scale)
if multiple_of > 1:
raw_w, raw_h = new_w, new_h
new_w = max(multiple_of, round(raw_w / multiple_of) * multiple_of)
new_h = max(multiple_of, round(raw_h / multiple_of) * multiple_of)
if scaled and max(new_w, new_h) > longest_side:
# Rounding to the nearest multiple pushed the longest side past the
# target — floor that side to the multiple instead and rescale the
# other side to match, so the requested cap is always honored.
if raw_w >= raw_h:
floored = max(multiple_of, (raw_w // multiple_of) * multiple_of)
other_scale = floored / raw_w
new_w = floored
new_h = max(multiple_of, round((raw_h * other_scale) / multiple_of) * multiple_of)
else:
floored = max(multiple_of, (raw_h // multiple_of) * multiple_of)
other_scale = floored / raw_h
new_h = floored
new_w = max(multiple_of, round((raw_w * other_scale) / multiple_of) * multiple_of)
return new_w, new_h
class SmartResize:
"""
Resize IMAGE by longest side. Lanczos included. Multiple_of snapping.
Works with single images and batches. IMAGE in → IMAGE out.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"longest_side": ("INT", {
"default": 1536, "min": 64, "max": 8192, "step": 8,
"tooltip": "Longest side target in pixels. Aspect ratio is always preserved."
}),
"multiple_of": ("INT", {
"default": 8, "min": 1, "max": 64, "step": 1,
"tooltip": "Snap both dimensions to a multiple of this value. Use 8 for SD/Flux."
}),
"interpolation": (["lanczos", "bicubic", "bilinear", "nearest"], {
"tooltip": "Resampling method. Lanczos is sharpest for downscaling."
}),
"upscale_if_smaller": ("BOOLEAN", {
"default": True,
"tooltip": "Upscale images that are smaller than the target longest side. Turn off to only ever downscale, never upscale."
}),
}
}
RETURN_TYPES = ("IMAGE", "INT", "INT")
RETURN_NAMES = ("image", "width", "height")
FUNCTION = "run"
CATEGORY = "CraftKit"
def run(self, image, longest_side, multiple_of, interpolation, upscale_if_smaller):
if image.shape[0] == 0:
return (image, 0, 0)
interp = INTERP_MAP[interpolation]
results = []
for i in range(image.shape[0]):
frame = image[i] # [H, W, C]
h, w = frame.shape[0], frame.shape[1]
new_w, new_h = _calc_new_size(w, h, longest_side, multiple_of, upscale_if_smaller)
if new_w == w and new_h == h:
results.append(frame)
else:
channels = frame.shape[-1]
pil_mode = {1: "L", 3: "RGB", 4: "RGBA"}.get(channels, "RGB")
arr = np.clip(frame.detach().cpu().numpy() * 255, 0, 255).astype("uint8")
pil = PILImage.fromarray(arr, mode=pil_mode)
pil = pil.resize((new_w, new_h), interp)
arr = np.array(pil).astype("float32") / 255.0
results.append(torch.from_numpy(arr))
stacked = torch.stack(results, dim=0)
return (stacked, stacked.shape[2], stacked.shape[1])
NODE_CLASS_MAPPINGS = {"SmartResize": SmartResize}
NODE_DISPLAY_NAME_MAPPINGS = {"SmartResize": "Smart Resize 📐"}