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; +}