From c49151495fddd642b6591bc79ff456b87241653a Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 29 Nov 2025 19:39:02 +1100 Subject: [PATCH] new mask editor --- image_filter.py | 20 ++++++++++++++++++++ image_filter_messaging.py | 10 ++++++++-- js/mask_utils.js | 38 +++++++++++++++++++++++++++++++++----- js/popup.js | 17 ++++++++++++----- 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/image_filter.py b/image_filter.py index a2ab1e8..aabf812 100644 --- a/image_filter.py +++ b/image_filter.py @@ -3,6 +3,11 @@ from comfy.model_management import InterruptProcessingException import os, random import torch +import base64 +import io +from PIL import Image +import numpy as np + from .image_filter_messaging import send_and_wait, Response, TimeoutResponse HIDDEN = { @@ -111,6 +116,7 @@ class TextImageFilterWithExtras(PreviewImage): "extra2" : ("STRING", {"default":""}), "extra3" : ("STRING", {"default":""}), "textareaheight" : ("INT", {"default": 150, "min": 50, "max": 500, "tooltip": "Height of text area in pixels"}), + "graph_id": ("STRING", {"default":""}), }, "hidden": HIDDEN, } @@ -155,6 +161,7 @@ class MaskImageFilter(PreviewImage, LoadImage): "extra1" : ("STRING", {"default":""}), "extra2" : ("STRING", {"default":""}), "extra3" : ("STRING", {"default":""}), + "graph_id": ("STRING", {"default":""}), }, "hidden": HIDDEN, } @@ -181,6 +188,19 @@ class MaskImageFilter(PreviewImage, LoadImage): return ( *(self.load_image(os.path.join('clipspace', response.masked_image)+" [input]")), *response.get_extras([extra1, extra2, extra3]) ) except FileNotFoundError: pass + elif (response.masked_data): + data = response.masked_data.split(',',1)[-1] + bytes_data = data.encode('utf-8') + image_data = base64.decodebytes(bytes_data) + data_io = io.BytesIO(image_data) + img = Image.open(data_io) + + mask = np.array(img.getchannel('A')).astype(np.float32) / 255.0 + mask = 1. - torch.from_numpy(mask) + mask = mask.unsqueeze(0) + + return ( image, mask, *response.get_extras([extra1, extra2, extra3]) ) + if if_no_mask == 'cancel': raise InterruptProcessingException() diff --git a/image_filter_messaging.py b/image_filter_messaging.py index 1559977..dd57760 100644 --- a/image_filter_messaging.py +++ b/image_filter_messaging.py @@ -11,11 +11,17 @@ WAITING_FOR_RESPONSE = "-9" SPECIALS = [REQUEST_RESHOW, CANCEL, WAITING_FOR_RESPONSE] class Response: - def __init__(self, selection:Optional[list[str]] = None, text:Optional[str] = None, - masked_image:Optional[str] = None, extras:Optional[list[str]] = None): + def __init__( + self, + selection:Optional[list[str]] = None, + text:Optional[str] = None, + masked_image:Optional[str] = None, + masked_data:Optional[str] = None, + extras:Optional[list[str]] = None): self.selection:list[int] = [int(x) for x in selection] if selection else [] self.text:Optional[str] = text self.masked_image:Optional[str] = masked_image + self.masked_data:Optional[str] = masked_data self.extras:Optional[list[str]] = extras def get_extras(self,defaults:list[str]) -> list[str]: diff --git a/js/mask_utils.js b/js/mask_utils.js index 40715bb..208e732 100644 --- a/js/mask_utils.js +++ b/js/mask_utils.js @@ -5,6 +5,8 @@ export function new_editor() { } function get_mask_editor_element() { + const newer = document.getElementsByClassName('p-dialog-mask') + if (newer.length==1) return newer[0] return new_editor() ? document.getElementById('maskEditor') : document.getElementById('maskCanvas')?.parentElement } @@ -13,17 +15,30 @@ export function mask_editor_showing() { } export function hide_mask_editor() { - if (mask_editor_showing()) document.getElementById('maskEditor').style.display = 'none' + if (mask_editor_showing() && document.getElementById('maskEditor')) document.getElementById('maskEditor').style.display = 'none' } function get_mask_editor_cancel_button() { - if (document.getElementById("maskEditor_topBarCancelButton")) return document.getElementById("maskEditor_topBarCancelButton") - return get_mask_editor_element?.parentElement?.lastChild?.childNodes[2] + var button = document.getElementById("maskEditor_topBarCancelButton") + if (button) return button + try { + button = Array.from(get_mask_editor_element().getElementsByTagName('button')).find((b)=>(b.ariaLabel=='Cancel')) + if (button) return button + } catch {} + + return get_mask_editor_element().parentElement.lastChild.childNodes[2] + } function get_mask_editor_save_button() { - if (document.getElementById("maskEditor_topBarSaveButton")) return document.getElementById("maskEditor_topBarSaveButton") - return get_mask_editor_element?.parentElement?.lastChild?.childNodes[2] + var button = document.getElementById("maskEditor_topBarSaveButton") + if (button) return button + try { + button = Array.from(get_mask_editor_element().getElementsByTagName('button')).find((b)=>(b.ariaLabel=='Save')) + if (button) return button + } catch {} + + return get_mask_editor_element?.parentElement?.lastChild?.childNodes[1] } export function mask_editor_listen_for_cancel(callback) { @@ -40,4 +55,17 @@ export function press_maskeditor_save() { export function press_maskeditor_cancel() { get_mask_editor_cancel_button()?.click() +} + +export function open_maskeditor(node) { + if (ComfyApp.open_maskeditor) { + ComfyApp.copyToClipspace(node) + ComfyApp.clipspace_return_node = node + ComfyApp.open_maskeditor() + } else { + const me_extension = app.extensions.find((e)=>(e.name=='Comfy.MaskEditor')) + const me_command = me_extension.commands.find((c)=>(c.id=='Comfy.MaskEditor.OpenMaskEditor')) + app.canvas.selected_nodes = [node,] + me_command.function() + } } \ No newline at end of file diff --git a/js/popup.js b/js/popup.js index 71dcd9d..07fdfae 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1,7 +1,7 @@ import { app, ComfyApp } from "../../scripts/app.js"; import { api } from "../../scripts/api.js" -import { mask_editor_listen_for_cancel, mask_editor_showing, hide_mask_editor, press_maskeditor_cancel, press_maskeditor_save, new_editor } from "./mask_utils.js"; +import { mask_editor_listen_for_cancel, mask_editor_showing, hide_mask_editor, press_maskeditor_cancel, press_maskeditor_save, new_editor, open_maskeditor } from "./mask_utils.js"; import { Log } from "./log.js"; import { create } from "./utils.js"; import { FloatingWindow } from "./floating_window.js"; @@ -351,13 +351,14 @@ class Popup extends HTMLElement { } else { this.state = State.MASK this.node.imgs = [] + this.node.images = [] detail.urls.forEach((url, i)=>{ this.node.imgs.push( new Image() ); this.node.imgs[i].src = api.apiURL( `/view?filename=${encodeURIComponent(url.filename)}&type=${url.type}&subfolder=${url.subfolder}`) + this.node.images.push( url ) }) - ComfyApp.copyToClipspace(this.node) - ComfyApp.clipspace_return_node = this.node - ComfyApp.open_maskeditor() + this.node.imageIndex = 0 + open_maskeditor(this.node) this.seen_editor = false } setTimeout(this.wait_while_mask_editing.bind(this), 200) @@ -373,7 +374,13 @@ class Popup extends HTMLElement { if (mask_editor_showing()) { setTimeout(this.wait_while_mask_editing.bind(this), 100) } else { - this._send_response({masked_image:this.extract_filename(this.node.imgs[0].src)}) + const masked_image = this.extract_filename(this.node.imgs[0].src) + if (masked_image) { + this._send_response({masked_image:this.extract_filename(this.node.imgs[0].src)}) + } else { + this._send_response({masked_data:this.node.imgs[0].src}) + } + } }