mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-21 20:00:17 +08:00
Multiselect image dialog
This commit is contained in:
parent
4110e628f3
commit
f9063bdfd3
32
nodes.py
32
nodes.py
@ -1125,9 +1125,12 @@ class LoadImageBatch:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
input_dir = folder_paths.get_input_directory()
|
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":
|
return {"required":
|
||||||
{"images": ("MULTIIMAGEUPLOAD", { "filepaths": sorted(files) } )},
|
{"images": ("MULTIIMAGEUPLOAD", { "filepaths": file_dict } )},
|
||||||
}
|
}
|
||||||
|
|
||||||
CATEGORY = "image"
|
CATEGORY = "image"
|
||||||
@ -1141,12 +1144,31 @@ class LoadImageBatch:
|
|||||||
output_images = []
|
output_images = []
|
||||||
output_masks = []
|
output_masks = []
|
||||||
|
|
||||||
for i in range(len(images)):
|
loaded_images = []
|
||||||
image_path = folder_paths.get_annotated_filepath(images[i])
|
|
||||||
|
|
||||||
|
for idx in range(len(images)):
|
||||||
|
image_path = folder_paths.get_annotated_filepath(images[idx])
|
||||||
i = Image.open(image_path)
|
i = Image.open(image_path)
|
||||||
i = ImageOps.exif_transpose(i)
|
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 = i.convert("RGB")
|
||||||
|
|
||||||
image = np.array(image).astype(np.float32) / 255.0
|
image = np.array(image).astype(np.float32) / 255.0
|
||||||
image = torch.from_numpy(image)[None,]
|
image = torch.from_numpy(image)[None,]
|
||||||
if 'A' in i.getbands():
|
if 'A' in i.getbands():
|
||||||
@ -1258,7 +1280,6 @@ class ImageScale:
|
|||||||
return (s,)
|
return (s,)
|
||||||
|
|
||||||
class ImageInvert:
|
class ImageInvert:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {"required": { "image": ("IMAGE",)}}
|
return {"required": { "image": ("IMAGE",)}}
|
||||||
@ -1274,7 +1295,6 @@ class ImageInvert:
|
|||||||
|
|
||||||
|
|
||||||
class ImagePadForOutpaint:
|
class ImagePadForOutpaint:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -442,38 +442,41 @@ const IMAGEUPLOAD = (node, inputName, inputData, app) => {
|
|||||||
return { widget: uploadWidget };
|
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 MULTIIMAGEUPLOAD = (node, inputName, inputData, app) => {
|
||||||
const imagesWidget = node.addWidget("text", inputName, inputData, () => {})
|
const imagesWidget = node.addWidget("text", inputName, inputData, () => {})
|
||||||
imagesWidget.disabled = true;
|
imagesWidget.disabled = true;
|
||||||
|
|
||||||
imagesWidget._filepaths = []
|
imagesWidget._filepaths = {}
|
||||||
if (inputData[1] && inputData[1].filepaths) {
|
if (inputData[1] && inputData[1].filepaths) {
|
||||||
imagesWidget._filepaths = inputData[1].filepaths
|
imagesWidget._filepaths = inputData[1].filepaths
|
||||||
}
|
}
|
||||||
|
|
||||||
let uploadWidget;
|
async function showImages(names) {
|
||||||
let clearWidget;
|
|
||||||
|
|
||||||
function showImages(names) {
|
|
||||||
node.imgs = []
|
node.imgs = []
|
||||||
|
|
||||||
for (const name of names) {
|
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 folder_separator = name.lastIndexOf("/");
|
||||||
let subfolder = "";
|
let subfolder = "";
|
||||||
if (folder_separator > -1) {
|
if (folder_separator > -1) {
|
||||||
subfolder = name.substring(0, folder_separator);
|
subfolder = name.substring(0, folder_separator);
|
||||||
name = name.substring(folder_separator + 1);
|
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?.();
|
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
|
// Add our own callback to the combo widget to render an image when it changes
|
||||||
const cb = node.callback;
|
const cb = node.callback;
|
||||||
imagesWidget.callback = function () {
|
imagesWidget.callback = () => {
|
||||||
showImages(imagesWidget.value);
|
showImages(imagesWidget.value).then(() => {
|
||||||
if (cb) {
|
if (cb) {
|
||||||
return cb.apply(this, arguments);
|
return cb.apply(this, arguments);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// On load if we have a value then render the image
|
// On load if we have a value then render the image
|
||||||
// The value isnt set immediately so we need to wait a moment
|
// 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
|
// 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) {
|
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) {
|
if (updateNode) {
|
||||||
showImages(imagesWidget.value);
|
await showImages(imagesWidget.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,12 +576,168 @@ const MULTIIMAGEUPLOAD = (node, inputName, inputData, app) => {
|
|||||||
document.body.append(fileInput);
|
document.body.append(fileInput);
|
||||||
|
|
||||||
// Create the button widget for selecting the files
|
// 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 = `
|
||||||
|
<div class="left">
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const rootElem = panel.addHTML(rootHtml, "root");
|
||||||
|
const left = rootElem.querySelector('.left')
|
||||||
|
const right = rootElem.querySelector('.right')
|
||||||
|
|
||||||
|
const previewHtml = `
|
||||||
|
<img class="image-preview" src="" />
|
||||||
|
<div class="bar">
|
||||||
|
<select class='folder-type'>
|
||||||
|
<option value="output">Output</option>
|
||||||
|
<option value="input">Input</option>
|
||||||
|
</select>
|
||||||
|
<select class='image-path'></select>
|
||||||
|
</div>
|
||||||
|
<div class="bar">
|
||||||
|
<button class="add-image">Add</button>
|
||||||
|
</div>`;
|
||||||
|
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 = `
|
||||||
|
<img src="${img.src}" />
|
||||||
|
<div class="bar">
|
||||||
|
<button class="delete">✕</button>
|
||||||
|
<button class="move_up">↑</button>
|
||||||
|
<button class="move_down">↓</button>
|
||||||
|
<span class='image-path'></span>
|
||||||
|
<span class='type'></span>
|
||||||
|
</div>`;
|
||||||
|
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.value = null;
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
}, { serialize: false });
|
}, { serialize: false });
|
||||||
|
|
||||||
clearWidget = node.addWidget("button", "clear all uploads", "images", () => {
|
const clearWidget = node.addWidget("button", "clear all uploads", "images", () => {
|
||||||
imagesWidget.value = []
|
imagesWidget.value = []
|
||||||
showImages(imagesWidget.value);
|
showImages(imagesWidget.value);
|
||||||
}, { serialize: false });
|
}, { serialize: false });
|
||||||
|
|||||||
151
web/style.css
151
web/style.css
@ -356,3 +356,154 @@ button.comfy-queue-btn {
|
|||||||
color: var(--input-text);
|
color: var(--input-text);
|
||||||
filter: brightness(50%);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user