mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-12 07:10:52 +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
|
||||
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 {
|
||||
|
||||
@ -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 = `
|
||||
<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.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 });
|
||||
|
||||
151
web/style.css
151
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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user