From a882dce5c8dfd2c78c73dbffbd1ed98b2d4bda3d Mon Sep 17 00:00:00 2001 From: arlozen Date: Mon, 24 Nov 2025 21:22:13 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=E9=87=8D=E6=9E=84=EF=BC=9A?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=A2=84=E8=A7=88=E5=99=A8=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/debug/__init__.py | 4 +- nodes/debug/image_base64_previewer.py | 80 --------- nodes/debug/image_batch_base64_previewer.py | 100 ----------- nodes/image/__init__.py | 3 +- nodes/image/image_safe_previewer.py | 52 ++++++ web/encoding/image_batch_base64_previewer.js | 159 ------------------ .../image_safe_previewer.js} | 31 ++-- 7 files changed, 76 insertions(+), 353 deletions(-) delete mode 100644 nodes/debug/image_base64_previewer.py delete mode 100644 nodes/debug/image_batch_base64_previewer.py create mode 100644 nodes/image/image_safe_previewer.py delete mode 100644 web/encoding/image_batch_base64_previewer.js rename web/{encoding/image_base64_previewer.js => image/image_safe_previewer.js} (70%) diff --git a/nodes/debug/__init__.py b/nodes/debug/__init__.py index 318c408..be0609e 100644 --- a/nodes/debug/__init__.py +++ b/nodes/debug/__init__.py @@ -1,4 +1,2 @@ # from .toast_box import * -# from .dialog_box import * -from .image_base64_previewer import * -from .image_batch_base64_previewer import * \ No newline at end of file +# from .dialog_box import * \ No newline at end of file diff --git a/nodes/debug/image_base64_previewer.py b/nodes/debug/image_base64_previewer.py deleted file mode 100644 index 8163c93..0000000 --- a/nodes/debug/image_base64_previewer.py +++ /dev/null @@ -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": "" - }] - } - } \ No newline at end of file diff --git a/nodes/debug/image_batch_base64_previewer.py b/nodes/debug/image_batch_base64_previewer.py deleted file mode 100644 index 6eb2e73..0000000 --- a/nodes/debug/image_batch_base64_previewer.py +++ /dev/null @@ -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": "" - }] - } - } \ No newline at end of file diff --git a/nodes/image/__init__.py b/nodes/image/__init__.py index 855be60..4da8d23 100644 --- a/nodes/image/__init__.py +++ b/nodes/image/__init__.py @@ -1,3 +1,4 @@ from .image_batch_selector import * from .image_encryptor import * -from .image_batch_encryptor import * \ No newline at end of file +from .image_batch_encryptor import * +from .image_safe_previewer import * \ No newline at end of file diff --git a/nodes/image/image_safe_previewer.py b/nodes/image/image_safe_previewer.py new file mode 100644 index 0000000..c3e1c61 --- /dev/null +++ b/nodes/image/image_safe_previewer.py @@ -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 + }] + } + } \ No newline at end of file diff --git a/web/encoding/image_batch_base64_previewer.js b/web/encoding/image_batch_base64_previewer.js deleted file mode 100644 index 8dc6b23..0000000 --- a/web/encoding/image_batch_base64_previewer.js +++ /dev/null @@ -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); - } - }; - } - } -}); \ No newline at end of file diff --git a/web/encoding/image_base64_previewer.js b/web/image/image_safe_previewer.js similarity index 70% rename from web/encoding/image_base64_previewer.js rename to web/image/image_safe_previewer.js index 8be489a..9e114c7 100644 --- a/web/encoding/image_base64_previewer.js +++ b/web/image/image_safe_previewer.js @@ -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; }