improve: lightweight preview to reduce network traffic (#733)

* To reduce bandwidth traffic in a remote environment, a lossy compression-based preview mode is provided for displaying simple visualizations in node-based widgets.

* Added 'preview=[image format]' option to the '/view' API.
* Updated node to use preview for displaying images as widgets.
* Excluded preview usage in the open image, save image, mask editor where the original data is required.

* Made preview_format parameterizable for extensibility.

* default preview format changed: jpeg -> webp

* Support advanced preview_format option.
- grayscale option for visual debugging
- quality option for aggressive reducing

L?;format;quality?

ex)
jpeg => rgb, jpeg, quality 90
L;webp;80 => grayscale, webp, quality 80
L;png => grayscale, png, quality 90
webp;50 => rgb, webp, quality 50

* move comment

* * add settings for preview_format
* default value is ''(= don't reencode)

---------

Co-authored-by: Lt.Dr.Data <lt.dr.data@gmail.com>
This commit is contained in:
Dr.Lt.Data 2023-06-05 14:49:43 +09:00 committed by GitHub
parent fed0a4dd29
commit 9f3a19b728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 6 deletions

View File

@ -217,6 +217,28 @@ class PromptServer():
file = os.path.join(output_dir, filename) file = os.path.join(output_dir, filename)
if os.path.isfile(file): if os.path.isfile(file):
if 'preview' in request.rel_url.query:
with Image.open(file) as img:
preview_info = request.rel_url.query['preview'].split(';')
if preview_info[0] == "L" or preview_info[0] == "l":
img = img.convert("L")
image_format = preview_info[1]
else:
img = img.convert("RGB") # jpeg doesn't support RGBA
image_format = preview_info[0]
quality = 90
if preview_info[-1].isdigit():
quality = int(preview_info[-1])
buffer = BytesIO()
img.save(buffer, format=image_format, optimize=True, quality=quality)
buffer.seek(0)
return web.Response(body=buffer.read(), content_type=f'image/{image_format}',
headers={"Content-Disposition": f"filename=\"{filename}\""})
if 'channel' not in request.rel_url.query: if 'channel' not in request.rel_url.query:
channel = 'rgba' channel = 'rgba'
else: else:

View File

@ -41,7 +41,7 @@ async function uploadMask(filepath, formData) {
}); });
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image(); ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image();
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = "/view?" + new URLSearchParams(filepath).toString(); ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = "/view?" + new URLSearchParams(filepath).toString() + app.getPreviewFormatParam();
if(ComfyApp.clipspace.images) if(ComfyApp.clipspace.images)
ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath; ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath;
@ -335,6 +335,7 @@ class MaskEditorDialog extends ComfyDialog {
const alpha_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src) const alpha_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src)
alpha_url.searchParams.delete('channel'); alpha_url.searchParams.delete('channel');
alpha_url.searchParams.delete('preview');
alpha_url.searchParams.set('channel', 'a'); alpha_url.searchParams.set('channel', 'a');
touched_image.src = alpha_url; touched_image.src = alpha_url;
@ -345,6 +346,7 @@ class MaskEditorDialog extends ComfyDialog {
const rgb_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src); const rgb_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src);
rgb_url.searchParams.delete('channel'); rgb_url.searchParams.delete('channel');
rgb_url.searchParams.delete('preview');
rgb_url.searchParams.set('channel', 'rgb'); rgb_url.searchParams.set('channel', 'rgb');
orig_image.src = rgb_url; orig_image.src = rgb_url;
this.image = orig_image; this.image = orig_image;

View File

@ -51,6 +51,14 @@ export class ComfyApp {
this.shiftDown = false; this.shiftDown = false;
} }
getPreviewFormatParam() {
let preview_format = this.ui.settings.getSettingValue("Comfy.PreviewFormat");
if(preview_format)
return `&preview=${preview_format}`;
else
return "";
}
static isImageNode(node) { static isImageNode(node) {
return node.imgs || (node && node.widgets && node.widgets.findIndex(obj => obj.name === 'image') >= 0); return node.imgs || (node && node.widgets && node.widgets.findIndex(obj => obj.name === 'image') >= 0);
} }
@ -231,14 +239,20 @@ export class ComfyApp {
options.unshift( options.unshift(
{ {
content: "Open Image", content: "Open Image",
callback: () => window.open(img.src, "_blank"), callback: () => {
let url = new URL(img.src);
url.searchParams.delete('preview');
window.open(url, "_blank")
},
}, },
{ {
content: "Save Image", content: "Save Image",
callback: () => { callback: () => {
const a = document.createElement("a"); const a = document.createElement("a");
a.href = img.src; let url = new URL(img.src);
a.setAttribute("download", new URLSearchParams(new URL(img.src).search).get("filename")); url.searchParams.delete('preview');
a.href = url;
a.setAttribute("download", new URLSearchParams(url.search).get("filename"));
document.body.append(a); document.body.append(a);
a.click(); a.click();
requestAnimationFrame(() => a.remove()); requestAnimationFrame(() => a.remove());
@ -365,7 +379,7 @@ export class ComfyApp {
const img = new Image(); const img = new Image();
img.onload = () => r(img); img.onload = () => r(img);
img.onerror = () => r(null); img.onerror = () => r(null);
img.src = "/view?" + new URLSearchParams(src).toString(); img.src = "/view?" + new URLSearchParams(src).toString() + app.getPreviewFormatParam();
}); });
}) })
).then((imgs) => { ).then((imgs) => {

View File

@ -462,6 +462,25 @@ export class ComfyUI {
defaultValue: true, defaultValue: true,
}); });
/**
* file format for preview
*
* L?;format;quality
*
* ex)
* L;webp;50 -> grayscale, webp, quality 50
* jpeg;80 -> rgb, jpeg, quality 80
* png -> rgb, png, default quality(=90)
*
* @type {string}
*/
const previewImage = this.settings.addSetting({
id: "Comfy.PreviewFormat",
name: "When displaying a preview in the image widget, convert it to a lightweight image. (webp, jpeg, webp;50, ...)",
type: "string",
defaultValue: "",
});
const fileInput = $el("input", { const fileInput = $el("input", {
id: "comfy-file-input", id: "comfy-file-input",
type: "file", type: "file",

View File

@ -303,7 +303,7 @@ export const ComfyWidgets = {
subfolder = name.substring(0, folder_separator); subfolder = name.substring(0, folder_separator);
name = name.substring(folder_separator + 1); name = name.substring(folder_separator + 1);
} }
img.src = `/view?filename=${name}&type=input&subfolder=${subfolder}`; img.src = `/view?filename=${name}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`;
node.setSizeForImage?.(); node.setSizeForImage?.();
} }