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:
@@ -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
@@ -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})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user