From f9063bdfd36c41eab128d2884c23b91ec32860d2 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Mon, 29 May 2023 16:44:41 -0500
Subject: [PATCH] Multiselect image dialog
---
nodes.py | 32 +++++--
web/scripts/widgets.js | 208 ++++++++++++++++++++++++++++++++++++-----
web/style.css | 151 ++++++++++++++++++++++++++++++
3 files changed, 361 insertions(+), 30 deletions(-)
diff --git a/nodes.py b/nodes.py
index ae3c784bf..73def68d2 100644
--- a/nodes.py
+++ b/nodes.py
@@ -1125,9 +1125,12 @@ class LoadImageBatch:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
- files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
+ output_dir = folder_paths.get_output_directory()
+ file_dict = {}
+ file_dict["input"] = sorted(f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)))
+ file_dict["output"] = sorted(f for f in os.listdir(output_dir) if os.path.isfile(os.path.join(output_dir, f)))
return {"required":
- {"images": ("MULTIIMAGEUPLOAD", { "filepaths": sorted(files) } )},
+ {"images": ("MULTIIMAGEUPLOAD", { "filepaths": file_dict } )},
}
CATEGORY = "image"
@@ -1141,12 +1144,31 @@ class LoadImageBatch:
output_images = []
output_masks = []
- for i in range(len(images)):
- image_path = folder_paths.get_annotated_filepath(images[i])
+ loaded_images = []
+ for idx in range(len(images)):
+ image_path = folder_paths.get_annotated_filepath(images[idx])
i = Image.open(image_path)
i = ImageOps.exif_transpose(i)
+ loaded_images.append(i)
+
+ min_size = float('inf')
+ min_image = None
+
+ for image in loaded_images:
+ size = image.size[0] * image.size[1]
+ if size < min_size:
+ min_size = size
+ min_image = image
+
+ for idx in range(len(images)):
+ i = loaded_images[idx]
+
+ if i != min_image:
+ i = i.resize(min_image.size)
+
image = i.convert("RGB")
+
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
@@ -1258,7 +1280,6 @@ class ImageScale:
return (s,)
class ImageInvert:
-
@classmethod
def INPUT_TYPES(s):
return {"required": { "image": ("IMAGE",)}}
@@ -1274,7 +1295,6 @@ class ImageInvert:
class ImagePadForOutpaint:
-
@classmethod
def INPUT_TYPES(s):
return {
diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js
index 71432ee0a..2327a7015 100644
--- a/web/scripts/widgets.js
+++ b/web/scripts/widgets.js
@@ -442,38 +442,41 @@ const IMAGEUPLOAD = (node, inputName, inputData, app) => {
return { widget: uploadWidget };
}
+async function loadImageAsync(imageURL) {
+ return new Promise((resolve) => {
+ const e = new Image();
+ e.setAttribute('crossorigin', 'anonymous');
+ e.addEventListener("load", () => { resolve(e); });
+ e.src = imageURL;
+ return e;
+ });
+}
+
const MULTIIMAGEUPLOAD = (node, inputName, inputData, app) => {
const imagesWidget = node.addWidget("text", inputName, inputData, () => {})
imagesWidget.disabled = true;
- imagesWidget._filepaths = []
+ imagesWidget._filepaths = {}
if (inputData[1] && inputData[1].filepaths) {
imagesWidget._filepaths = inputData[1].filepaths
}
- let uploadWidget;
- let clearWidget;
-
- function showImages(names) {
+ async function showImages(names) {
node.imgs = []
for (const name of names) {
- const img = new Image();
- img.onload = () => {
- // TODO await this?
- node.imgs.push(img)
- node.imageIndex = null;
- node.setSizeForImage?.();
- app.graph.setDirtyCanvas(true);
- };
let folder_separator = name.lastIndexOf("/");
let subfolder = "";
if (folder_separator > -1) {
subfolder = name.substring(0, folder_separator);
name = name.substring(folder_separator + 1);
}
- img.src = `/view?filename=${name}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`;
+ const src = `/view?filename=${name}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`;
+ const img = await loadImageAsync(src);
+ node.imgs.push(img)
+ node.imageIndex = null;
node.setSizeForImage?.();
+ app.graph.setDirtyCanvas(true);
}
}
@@ -512,19 +515,20 @@ const MULTIIMAGEUPLOAD = (node, inputName, inputData, app) => {
// Add our own callback to the combo widget to render an image when it changes
const cb = node.callback;
- imagesWidget.callback = function () {
- showImages(imagesWidget.value);
- if (cb) {
- return cb.apply(this, arguments);
- }
+ imagesWidget.callback = () => {
+ showImages(imagesWidget.value).then(() => {
+ if (cb) {
+ return cb.apply(this, arguments);
+ }
+ })
};
// On load if we have a value then render the image
// The value isnt set immediately so we need to wait a moment
// No change callbacks seem to be fired on initial setting of the value
- requestAnimationFrame(() => {
+ requestAnimationFrame(async () => {
if (Array.isArray(imagesWidget.value) && imagesWidget.value.length > 0) {
- showImages(imagesWidget.value);
+ await showImages(imagesWidget.value);
}
});
@@ -553,7 +557,7 @@ const MULTIIMAGEUPLOAD = (node, inputName, inputData, app) => {
}
if (updateNode) {
- showImages(imagesWidget.value);
+ await showImages(imagesWidget.value);
}
}
@@ -572,12 +576,168 @@ const MULTIIMAGEUPLOAD = (node, inputName, inputData, app) => {
document.body.append(fileInput);
// Create the button widget for selecting the files
- uploadWidget = node.addWidget("button", "choose files to upload", "images", () => {
+ const pickWidget = node.addWidget("button", "pick files from ComfyUI folders", "images", () => {
+ const graphCanvas = LiteGraph.LGraphCanvas.active_canvas
+ if (graphCanvas == null)
+ return;
+
+ const panel = graphCanvas.createPanel("Pick Images", { closable: true });
+ panel.node = node;
+ panel.classList.add("multiimageupload_dialog");
+ const swap = (arr, i, j) => {
+ const temp = arr[i];
+ arr[i] = arr[j];
+ arr[j] = temp;
+ }
+
+ const rootHtml = `
+
+
+
+
+`;
+ const rootElem = panel.addHTML(rootHtml, "root");
+ const left = rootElem.querySelector('.left')
+ const right = rootElem.querySelector('.right')
+
+ const previewHtml = `
+
+
+
+
+
+
+
+
`;
+ const previewElem = document.createElement("div");
+ previewElem.innerHTML = previewHtml;
+ previewElem.className = "multiimageupload_preview";
+ right.appendChild(previewElem);
+
+ const folderTypeSel = previewElem.querySelector('.folder-type');
+ const imagePathSel = previewElem.querySelector('.image-path');
+ const imagePreview = previewElem.querySelector('.image-preview');
+
+ imagePathSel.addEventListener("change", (event) => {
+ const filename = event.target.value;
+ const type = folderTypeSel.value;
+ imagePreview.src = `/view?filename=${filename}&type=${type}`
+ });
+
+ folderTypeSel.addEventListener("change", (event) => {
+ imagePathSel.innerHTML = "";
+ const filepaths = imagesWidget._filepaths[event.target.value];
+ if (filepaths == null)
+ return;
+
+ for (const filepath of filepaths) {
+ const filename = filepath.split('\\').pop().split('/').pop();
+ const opt = document.createElement('option');
+ opt.value = filepath
+ opt.innerHTML = filename
+ imagePathSel.appendChild(opt);
+ }
+
+ imagePathSel.value = filepaths[0]
+ imagePathSel.dispatchEvent(new Event('change'));
+ });
+
+ folderTypeSel.value = "output";
+ folderTypeSel.dispatchEvent(new Event('change'));
+
+ const addButton = previewElem.querySelector('.add-image');
+ addButton.addEventListener("click", async (event) => {
+ const filename = imagePathSel.value;
+ const type = folderTypeSel.value;
+ const value = `${filename} [${type}]`;
+ imagesWidget._real_value.push(value)
+ imagesWidget.value = imagesWidget._real_value
+ await showImages(imagesWidget.value);
+ inner_refresh();
+ })
+
+ const clearButton = panel.addButton("Clear", () => {
+ imagesWidget.value = []
+ showImages(imagesWidget.value);
+ inner_refresh();
+ })
+ clearButton.style.display = "block";
+ clearButton.style.marginLeft = "initial";
+
+ const inner_refresh = () => {
+ left.innerHTML = ""
+ graphCanvas.draw(true);
+
+ if (node.imgs) {
+ for (let i = 0; i < imagesWidget.value.length; i++) {
+ const imagePath = imagesWidget.value[i];
+ const img = node.imgs[i];
+ if (!imagePath || !img)
+ continue;
+ const html = `
+
+
+
+
+
+
+
+
`;
+ const elem = document.createElement("div");
+ elem.innerHTML = html;
+ elem.className = "multiimageupload_image";
+ left.appendChild(elem);
+
+ elem.dataset["imagePath"] = imagePath
+ elem.dataset["imageIndex"] = "" + i;
+ elem.querySelector(".image-path").innerText = imagePath
+ elem.querySelector(".type").innerText = ""
+ elem.querySelector(".delete").addEventListener("click", function(e) {
+ const imageIndex = +this.parentNode.parentNode.dataset["imageIndex"]
+ imagesWidget._real_value.splice(imageIndex, 1)
+ imagesWidget.value = imagesWidget._real_value
+ node.imgs.splice(imageIndex, 1);
+ node.imageIndex = null;
+ node.setSizeForImage?.();
+ inner_refresh();
+ });
+ const move_up = elem.querySelector(".move_up");
+ move_up.disabled = i <= 0;
+ move_up.addEventListener("click", function(e) {
+ const imageIndex = +this.parentNode.parentNode.dataset["imageIndex"]
+ if (imageIndex < 0)
+ return;
+ swap(imagesWidget.value, imageIndex, imageIndex - 1);
+ swap(node.imgs, imageIndex, imageIndex - 1);
+ inner_refresh();
+ });
+ const move_down = elem.querySelector(".move_down")
+ move_down.disabled = i >= imagesWidget.value.length - 1;
+ move_down.addEventListener("click", function(e) {
+ const imageIndex = +this.parentNode.parentNode.dataset["imageIndex"]
+ if (imageIndex > imagesWidget.value.length - 1)
+ return;
+ swap(imagesWidget.value, imageIndex, imageIndex + 1);
+ swap(node.imgs, imageIndex, imageIndex + 1);
+ inner_refresh();
+ });
+ }
+ }
+ }
+
+ inner_refresh();
+ document.body.appendChild(panel);
+ }, { serialize: false });
+
+ const uploadWidget = node.addWidget("button", "choose files to upload", "images", () => {
fileInput.value = null;
fileInput.click();
}, { serialize: false });
- clearWidget = node.addWidget("button", "clear all uploads", "images", () => {
+ const clearWidget = node.addWidget("button", "clear all uploads", "images", () => {
imagesWidget.value = []
showImages(imagesWidget.value);
}, { serialize: false });
diff --git a/web/style.css b/web/style.css
index 47571a16e..7908f8df0 100644
--- a/web/style.css
+++ b/web/style.css
@@ -356,3 +356,154 @@ button.comfy-queue-btn {
color: var(--input-text);
filter: brightness(50%);
}
+
+.litegraph .dialog.multiimageupload_dialog {
+ font-family: Arial;
+ display: inline-block;
+ text-align: right;
+ color: #AAA;
+ top: 10%;
+ left: calc(50% - 400px);
+ width: 800px;
+ height: calc(100% - (10% * 2));
+ max-width: initial;
+ min-width: 200px;
+ max-height: initial;
+ min-height: 20px;
+ padding: 4px;
+ margin: auto;
+ overflow: hidden;
+ cursor: pointer;
+ border-radius: 3px;
+}
+
+.litegraph .dialog select {
+ margin-right: 20px;
+ padding-left: 4px;
+ font-size: 12pt;
+ background-color: #1c1c1c;
+ color: #ccc;
+ border: 0;
+}
+
+.multiimageupload_dialog {
+ position: fixed;
+ padding: 4px;
+}
+
+.multiimageupload_dialog .root {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+}
+
+.multiimageupload_dialog .left {
+ width: 50%;
+ overflow-y: auto;
+ border-right: 2px solid #555;
+}
+
+.multiimageupload_dialog .right {
+ width: 50%;
+}
+
+.multiimageupload_preview img {
+ width: 300px;
+ height: 300px;
+ object-fit: contain;
+ display: block;
+ margin-left: 16px;
+ margin-bottom: 8px;
+}
+
+.multiimageupload_preview .bar {
+ display: flex;
+ padding: 0.5rem 0rem 0.5rem 1rem;
+}
+
+.multiimageupload_preview select {
+ max-width: 250px;
+}
+
+.multiimageupload_dialog :disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+
+.multiimageupload_dialog:hover {
+ background-color: #333;
+}
+
+.multiimageupload_dialog.extra {
+ margin-top: 8px;
+}
+
+.multiimageupload_dialog span.name {
+ font-size: 1.3em;
+ padding-left: 4px;
+}
+
+.multiimageupload_dialog span.type {
+ opacity: 0.5;
+ margin-right: 20px;
+ padding-left: 4px;
+}
+
+.multiimageupload_dialog span.label {
+ display: inline-block;
+ width: 60px;
+ padding: 0px 10px;
+}
+
+.multiimageupload_dialog input {
+ width: 140px;
+ color: #999;
+ background-color: #1A1A1A;
+ border-radius: 4px;
+ border: 0;
+ margin-right: 10px;
+ padding: 4px;
+ padding-left: 10px;
+}
+
+.multiimageupload_dialog button {
+ background-color: #1c1c1c;
+ color: #aaa;
+ border: 0;
+ border-radius: 2px;
+ padding: 4px 10px;
+ cursor: pointer;
+}
+
+.multiimageupload_dialog.extra {
+ color: #ccc;
+}
+
+.multiimageupload_dialog.extra input {
+ background-color: #111;
+}
+
+.multiimageupload_image {
+ padding: 4px;
+ display: flex;
+ flex-direction: column;
+}
+
+.multiimageupload_image .bar {
+ padding: 4px;
+ display: flex;
+ flex-direction: row;
+}
+
+.multiimageupload_image .bar span {
+ margin: auto;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.multiimageupload_image img {
+ width: 300px;
+ height: 300px;
+ object-fit: contain;
+}