From e3cf180549bcabd67f8ba815b2b5a4e97e104143 Mon Sep 17 00:00:00 2001 From: curious-george-is-available Date: Thu, 30 Apr 2026 13:28:17 +0530 Subject: [PATCH] Initial commit: Fast Preview node --- .gitignore | 3 ++ __init__.py | 13 ++++++++ fast_preview.py | 68 ++++++++++++++++++++++++++++++++++++++++++ web/js/fast_preview.js | 36 ++++++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 fast_preview.py create mode 100644 web/js/fast_preview.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b908d4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.DS_Store diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..be5a6e9 --- /dev/null +++ b/__init__.py @@ -0,0 +1,13 @@ +from .fast_preview import FastPreviewImage + +WEB_DIRECTORY = "./web" + +NODE_CLASS_MAPPINGS = { + "FastPreviewImage": FastPreviewImage +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "FastPreviewImage": "Fast Preview & Optimize" +} + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] diff --git a/fast_preview.py b/fast_preview.py new file mode 100644 index 0000000..a775173 --- /dev/null +++ b/fast_preview.py @@ -0,0 +1,68 @@ +import os +import random +import string +import numpy as np +from PIL import Image +import folder_paths + +class FastPreviewImage: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "image_format": (["webp", "jpeg", "png"], {"default": "webp"}), + "quality": ("INT", {"default": 80, "min": 1, "max": 100}), + } + } + + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "save_images" + CATEGORY = "image" + + def save_images(self, images, image_format="webp", quality=80): + results = list() + file_sizes = list() + temp_dir = folder_paths.get_temp_directory() + + for batch_number, image in enumerate(images): + # Convert tensor to PIL Image + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + + if image_format == "jpeg" and img.mode in ("RGBA", "P"): + img = img.convert("RGB") + + # Generate random 8-character string for the prefix + prefix = ''.join(random.choices(string.ascii_letters + string.digits, k=8)) + filename = f"fast_preview_{prefix}_{batch_number}.{image_format}" + file_path = os.path.join(temp_dir, filename) + + # Determine compression kwargs based on format + kwargs = {} + if image_format == "webp": + kwargs = {"quality": quality, "method": 2} + elif image_format == "jpeg": + kwargs = {"quality": quality, "optimize": True} + elif image_format == "png": + kwargs = {"compress_level": max(0, min(9, int((100 - quality) / 10)))} + + # Save the image + img.save(file_path, **kwargs) + + # Calculate file size + size_in_bytes = os.path.getsize(file_path) + if size_in_bytes < 1024 * 1024: + size_str = f"{size_in_bytes / 1024:.2f} KB" + else: + size_str = f"{size_in_bytes / (1024 * 1024):.2f} MB" + + file_sizes.append(size_str) + results.append({ + "filename": filename, + "subfolder": "", + "type": "temp" + }) + + return {"ui": {"images": results, "file_sizes": file_sizes}} diff --git a/web/js/fast_preview.js b/web/js/fast_preview.js new file mode 100644 index 0000000..7888efc --- /dev/null +++ b/web/js/fast_preview.js @@ -0,0 +1,36 @@ +import { app } from "../../scripts/app.js"; +import { ComfyWidgets } from "../../scripts/widgets.js"; + +app.registerExtension({ + name: "Comfy.FastPreviewImage", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "FastPreviewImage") { + const onExecuted = nodeType.prototype.onExecuted; + + nodeType.prototype.onExecuted = function(message) { + const r = onExecuted ? onExecuted.apply(this, arguments) : undefined; + + if (message?.file_sizes) { + let widget = this.widgets?.find((w) => w.name === "file_size_display"); + + if (!widget) { + const widgetObj = ComfyWidgets["STRING"](this, "file_size_display", ["STRING", { multiline: false }], app); + widget = widgetObj.widget || widgetObj; + + if (widget && widget.inputEl) { + widget.inputEl.readOnly = true; + widget.inputEl.style.opacity = "0.8"; + widget.inputEl.style.textAlign = "center"; + } + } + + if (widget) { + widget.value = `Size: ${message.file_sizes.join(" | ")}`; + } + } + + return r; + }; + } + } +});