diff --git a/README.md b/README.md new file mode 100644 index 0000000..a49a100 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# ComfyUI-SaveAVIF +A custom node on ComfyUI that saves images in AVIF format. + +Workflow can be loaded from images saved at this node. + +## Description: + +It can be used in the same way as the Save Image node. + +Note that encoding to AVIF takes a little time. + +c_quality: Set quality for color (0 - 100, where 100 is lossless) + +enc_speed: Encoder speed (0 - 10, slowest/smallest - fastest/largest) + +When encoding is finished, the node outputs an image, which is a pass-through of the input. +This is intended for use such as notifying the end of encoding by connecting a node that plays a sound. + +## Installation: + +Install it using ComfyUI Manager. + +Or use `git clone https://github.com/pkpkTech/ComfyUI-SaveAVIF` in your ComfyUI custom nodes directory + +and `pip install -r requirements.txt` + diff --git a/SaveAvif.py b/SaveAvif.py new file mode 100644 index 0000000..52a81e6 --- /dev/null +++ b/SaveAvif.py @@ -0,0 +1,74 @@ +import numpy as np +from PIL import Image +import pillow_avif +import folder_paths +import os +import json + +from comfy.cli_args import args + +class SaveAvif: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + + @classmethod + def INPUT_TYPES(s): + return {"required": + {"images": ("IMAGE", ), + "filename_prefix": ("STRING", {"default": "ComfyUI"}), + "c_quality":("INT", {"default": 75, "min": 0, "max": 100, "step": 1}), + "enc_speed":("INT", {"default": 6, "min": 0, "max": 10, "step": 1})}, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image( pass-through )",) + FUNCTION = "save_avif" + + OUTPUT_NODE = True + + CATEGORY = "image" + + def save_avif(self, images, filename_prefix="ComfyUI", c_quality=75, enc_speed=6, prompt=None, extra_pnginfo=None): + + filename_prefix += self.prefix_append + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) + results = list() + + for image in images: + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + + img_exif = img.getexif() + + if not args.disable_metadata: + if prompt is not None: + prompt_text = "".join(json.dumps(prompt)) + img_exif[0x010f] ="Prompt:" + prompt_text + if extra_pnginfo is not None: + workflow_metadata = str() + for x in extra_pnginfo: + workflow_metadata += "".join(json.dumps(extra_pnginfo[x])) + img_exif[0x010e] = "Workflow:"+ workflow_metadata + + file = f"{filename}_{counter:05}_.avif" + img.save(os.path.join(full_output_folder, file), quality= c_quality, speed= enc_speed, exif= img_exif) + + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }); + counter += 1 + + return { "result": (images,), "ui": { "images": results } } + +NODE_CLASS_MAPPINGS = { + "SaveAvif": SaveAvif +} +NODE_DISPLAY_NAME_MAPPINGS = { + "SaveAvif": "Save AVIF" +} + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0e3cb8e --- /dev/null +++ b/__init__.py @@ -0,0 +1,30 @@ +import shutil +import folder_paths +import os, sys, subprocess +import filecmp + + + +print("### Loading: Save AVIF") + +comfy_path = os.path.dirname(folder_paths.__file__) + +def setup_js(): + self_path = os.path.dirname(__file__) + js_dest_path = os.path.join(comfy_path, "web", "extensions", "saveavif") + info_src_path = os.path.join(self_path, "web", "avifInfo.js") + sex_src_path = os.path.join(self_path, "web", "saveAvifExtraOutput.js") + + ## Creating folder if it's not present, then Copy. + print("Copying JS files for Workflow loading") + if (os.path.isdir(js_dest_path)==False): + os.mkdir(js_dest_path) + + shutil.copy(info_src_path, js_dest_path) + shutil.copy(sex_src_path, js_dest_path) + +setup_js() + +from .SaveAvif import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..81460bb --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pillow-avif-plugin \ No newline at end of file diff --git a/web/avifInfo.js b/web/avifInfo.js new file mode 100644 index 0000000..cfa6310 --- /dev/null +++ b/web/avifInfo.js @@ -0,0 +1,61 @@ +import { app } from "/scripts/app.js"; +app.registerExtension({ + name: "pkpk.saveavif.prompt", +setup(app,file){ + +async function getAvifExifData(avifFile) { + const reader = new FileReader(); + reader.readAsArrayBuffer(avifFile); + + return new Promise((resolve, reject) => { + reader.onloadend = function() { + const buffer = reader.result; + const view = new DataView(buffer); + let offset = 0; + + // Search for the "EXIF" or "Exif" tag + while (offset < view.byteLength - 4) { + const tag = view.getUint32(offset, true); + if (tag === 0x46495845 || tag === 0x66697845) { + const exifOffset = offset + 6; + const exifData = buffer.slice(exifOffset); + const exifString = new TextDecoder().decode(exifData).replaceAll(String.fromCharCode(0), ''); + let exifJsonString = exifString.slice(exifString.indexOf("Workflow")); + let promptregex="(? w.name === "filename_prefix"); + widget.serializeValue = () => { + return applyTextReplacements(app, widget.value); + }; + + return r; + }; + } else { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + if (!this.properties || !("Node name for S&R" in this.properties)) { + this.addProperty("Node name for S&R", this.constructor.type, "string"); + } + + return r; + }; + } + }, +});