Add Browse folder button + longest_side presets to Smart Batch Resize

- Browse folder button: native Windows folder picker via PowerShell
  (TopMost owner form + OpenFileDialog for modern Explorer view)
- longest_side preset chips (512/768/1024/1536) drawn on canvas,
  active preset highlighted
- Reorder inputs into logical groups (input → resize → naming → folder → options)
- README: describe Smart Batch Resize disk-save / filename-preservation behaviour
This commit is contained in:
rubz
2026-06-29 22:02:37 +02:00
parent e218330869
commit 9bf268bf1b
4 changed files with 160 additions and 4 deletions
+2
View File
@@ -60,6 +60,8 @@ Load **all images from a folder**, resize each one by longest side, and save wit
Use this for **bulk preprocessing** — e.g. preparing a LoRA dataset from a folder of high-res images.
Processes and saves directly to disk with the **original filename preserved**. The output folder is automatically named with the resolution (e.g. `resized_1024`) so you always know what's inside. Filenames are also passed as outputs so you can use them further in your pipeline.
| Input | Type | Default | Description |
|---|---|---|---|
| input_folder | STRING | D:/images/input | Source folder path |
+44
View File
@@ -9,3 +9,47 @@ NODE_CLASS_MAPPINGS = {**A, **B, **C, **D}
NODE_DISPLAY_NAME_MAPPINGS = {**A_NAMES, **B_NAMES, **C_NAMES, **D_NAMES}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
import asyncio
import concurrent.futures
from aiohttp import web
from server import PromptServer
@PromptServer.instance.routes.get("/craftkit/browse_folder")
async def browse_folder(request):
import subprocess, sys
def _open_dialog():
if sys.platform != "win32":
return ""
ps = (
"Add-Type -AssemblyName System.Windows.Forms;"
"$r='';"
"$o=New-Object System.Windows.Forms.Form;"
"$o.TopMost=$true;$o.ShowInTaskbar=$false;$o.FormBorderStyle='None';"
"$o.Width=1;$o.Height=1;$o.Opacity=0;$o.StartPosition='CenterScreen';"
"$o.Add_Shown({"
"$o.Activate();"
"$d=New-Object System.Windows.Forms.OpenFileDialog;"
"$d.Title='Select input folder';"
"$d.ValidateNames=$false;$d.CheckFileExists=$false;$d.CheckPathExists=$true;"
"$d.FileName='Folder Selection.';"
"$d.Filter='Folders|*.none';"
"if($d.ShowDialog($o) -eq 'OK'){$r=[System.IO.Path]::GetDirectoryName($d.FileName)};"
"$o.Close()});"
"$o.ShowDialog()|Out-Null;"
"$r"
)
result = subprocess.run(
["powershell", "-NoProfile", "-Command", ps],
capture_output=True, text=True
)
return result.stdout.strip()
loop = asyncio.get_running_loop()
with concurrent.futures.ThreadPoolExecutor() as pool:
folder = await loop.run_in_executor(pool, _open_dialog)
if folder and __import__("os").path.isdir(folder):
return web.json_response({"ok": True, "path": folder})
return web.json_response({"ok": False, "cancelled": True})
+110
View File
@@ -0,0 +1,110 @@
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "Craftopia.SmartBatchResize",
async nodeCreated(node) {
if (node.comfyClass !== "SmartBatchResize") return;
const folderWidget = node.widgets?.find(w => w.name === "input_folder");
if (!folderWidget) return;
const btn = node.addWidget("button", "📁 Browse folder", null, async () => {
try {
const res = await fetch("/craftkit/browse_folder");
const data = await res.json();
if (data.ok && data.path) {
folderWidget.value = data.path;
node.setDirtyCanvas(true);
}
} catch (e) {
console.error("[SmartBatchResize] Browse failed:", e);
}
});
// Exclude button from serialization (saves undefined placeholder to keep index alignment)
btn.serializeValue = () => undefined;
// Move Browse button to right after input_folder (index 1)
const folderIdx = node.widgets.indexOf(folderWidget);
const btnIdx = node.widgets.indexOf(btn);
if (btnIdx !== folderIdx + 1) {
node.widgets.splice(btnIdx, 1);
node.widgets.splice(folderIdx + 1, 0, btn);
}
// Compact preset chips for longest_side. Start from a REAL button widget
// (so all of ComfyUI's layout/computeSize/sizing integration is correct)
// and only override its draw + mouse. A bare custom widget with our own
// computeSize forced the node width and broke the layout on click.
const longestSideWidget = node.widgets?.find(w => w.name === "longest_side");
if (longestSideWidget) {
const PRESETS = [512, 768, 1024, 1536];
// Per-chip click handled in the button callback (buttons route clicks
// to their callback, not to .mouse). We read the pointer position from
// the canvas at click time and match it against the drawn chip rects.
const presetWidget = node.addWidget("button", "size_presets", null, (value, canvas, node) => {
const gm = (canvas || app.canvas)?.graph_mouse;
if (!gm) return;
const lx = gm[0] - node.pos[0];
const ly = gm[1] - node.pos[1];
for (const r of presetWidget._rects) {
if (lx >= r.x1 && lx <= r.x2 && ly >= r.y1 && ly <= r.y2) {
longestSideWidget.value = r.size;
longestSideWidget.callback?.(r.size, app.canvas, node);
node.setDirtyCanvas(true);
return;
}
}
});
presetWidget.serializeValue = () => undefined;
presetWidget._rects = [];
presetWidget.draw = function (ctx, node, widgetWidth, y, height) {
const margin = 14;
const gap = 5;
const innerW = Math.max(40, widgetWidth - margin * 2);
const n = PRESETS.length;
const cellW = Math.max(10, (innerW - gap * (n - 1)) / n);
const h = Math.min(height - 2, 18);
const top = y + (height - h) / 2;
const current = longestSideWidget.value;
this._rects = [];
ctx.save();
ctx.font = "11px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (let i = 0; i < n; i++) {
const x = margin + i * (cellW + gap);
const active = current === PRESETS[i];
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(x, top, cellW, h, 4);
else ctx.rect(x, top, cellW, h);
ctx.fillStyle = active ? "#f28f41" : "#2a2a2a";
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = active ? "#f28f41" : "#555";
ctx.stroke();
ctx.fillStyle = active ? "#111" : "#ccc";
ctx.fillText(String(PRESETS[i]), x + cellW / 2, top + h / 2 + 0.5);
this._rects.push({ x1: x, x2: x + cellW, y1: top, y2: top + h, size: PRESETS[i] });
}
ctx.restore();
};
// Move preset row to right after longest_side
const lsIdx = node.widgets.indexOf(longestSideWidget);
const pwIdx = node.widgets.indexOf(presetWidget);
if (pwIdx !== lsIdx + 1) {
node.widgets.splice(pwIdx, 1);
node.widgets.splice(lsIdx + 1, 0, presetWidget);
}
}
},
});
+4 -4
View File
@@ -41,7 +41,7 @@ class SmartBatchResize:
"multiline": False,
"tooltip": "Folder containing images to resize."
}),
# RESIZE SETTINGS
"longest_side": ("INT", {
"default": 1024, "min": 64, "max": 8192, "step": 64,
@@ -54,7 +54,7 @@ class SmartBatchResize:
"interpolation": (["lanczos", "bicubic", "bilinear", "nearest"], {
"default": "lanczos"
}),
# OUTPUT NAMING
"suffix_resolution": ("BOOLEAN", {
"default": True,
@@ -65,7 +65,7 @@ class SmartBatchResize:
"multiline": False,
"tooltip": "Custom text to add before resolution. Only used when suffix_resolution is on."
}),
# OUTPUT LOCATION
"folder_resolution": ("BOOLEAN", {
"default": False,
@@ -76,7 +76,7 @@ class SmartBatchResize:
"multiline": False,
"tooltip": "Subfolder name. Resolution is appended if folder_resolution is on."
}),
# OPTIONS
"skip_if_exists": ("BOOLEAN", {
"default": True,