♻️重构:调整预览器设计

This commit is contained in:
arlozen
2025-11-24 21:22:13 +08:00
parent 2341ef528c
commit a882dce5c8
7 changed files with 76 additions and 353 deletions
+1 -3
View File
@@ -1,4 +1,2 @@
# from .toast_box import *
# from .dialog_box import *
from .image_base64_previewer import *
from .image_batch_base64_previewer import *
# from .dialog_box import *
-80
View File
@@ -1,80 +0,0 @@
from ... import register_node
from ...utils.format import static_image_formats
@register_node(emoji="🪲")
class ImageBase64Previewer:
"""
A ComfyUI node that previews base64 encoded images in the frontend.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"base64": ("STRING", {
"default": ""
}),
"format": (static_image_formats, {
"default": static_image_formats[0],
}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("base64",)
FUNCTION = "run"
CATEGORY = "EasyToolkit/Debug"
OUTPUT_NODE = True
def run(self, base64, format="image/png"):
"""
Preview base64 encoded image in frontend.
"""
# Validate base64 string
if not base64 or not base64.strip():
return {
"result": ("",),
"ui": {
"base64_preview": [{
"base64_data": "",
"format": format,
"error": "Empty base64 image data"
}]
}
}
# Clean the base64 string (remove data:image/... prefix if present)
clean_base64 = base64.strip()
if clean_base64.startswith("data:image/"):
# Extract just the base64 part after the comma
parts = clean_base64.split(",", 1)
if len(parts) == 2:
clean_base64 = parts[1]
# Validate base64 format
if not clean_base64:
return {
"result": (base64,),
"ui": {
"base64_preview": [{
"base64_data": "",
"format": format,
"error": "Invalid base64 image format"
}]
}
}
# Return the base64 data for frontend preview
return {
"result": (base64,),
"ui": {
"base64_preview": [{
"base64_data": clean_base64,
"format": format,
"error": ""
}]
}
}
-100
View File
@@ -1,100 +0,0 @@
from ... import register_node
from ...utils.format import static_image_formats
@register_node(emoji="🪲")
class ImageBatchBase64Previewer:
"""
A ComfyUI node that previews batch of base64 encoded images in the frontend.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"base64_batch": ("STRING", {
"default": ""
}),
"format": (static_image_formats, {
"default": static_image_formats[0],
}),
"fps": ("FLOAT", {
"default": 24.0,
"min": 1.0,
"max": 120.0,
"step": 1.0
}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("base64_batch",)
FUNCTION = "run"
CATEGORY = "EasyToolkit/Debug"
OUTPUT_NODE = True
def run(self, base64_batch, format="image/png", fps=24.0):
"""
Preview batch of base64 encoded images in frontend.
"""
# Validate base64 string
if not base64_batch or not base64_batch.strip():
return {
"result": ("",),
"ui": {
"base64_batch_preview": [{
"base64_datas": [],
"format": format,
"fps": fps,
"error": "Empty base64 image data"
}]
}
}
# Parse the base64 list (separated by newlines)
base64_items = base64_batch.strip().split("\n")
clean_base64_list = []
for base64_item in base64_items:
if not base64_item.strip():
continue
# Clean the base64 string (remove data:image/... prefix if present)
clean_base64 = base64_item.strip()
if clean_base64.startswith("data:image/"):
# Extract just the base64 part after the comma
parts = clean_base64.split(",", 1)
if len(parts) == 2:
clean_base64 = parts[1]
if clean_base64:
clean_base64_list.append(clean_base64)
# Validate base64 format
if not clean_base64_list:
return {
"result": (base64_batch,),
"ui": {
"base64_batch_preview": [{
"base64_datas": [],
"format": format,
"fps": fps,
"error": "No valid base64 images found"
}]
}
}
# Return the base64 data for frontend preview
return {
"result": (base64_batch,),
"ui": {
"base64_batch_preview": [{
"base64_datas": clean_base64_list,
"format": format,
"fps": fps,
"error": ""
}]
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
from .image_batch_selector import *
from .image_encryptor import *
from .image_batch_encryptor import *
from .image_batch_encryptor import *
from .image_safe_previewer import *
+52
View File
@@ -0,0 +1,52 @@
from ... import register_node
from ...utils.format import static_image_formats
from ...utils.image import image_to_bytes
from ...utils.encoding import encode_bytes
import base64
@register_node(emoji="🖼️")
class ImageSafePreviewer:
"""
A ComfyUI node that previews images in the frontend by converting them to base64.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"format": (static_image_formats, {
"default": static_image_formats[0],
}),
}
}
RETURN_TYPES = ()
RETURN_NAMES = ()
FUNCTION = "run"
CATEGORY = "EasyToolkit/Image"
OUTPUT_NODE = True
def run(self, image, format="image/png"):
"""
Convert image to base64 and preview in frontend.
"""
# Convert image to bytes
image_bytes = image_to_bytes(image, format)
# Convert bytes to base64
base64_data = encode_bytes(image_bytes, "base64")
# Return the base64 data for frontend preview
return {
"result": (),
"ui": {
"base64_preview": [{
"base64_data": base64_data,
"format": format
}]
}
}
@@ -1,159 +0,0 @@
import { app } from "../../../scripts/app.js";
import {
calculateAvailableSpace,
drawImagePreview
} from "../preview_utils.js";
/**
* Extension for ImageBatchBase64Previewer node
* Displays batch of base64 encoded images directly on the node during workflow execution
*/
app.registerExtension({
name: "EasyToolkit.ImageBatchBase64Previewer",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === "ImageBatchBase64Previewer") {
// Add custom node behavior
const originalOnExecuted = nodeType.prototype.onExecuted;
this.previewImages = [];
this.currentFrame = 0;
this.lastFrameTime = 0;
this.animationFrameId = null;
nodeType.prototype.onExecuted = function(output) {
// Call original method if it exists
if (originalOnExecuted) {
originalOnExecuted.call(this, output);
}
const previews = output?.base64_batch_preview;
if (previews && previews.length > 0) {
const preview = previews[0];
// Clear previous images and animation
this.previewImages = [];
this.currentFrame = 0;
this.lastFrameTime = 0;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
// Load all images
if (preview.base64_datas && preview.base64_datas.length > 0) {
let loadedCount = 0;
const totalImages = preview.base64_datas.length;
preview.base64_datas.forEach((base64Data, index) => {
const img = new Image();
img.src = `data:${preview.format};base64,${base64Data}`;
img.onload = () => {
this.previewImages[index] = img;
loadedCount++;
// Start animation when all images are loaded
if (loadedCount === totalImages && totalImages > 1) {
this.startAnimation(preview.fps || 24.0);
}
this.setDirtyCanvas(true, true);
};
img.onerror = () => {
console.error(`Failed to load image ${index + 1}`);
loadedCount++;
if (loadedCount === totalImages) {
this.setDirtyCanvas(true, true);
}
};
});
}
}
};
// Start animation for multiple images
nodeType.prototype.startAnimation = function(fps) {
const frameInterval = 1000 / fps;
const animate = (currentTime) => {
if (!this.lastFrameTime) {
this.lastFrameTime = currentTime;
}
const elapsed = currentTime - this.lastFrameTime;
if (elapsed > frameInterval) {
this.currentFrame = (this.currentFrame + 1) % this.previewImages.length;
this.lastFrameTime = currentTime;
this.setDirtyCanvas(true, true);
}
this.animationFrameId = requestAnimationFrame(animate);
};
this.animationFrameId = requestAnimationFrame(animate);
};
// Override onDrawForeground to draw the preview image(s) on the node
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
if (onDrawForeground) {
onDrawForeground.apply(this, arguments);
}
if (this.previewImages && this.previewImages.length > 0) {
// Calculate available space using utility function
const { margin, startY, availableWidth, availableHeight } = calculateAvailableSpace(this);
// Get current image to display
let currentImage;
if (this.previewImages.length === 1) {
currentImage = this.previewImages[0];
} else {
currentImage = this.previewImages[this.currentFrame];
}
if (currentImage && currentImage.complete) {
// Draw image preview using utility function
const { height: drawnHeight } = drawImagePreview(
ctx,
currentImage,
margin,
startY,
availableWidth,
availableHeight
);
// Auto-resize node to fit image if needed
const minHeight = startY + drawnHeight + margin;
if (this.size[1] < minHeight) {
this.size[1] = minHeight;
}
// Show frame info for multiple images
if (this.previewImages.length > 1) {
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fillRect(margin, startY - 20, 80, 18);
ctx.fillStyle = "white";
ctx.font = "12px Arial";
ctx.fillText(`Frame: ${this.currentFrame + 1}/${this.previewImages.length}`, margin + 5, startY - 5);
}
}
}
};
// Clean up animation when node is removed
const originalOnRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
if (originalOnRemoved) {
originalOnRemoved.apply(this, arguments);
}
};
}
}
});
@@ -5,14 +5,14 @@ import {
} from "../preview_utils.js";
/**
* Extension for ImageBase64Previewer node
* Extension for ImageSafePreviewer node
* Displays base64 encoded images directly on the node during workflow execution
*/
app.registerExtension({
name: "EasyToolkit.ImageBase64Previewer",
name: "EasyToolkit.ImageSafePreviewer",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === "ImageBase64Previewer") {
if (nodeData.name === "ImageSafePreviewer") {
// Add custom node behavior
const originalOnExecuted = nodeType.prototype.onExecuted;
@@ -37,19 +37,18 @@ app.registerExtension({
};
}
};
// Override onDrawForeground to draw the preview image on the node
// Override onDrawForeground to draw the preview image and resolution text on the node
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
if (onDrawForeground) {
onDrawForeground.apply(this, arguments);
}
if (this.previewImage && this.previewImage.complete) {
// Calculate available space using utility function
const { margin, startY, availableWidth, availableHeight } = calculateAvailableSpace(this);
// Draw image preview using utility function
const { height: drawnHeight } = drawImagePreview(
ctx,
@@ -59,9 +58,21 @@ app.registerExtension({
availableWidth,
availableHeight
);
// Auto-resize node to fit image if needed
const minHeight = startY + drawnHeight + margin;
// Draw resolution text below the image
const resolutionText = `${this.previewImage.width} × ${this.previewImage.height}`;
const textY = startY + drawnHeight + 15;
ctx.save();
ctx.fillStyle = "#888";
ctx.font = "12px monospace";
ctx.textAlign = "center";
ctx.fillText(resolutionText, this.size[0] / 2, textY);
ctx.restore();
// Auto-resize node to fit image and resolution text if needed
const textHeight = 15; // Additional space for resolution text
const minHeight = startY + drawnHeight + margin + textHeight;
if (this.size[1] < minHeight) {
this.size[1] = minHeight;
}