Add image preview functionality and fix subfolder filtering

- Add image preview display matching standard LoadImage node behavior
- Fix image path handling to use ComfyUI's folder_paths.get_annotated_filepath
- Enable image_upload widget capability
- Implement delayed update strategy to override ComfyUI's automatic image loading
- Add race condition protection for image preview updates
- Maintain subfolder prefix in image names for proper ComfyUI compatibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rdomunky
2025-07-19 21:21:16 -07:00
parent 0de6ee8b12
commit 7752d56a03
2 changed files with 106 additions and 30 deletions

View File

@@ -85,6 +85,7 @@ class SubfolderImageLoader:
}), }),
"image": (default_images if default_images else [""], { "image": (default_images if default_images else [""], {
"default": default_images[0] if default_images else "", "default": default_images[0] if default_images else "",
"image_upload": True,
"tooltip": "Choose an image from the selected subfolder. This list is filtered based on your subfolder selection." "tooltip": "Choose an image from the selected subfolder. This list is filtered based on your subfolder selection."
}), }),
}, },
@@ -106,9 +107,9 @@ class SubfolderImageLoader:
# Root folder - show only images without subfolder (no slash) # Root folder - show only images without subfolder (no slash)
return [img for img in all_images if '/' not in img] return [img for img in all_images if '/' not in img]
else: else:
# Specific subfolder - show only images from that subfolder, without prefix # Specific subfolder - show images WITH subfolder prefix for ComfyUI compatibility
prefix = subfolder + "/" prefix = subfolder + "/"
filtered = [img[len(prefix):] for img in all_images filtered = [img for img in all_images
if img.startswith(prefix) and '/' not in img[len(prefix):]] if img.startswith(prefix) and '/' not in img[len(prefix):]]
return filtered return filtered
@@ -264,34 +265,20 @@ class SubfolderImageLoader:
input_dir = folder_paths.get_input_directory() input_dir = folder_paths.get_input_directory()
# Handle the case where image might contain subfolder prefix # The image parameter now contains the full path (subfolder/filename) when applicable
clean_image = image # Use it directly as the image identifier for ComfyUI
actual_subfolder = subfolder image_identifier = image
# If image contains a path separator, extract the subfolder and filename # Extract clean filename for return value
if '/' in image: if '/' in image:
parts = image.split('/') parts = image.split('/')
if len(parts) == 2: clean_image = parts[-1] # Get the filename part
potential_subfolder, clean_image = parts
# Use the subfolder from the image path if no subfolder is explicitly set
if not actual_subfolder:
actual_subfolder = potential_subfolder
# Build the full path
if actual_subfolder:
file_path = os.path.join(input_dir, actual_subfolder, clean_image)
else: else:
file_path = os.path.join(input_dir, clean_image) clean_image = image
# Validate path is safe # Use ComfyUI's standard method to get the full file path
file_path_abs = os.path.abspath(file_path) # This handles the path resolution and validation automatically
input_dir_abs = os.path.abspath(input_dir) file_path = folder_paths.get_annotated_filepath(image_identifier)
if not file_path_abs.startswith(input_dir_abs):
raise ValueError("Invalid file path: outside input directory")
# Check file exists
if not os.path.exists(file_path):
raise FileNotFoundError(f"Image file not found: {file_path}")
# Load and process image # Load and process image
image_tensor, mask_tensor = self.process_image(file_path, load_mask) image_tensor, mask_tensor = self.process_image(file_path, load_mask)

View File

