181 lines
6.4 KiB
Python
181 lines
6.4 KiB
Python
import os
|
|
from pathlib import Path
|
|
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,
|
|
}
|
|
|
|
SUPPORTED_EXT = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff", ".tif"}
|
|
|
|
|
|
def _calc_new_size(w, h, max_pixels, multiple_of):
|
|
longest = max(w, h)
|
|
if longest <= max_pixels:
|
|
new_w, new_h = w, h
|
|
else:
|
|
scale = max_pixels / longest
|
|
new_w = int(w * scale)
|
|
new_h = int(h * scale)
|
|
if multiple_of > 1:
|
|
new_w = max(multiple_of, round(new_w / multiple_of) * multiple_of)
|
|
new_h = max(multiple_of, round(new_h / multiple_of) * multiple_of)
|
|
return new_w, new_h
|
|
|
|
|
|
class SmartBatchResize:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
# INPUT
|
|
"input_folder": ("STRING", {
|
|
"default": "",
|
|
"multiline": False,
|
|
"tooltip": "Folder containing images to resize."
|
|
}),
|
|
|
|
# RESIZE SETTINGS
|
|
"max_pixels": ("INT", {
|
|
"default": 1024, "min": 64, "max": 8192, "step": 64,
|
|
"tooltip": "Longest side target in pixels. Aspect ratio is always preserved."
|
|
}),
|
|
"multiple_of": ("INT", {
|
|
"default": 8, "min": 1, "max": 128, "step": 1,
|
|
"tooltip": "Snap both dimensions to a multiple of this value. Use 8 for SD/Flux."
|
|
}),
|
|
"interpolation": (["lanczos", "bicubic", "bilinear", "nearest"], {
|
|
"default": "lanczos"
|
|
}),
|
|
|
|
# OUTPUT NAMING
|
|
"suffix_resolution": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Append resolution to filename. E.g. image_1024.png"
|
|
}),
|
|
"suffix_custom": ("STRING", {
|
|
"default": "",
|
|
"multiline": False,
|
|
"tooltip": "Custom text to add before resolution. Only used when suffix_resolution is on."
|
|
}),
|
|
|
|
# OUTPUT LOCATION
|
|
"folder_resolution": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "Append resolution to subfolder name. E.g. resized_1024"
|
|
}),
|
|
"folder_custom": ("STRING", {
|
|
"default": "resized",
|
|
"multiline": False,
|
|
"tooltip": "Subfolder name. Resolution is appended if folder_resolution is on."
|
|
}),
|
|
|
|
# OPTIONS
|
|
"skip_if_exists": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Skip files that already exist in the output folder."
|
|
}),
|
|
"delimiter": ("STRING", {
|
|
"default": "_",
|
|
"multiline": False,
|
|
"tooltip": "Separator between filename and folder parts. E.g. _ or -"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT")
|
|
RETURN_NAMES = ("images", "filenames", "output_paths", "count")
|
|
OUTPUT_IS_LIST = (True, True, True, False)
|
|
FUNCTION = "run"
|
|
CATEGORY = "Craftopia"
|
|
OUTPUT_NODE = True
|
|
|
|
def run(self, input_folder, max_pixels, multiple_of, interpolation,
|
|
suffix_resolution, suffix_custom,
|
|
folder_resolution, folder_custom,
|
|
skip_if_exists, delimiter):
|
|
|
|
# Resolve filename suffix
|
|
suffix_raw = suffix_custom.strip()
|
|
if suffix_resolution:
|
|
if suffix_raw == "":
|
|
suffix_raw = f"{delimiter}{max_pixels}"
|
|
else:
|
|
suffix_raw = f"{suffix_raw}{delimiter}{max_pixels}"
|
|
|
|
# Ensure delimiter is prepended if there's a suffix
|
|
if suffix_raw != "" and not suffix_raw.startswith(delimiter):
|
|
filename_suffix = f"{delimiter}{suffix_raw}"
|
|
else:
|
|
filename_suffix = suffix_raw
|
|
|
|
# Resolve output subfolder
|
|
subfolder = folder_custom.strip()
|
|
|
|
if subfolder == "":
|
|
if folder_resolution:
|
|
subfolder = str(max_pixels) # Only resolution
|
|
else:
|
|
subfolder = "resized" # Fallback
|
|
else:
|
|
if folder_resolution:
|
|
subfolder = f"{subfolder}{delimiter}{max_pixels}"
|
|
|
|
input_folder = input_folder.strip()
|
|
if not os.path.isdir(input_folder):
|
|
raise ValueError(f"[SmartBatchResize] Folder not found: {input_folder}")
|
|
|
|
files = sorted([
|
|
f for f in Path(input_folder).iterdir()
|
|
if f.is_file() and f.suffix.lower() in SUPPORTED_EXT
|
|
])
|
|
|
|
if not files:
|
|
raise ValueError(f"[SmartBatchResize] No images found in: {input_folder}")
|
|
|
|
out_dir = Path(input_folder) / subfolder
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
interp = INTERP_MAP[interpolation]
|
|
images_out, filenames_out, paths_out = [], [], []
|
|
|
|
for f in files:
|
|
out_name = f"{f.stem}{filename_suffix}{f.suffix.lower()}"
|
|
out_path = out_dir / out_name
|
|
|
|
if skip_if_exists and out_path.exists():
|
|
print(f"[SmartBatchResize] Skipped (exists): {out_name}")
|
|
continue
|
|
|
|
img = PILImage.open(f).convert("RGB")
|
|
w, h = img.size
|
|
new_w, new_h = _calc_new_size(w, h, max_pixels, multiple_of)
|
|
|
|
img_resized = img.resize((new_w, new_h), interp)
|
|
|
|
save_kwargs = {"quality": 95} if f.suffix.lower() in (".jpg", ".jpeg") else {}
|
|
img_resized.save(out_path, **save_kwargs)
|
|
|
|
print(f"[SmartBatchResize] {f.name} → {out_name} ({w}x{h} → {new_w}x{new_h})")
|
|
|
|
arr = np.array(img_resized).astype("float32") / 255.0
|
|
images_out.append(torch.from_numpy(arr).unsqueeze(0))
|
|
filenames_out.append(out_name)
|
|
paths_out.append(str(out_path))
|
|
|
|
if not images_out:
|
|
raise ValueError("[SmartBatchResize] No images processed.")
|
|
|
|
return (images_out, filenames_out, paths_out, len(images_out))
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {"SmartBatchResize": SmartBatchResize}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {"SmartBatchResize": "Smart Batch Resize 📁"}
|