Browse Source

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>
pull/717/merge
Dr.Lt.Data 1 year ago committed by GitHub
parent
commit
9f3a19b728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      server.py
  2. 4
      web/extensions/core/maskeditor.js
  3. 22
      web/scripts/app.js
  4. 19
      web/scripts/ui.js
  5. 2
      web/scripts/widgets.js

22
server.py

@ -217,6 +217,28 @@ class PromptServer():
file = os.path.join(output_dir, filename)
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:
channel = 'rgba'
else:

4
web/extensions/core/maskeditor.js

@ -41,7 +41,7 @@ async function uploadMask(filepath, formData) {
});
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)
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)
alpha_url.searchParams.delete('channel');
alpha_url.searchParams.delete('preview');
alpha_url.searchParams.set('channel', 'a');
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);
rgb_url.searchParams.delete('channel');
rgb_url.searchParams.delete('preview');
rgb_url.searchParams.set('channel', 'rgb');
orig_image.src = rgb_url;
this.image = orig_image;

22
web/scripts/app.js

@ -51,6 +51,14 @@ export class ComfyApp {
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) {
return node.imgs || (node && node.widgets && node.widgets.findIndex(obj => obj.name === 'image') >= 0);
}
@ -231,14 +239,20 @@ export class ComfyApp {
options.unshift(
{
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",
callback: () => {
const a = document.createElement("a");
a.href = img.src;
a.setAttribute("download", new URLSearchParams(new URL(img.src).search).get("filename"));
let url = new URL(img.src);
url.searchParams.delete('preview');
a.href = url;
a.setAttribute("download", new URLSearchParams(url.search).get("filename"));
document.body.append(a);
a.click();
requestAnimationFrame(() => a.remove());
@ -365,7 +379,7 @@ export class ComfyApp {
const img = new Image();
img.onload = () => r(img);
img.onerror = () => r(null);
img.src = "/view?" + new URLSearchParams(src).toString();
img.src = "/view?" + new URLSearchParams(src).toString() + app.getPreviewFormatParam();
});
})
).then((imgs) => {

19
web/scripts/ui.js

@ -462,6 +462,25 @@ export class ComfyUI {
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", {
id: "comfy-file-input",
type: "file",

2
web/scripts/widgets.js

@ -303,7 +303,7 @@ export const ComfyWidgets = {
subfolder = name.substring(0, folder_separator);
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?.();
}

Loading…
Cancel
Save