init
This commit is contained in:
@@ -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`
|
||||
|
||||
+74
@@ -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"
|
||||
}
|
||||
|
||||
+30
@@ -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']
|
||||
@@ -0,0 +1 @@
|
||||
pillow-avif-plugin
|
||||
@@ -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="(?<!\{)}Prompt:{(?![\w\s]*[\}])";
|
||||
let exifJsonStringMap = new Map([
|
||||
["workflow",exifJsonString.slice(9,exifJsonString.search(promptregex)+1)],
|
||||
["prompt",exifJsonString.substring((exifJsonString.search(promptregex)+8))]
|
||||
]);
|
||||
let fullJson=Object.fromEntries(exifJsonStringMap);
|
||||
|
||||
resolve(fullJson);
|
||||
|
||||
}
|
||||
offset++;
|
||||
}
|
||||
|
||||
reject(new Error('EXIF metadata not found'));
|
||||
}})};
|
||||
|
||||
|
||||
|
||||
const handleFile = app.handleFile;
|
||||
app.handleFile = async function(file) {
|
||||
if (file.type === "image/avif") {
|
||||
|
||||
const avifInfo =await getAvifExifData(file);
|
||||
if (avifInfo) {
|
||||
if (avifInfo.workflow) {
|
||||
if(app.load_workflow_with_components) {
|
||||
app.load_workflow_with_components(avifInfo.workflow);
|
||||
}
|
||||
else
|
||||
this.loadGraphData(JSON.parse(avifInfo.workflow));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return handleFile.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
},});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { applyTextReplacements } from "../../scripts/utils.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "pkpk.saveavif.exoutput",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "SaveAvif") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
const widget = this.widgets.find((w) => 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;
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user