@@ -27,6 +27,74 @@ app.registerExtension({
this.updateImageList(value); this.updateImageList(value);
}; };
// Flag to prevent concurrent updateImageList calls
this._updatingImageList = false;
// Track all pending image loads so we can cancel them
this._pendingImages = [];
// Disable ComfyUI's automatic image loading to prevent interference
this.imageIndex = null; // Remove automatic image loading
// Override onLoaded function to prevent automatic loading
this.onLoaded = function() {
// Do nothing - prevent ComfyUI's automatic image loading
};
// Simple image array for preview functionality
this.imgs = [];
// showImage function with delayed updates to override ComfyUI's automatic loading
this.showImage = function(name) {
// Cancel ALL previous pending image loads
this._pendingImages.forEach(pendingImg => {
pendingImg.onload = null;
pendingImg.onerror = null;
pendingImg.src = ''; // Stop loading
});
this._pendingImages = [];
const img = new Image();
// Add to pending list
this._pendingImages.push(img);
img.onload = () => {
// Only update if this image is still in the pending list (not cancelled)
if (this._pendingImages.includes(img)) {
// Apply multiple delayed updates to override ComfyUI's persistent automatic loading
[100, 200, 300].forEach(delay => {
setTimeout(() => {
// Check if this image is still the most recent one
if (this._pendingImages.includes(img)) {
this.imgs = [img];
app.graph.setDirtyCanvas(true);
}
}, delay);
});
// Remove from pending list after all delays
setTimeout(() => {
const index = this._pendingImages.indexOf(img);
if (index > -1) {
this._pendingImages.splice(index, 1);
}
}, 350);
}
};
if (name) {
let subfolder = this.widgets?.find(w => w.name === "subfolder")?.value ?? "";
// Extract filename from path if needed
const filename = name.includes('/') ? name.split('/').pop() : name;
const imageUrl = api.apiURL(`/view?filename=${encodeURIComponent(filename)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`);
img.src = imageUrl;
} else {
this.imgs = [];
app.graph.setDirtyCanvas(true);
}
};
// Also run initial filtering based on current subfolder value // Also run initial filtering based on current subfolder value
const currentSubfolder = subfolderWidget.value || ""; const currentSubfolder = subfolderWidget.value || "";
this.updateImageList(currentSubfolder); this.updateImageList(currentSubfolder);
@@ -41,10 +109,18 @@ app.registerExtension({
// Method to update image list based on selected subfolder // Method to update image list based on selected subfolder
nodeType.prototype.updateImageList = async function(selectedSubfolder) { nodeType.prototype.updateImageList = async function(selectedSubfolder) {
// Prevent concurrent updateImageList calls
if (this._updatingImageList) {
return;
}
try { try {
// Set flag to indicate we're updating the list programmatically
this._updatingImageList = true;
// Get the image widget // Get the image widget
const imageWidget = this.widgets?.find(w => w.name === "image"); const imageWidget = this.widgets?.find(w => w.name === "image");
if (!imageWidget) { if (!imageWidget) {
this._updatingImageList = false;
return; return;
} }
@@ -62,12 +138,14 @@ app.registerExtension({
if (!response.ok) { if (!response.ok) {
console.error("SubfolderImageLoader: Failed to refresh image list:", response.statusText); console.error("SubfolderImageLoader: Failed to refresh image list:", response.statusText);
this._updatingImageList = false;
return; return;
} }
const data = await response.json(); const data = await response.json();
if (!data.success) { if (!data.success) {
console.error("SubfolderImageLoader: Server error:", data.error); console.error("SubfolderImageLoader: Server error:", data.error);
this._updatingImageList = false;
return; return;
} }
@@ -93,13 +171,19 @@ app.registerExtension({
"image", "image",
newValue, newValue,
(value) => { (value) => {
// Always show image preview when widget value changes
if (this.showImage) {
this.showImage(value);
}
// Call original callback if it existed // Call original callback if it existed
if (oldCallback) { if (oldCallback) {
oldCallback.call(newWidget, value); oldCallback.call(newWidget, value);
} }
}, },
{ {
values: filteredImages.length > 0 ? filteredImages : [""] values: filteredImages.length > 0 ? filteredImages : [""],
image_upload: true
} }
); );
@@ -112,10 +196,12 @@ app.registerExtension({
// Force update the node's properties // Force update the node's properties
this.setProperty("image", newValue); this.setProperty("image", newValue);
// Trigger the widget callback to ensure the change is registered // Use requestAnimationFrame for initial image display
if (newWidget.callback) { requestAnimationFrame(() => {
newWidget.callback(newValue); if (newValue && this.showImage) {
} this.showImage(newValue);
}
});
} }
// Force UI update // Force UI update
@@ -126,6 +212,9 @@ app.registerExtension({
} catch (error) { } catch (error) {
console.error("SubfolderImageLoader: Error updating image list:", error); console.error("SubfolderImageLoader: Error updating image list:", error);
} finally {
// Always clear the flag when done
this._updatingImageList = false;
} }
}; };