diff --git a/sdppp_python/comfy_nodes.py b/sdppp_python/comfy_nodes.py index a9c44e8..e584b2e 100644 --- a/sdppp_python/comfy_nodes.py +++ b/sdppp_python/comfy_nodes.py @@ -1,177 +1,177 @@ -import numpy as np -import time -from nodes import LoadImage -from PIL import Image -from .apis import addImageCache - -def define_comfyui_nodes(sdppp): - def validate_sdppp(): - if not sdppp.has_ps_instance(): - return 'Photoshop is not connected' - return True - - def call_async_func_in_server_thread(coro, dontwait = False): - handle = { - 'done': False, - 'result': None, - 'error': None - } - loop = sdppp.loop - async def do_call(): - try: - handle['result'] = await coro - except Exception as e: - handle['error'] = e - finally: - handle['done'] = True - loop.create_task(do_call()) - - if not dontwait: - while not handle['done']: - pass - if handle['error'] is not None: - raise handle['error'] - else: - return handle['result'] - else: - return None - - - class GetImageFromPhotoshopLayerNode: - @classmethod - def VALIDATE_INPUTS(s, layer): - return validate_sdppp() - - @classmethod - def IS_CHANGED(self, layer, use_layer_bounds): - if not sdppp.has_ps_instance(): - return np.random.rand() - else: - photoshopInstance = sdppp.get_ps_instance() - id = photoshopInstance.layer_name_to_id(layer) - bounds_id = photoshopInstance.layer_name_to_id(use_layer_bounds, id) - start = time.time() - is_changed, history_state_id = call_async_func_in_server_thread(photoshopInstance.check_layer_bounds_combo_changed(id, bounds_id)) - if is_changed and history_state_id is None: - return np.random.rand() - photoshopInstance.update_comfyui_last_value(id, bounds_id, history_state_id) - return history_state_id - - @classmethod - def INPUT_TYPES(cls): - if validate_sdppp() is not True: - layer_strs = [] - bounds_strs = [] - else: - layer_strs = sdppp.get_ps_instance().get_base_layers() - bounds_strs = sdppp.get_ps_instance().get_bounds_layers() - - return { - "required": { - "layer": (layer_strs, {"default": layer_strs[0] if len(layer_strs) > 0 else None}), - "use_layer_bounds": (bounds_strs, {"default": bounds_strs[0] if len(bounds_strs) > 0 else None}), - } - } - - RETURN_TYPES = ("IMAGE", "MASK", "FLOAT") - RETURN_NAMES = ("image_out", "mask_out", "layer_opacity") - FUNCTION = "get_image" - CATEGORY = "Photoshop" - - def get_image(self, layer, use_layer_bounds): - if validate_sdppp() is not True: - raise ValueError('Photoshop is not connected') - photoshopInstance = sdppp.get_ps_instance() - - id = photoshopInstance.layer_name_to_id(layer) - bounds_id = photoshopInstance.layer_name_to_id(use_layer_bounds, id) - - start = time.time() - image_id, layer_opacity = call_async_func_in_server_thread(photoshopInstance.get_image(layer_id=id, bounds_id=bounds_id)) - - loadImage = LoadImage() - (output_image, output_mask) = loadImage.load_image(image_id) - return (output_image, output_mask, layer_opacity / 100) - - - class SendImageToPhotoshopLayerNode: - @classmethod - def VALIDATE_INPUTS(s, images): - return validate_sdppp() - - @classmethod - def INPUT_TYPES(cls): - if validate_sdppp() is not True: - layer_strs = [] - else: - layer_strs = sdppp.get_ps_instance().get_set_layers() - - return { - "required": { - "images": ("IMAGE", ), - "layer": (layer_strs, {"default": layer_strs[0] if len(layer_strs) > 0 else None}), - } - } - - RETURN_TYPES = () - FUNCTION = "send_image" - CATEGORY = "Photoshop" - OUTPUT_NODE = True - - def send_image(self, images, layer): - if validate_sdppp() is not True: - raise ValueError('Photoshop is not connected') - photoshopInstance = sdppp.get_ps_instance() - - image_ids = [] - for (batch_number, image) in enumerate(images): - i = 255. * image.cpu().numpy() - img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - image_ids.append(addImageCache(img)) - layer_id = photoshopInstance.layer_name_to_id(layer) - call_async_func_in_server_thread(photoshopInstance.send_images(image_ids=image_ids, layer_id=layer_id), True) - - return (None,) - - class ImageTimesOpacity: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "images": ("IMAGE", ), - "opacity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_times_opacity" - CATEGORY = "Photoshop" - - def image_times_opacity(self, images, opacity): - image_out = images * opacity - return (image_out,) - - class MaskTimesOpacity: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "masks": ("MASK", ), - "opacity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}), - } - } - - RETURN_TYPES = ("MASK",) - FUNCTION = "mask_times_opacity" - CATEGORY = "Photoshop" - - def mask_times_opacity(self, masks, opacity): - mask_out = masks * opacity - return (mask_out,) - - return { - 'GetImageFromPhotoshopLayerNode': GetImageFromPhotoshopLayerNode, - 'SendImageToPhotoshopLayerNode': SendImageToPhotoshopLayerNode, - 'ImageTimesOpacity': ImageTimesOpacity, - 'MaskTimesOpacity': MaskTimesOpacity, +import numpy as np +import time +from nodes import LoadImage +from PIL import Image +from .apis import addImageCache + +def define_comfyui_nodes(sdppp): + def validate_sdppp(): + if not sdppp.has_ps_instance(): + return 'Photoshop is not connected' + return True + + def call_async_func_in_server_thread(coro, dontwait = False): + handle = { + 'done': False, + 'result': None, + 'error': None + } + loop = sdppp.loop + async def do_call(): + try: + handle['result'] = await coro + except Exception as e: + handle['error'] = e + finally: + handle['done'] = True + loop.create_task(do_call()) + + if not dontwait: + while not handle['done']: + pass + if handle['error'] is not None: + raise handle['error'] + else: + return handle['result'] + else: + return None + + + class GetImageFromPhotoshopLayerNode: + @classmethod + def VALIDATE_INPUTS(s, layer): + return validate_sdppp() + + @classmethod + def IS_CHANGED(self, layer, use_layer_bounds): + if not sdppp.has_ps_instance(): + return np.random.rand() + else: + photoshopInstance = sdppp.get_ps_instance() + id = photoshopInstance.layer_name_to_id(layer) + bounds_id = photoshopInstance.layer_name_to_id(use_layer_bounds, id) + start = time.time() + is_changed, history_state_id = call_async_func_in_server_thread(photoshopInstance.check_layer_bounds_combo_changed(id, bounds_id)) + if is_changed and history_state_id is None: + return np.random.rand() + photoshopInstance.update_comfyui_last_value(id, bounds_id, history_state_id) + return history_state_id + + @classmethod + def INPUT_TYPES(cls): + if validate_sdppp() is not True: + layer_strs = [] + bounds_strs = [] + else: + layer_strs = sdppp.get_ps_instance().get_base_layers() + bounds_strs = sdppp.get_ps_instance().get_bounds_layers() + + return { + "required": { + "layer": (layer_strs, {"default": layer_strs[0] if len(layer_strs) > 0 else None}), + "use_layer_bounds": (bounds_strs, {"default": bounds_strs[0] if len(bounds_strs) > 0 else None}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "FLOAT") + RETURN_NAMES = ("image_out", "mask_out", "layer_opacity") + FUNCTION = "get_image" + CATEGORY = "Photoshop" + + def get_image(self, layer, use_layer_bounds): + if validate_sdppp() is not True: + raise ValueError('Photoshop is not connected') + photoshopInstance = sdppp.get_ps_instance() + + id = photoshopInstance.layer_name_to_id(layer) + bounds_id = photoshopInstance.layer_name_to_id(use_layer_bounds, id) + + start = time.time() + image_id, layer_opacity = call_async_func_in_server_thread(photoshopInstance.get_image(layer_id=id, bounds_id=bounds_id)) + + loadImage = LoadImage() + (output_image, output_mask) = loadImage.load_image(image_id) + return (output_image, output_mask, layer_opacity / 100) + + + class SendImageToPhotoshopLayerNode: + @classmethod + def VALIDATE_INPUTS(s, images): + return validate_sdppp() + + @classmethod + def INPUT_TYPES(cls): + if validate_sdppp() is not True: + layer_strs = [] + else: + layer_strs = sdppp.get_ps_instance().get_set_layers() + + return { + "required": { + "images": ("IMAGE", ), + "layer": (layer_strs, {"default": layer_strs[0] if len(layer_strs) > 0 else None}), + } + } + + RETURN_TYPES = () + FUNCTION = "send_image" + CATEGORY = "Photoshop" + OUTPUT_NODE = True + + def send_image(self, images, layer): + if validate_sdppp() is not True: + raise ValueError('Photoshop is not connected') + photoshopInstance = sdppp.get_ps_instance() + + image_ids = [] + for (batch_number, image) in enumerate(images): + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + image_ids.append(addImageCache(img)) + layer_id = photoshopInstance.layer_name_to_id(layer) + call_async_func_in_server_thread(photoshopInstance.send_images(image_ids=image_ids, layer_id=layer_id), True) + + return (None,) + + class ImageTimesOpacity: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE", ), + "opacity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "image_times_opacity" + CATEGORY = "Photoshop" + + def image_times_opacity(self, images, opacity): + image_out = images * opacity + return (image_out,) + + class MaskTimesOpacity: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "masks": ("MASK", ), + "opacity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("MASK",) + FUNCTION = "mask_times_opacity" + CATEGORY = "Photoshop" + + def mask_times_opacity(self, masks, opacity): + mask_out = masks * opacity + return (mask_out,) + + return { + 'GetImageFromPhotoshopLayerNode': GetImageFromPhotoshopLayerNode, + 'SendImageToPhotoshopLayerNode': SendImageToPhotoshopLayerNode, + 'ImageTimesOpacity': ImageTimesOpacity, + 'MaskTimesOpacity': MaskTimesOpacity, } \ No newline at end of file diff --git a/sdppp_python/photoshop_instance.py b/sdppp_python/photoshop_instance.py index ed58db7..a18ac7e 100644 --- a/sdppp_python/photoshop_instance.py +++ b/sdppp_python/photoshop_instance.py @@ -1,121 +1,121 @@ -import time - -class PhotoshopInstance: - SPECIAL_LAYER_NEW_LAYER = '### New Layer ###' - SPECIAL_LAYER_USE_CANVAS = '### Use Canvas ###' - SPECIAL_LAYER_USE_SELECTION = '### Use Selection ###' - SPECIAL_LAYER_SAME_AS_LAYER = '### Same as Layer ###' - SPECIAL_LAYER_NAME_TO_ID = { - SPECIAL_LAYER_USE_CANVAS: 0, - SPECIAL_LAYER_USE_SELECTION: -1, - SPECIAL_LAYER_NEW_LAYER: -2, - SPECIAL_LAYER_SAME_AS_LAYER: -3 - } - - def __init__(self, sdppp, sid): - self.sdppp = sdppp - self.sid = sid - self.layers = [] - self.reset_change_tracker() - - def get_layers(self): - return list(map(lambda layer: f"{layer['name']} (id:{layer['id']})", self.layers)) - - def get_base_layers(self): - raw_layer_strs = self.get_layers() - layer_strs = list(raw_layer_strs) - layer_strs.insert(0, self.SPECIAL_LAYER_USE_CANVAS) - return layer_strs - - def get_bounds_layers(self): - bounds_strs = list(self.get_layers()) - bounds_strs.insert(0, self.SPECIAL_LAYER_USE_CANVAS) - bounds_strs.insert(1, self.SPECIAL_LAYER_USE_SELECTION) - bounds_strs.insert(1, self.SPECIAL_LAYER_SAME_AS_LAYER) - return bounds_strs - - def get_set_layers(self): - raw_layer_strs = self.get_layers() - set_layer_strs = list(raw_layer_strs) - set_layer_strs.insert(0, self.SPECIAL_LAYER_NEW_LAYER) - return set_layer_strs - - def layer_name_to_id(self, layer_name, refrence_id=None): - id = 0 - if layer_name == self.SPECIAL_LAYER_SAME_AS_LAYER: - id = refrence_id - elif self.SPECIAL_LAYER_NAME_TO_ID.get(layer_name, None) is not None: - id = self.SPECIAL_LAYER_NAME_TO_ID[layer_name] - else: - layer_name_and_id_split = layer_name.split('(id:') - id = int(layer_name_and_id_split.pop().strip()[:-1]) - return id - - async def get_image(self, layer_id, bounds_id=False): - result = await self.sdppp.sio.call('get_image', data={'layer_id': layer_id, 'use_layer_bounds': bounds_id}, to=self.sid) - if not result: - return None, None - if 'error' in result: - raise Exception('sdppp PS side error:' + result['error']) - history_state_id = await self._update_layer_bounds_history_state_id(layer_id, bounds_id) - await self._update_history_state_id_after_internal_change(history_state_id) - - return result['upload_name'], result['layer_opacity'] - - async def send_images(self, image_ids, layer_id=""): - result = await self.sdppp.sio.call('send_images', data={'image_ids': image_ids, 'layer_id': layer_id}, to=self.sid) - await self._update_history_state_id_after_internal_change() - return result - - async def get_active_history_state_id(self): - result = await self.sdppp.sio.call('get_active_history_state_id', data={}, to=self.sid) - if result is None: - return 0 - if 'error' in result: - raise Exception('sdppp get_active_history_state_id PS side error:' + result['error']) - id = result.get('history_state_id', 0) - return id - - - # === - def reset_change_tracker(self): - self.get_img_state_id = 0 - self.change_tracker = {} - self.comfyui_last_value_tracker = {} - - # for apis calls - async def is_ps_history_changed(self): - current_id = await self.get_active_history_state_id() - return self.get_img_state_id != current_id and self.get_img_state_id < current_id - - async def check_layer_bounds_combo_changed(self, layer, use_layer_bounds): - layer_bounds_combo = f"{layer}{use_layer_bounds}" - layer_bounds_history_state_id = self.change_tracker.get(layer_bounds_combo, None) - if layer_bounds_history_state_id is None: - return True - latest_state_id = await self.get_active_history_state_id() - if latest_state_id > layer_bounds_history_state_id: - return True, latest_state_id - #didn't change, use last value - comfyui_last_value = self.comfyui_last_value_tracker.get(layer_bounds_combo, None) or layer_bounds_history_state_id - return False, comfyui_last_value - - # have to track this value because comfyui determines if the value is changed by comparing it with the last value. - # can't use the real history id because sending images changes the history id, and comfyui will think the value is changed when it's not - def update_comfyui_last_value(self, layer, use_layer_bounds, value): - layer_bounds_combo = f"{layer}{use_layer_bounds}" - self.comfyui_last_value_tracker[layer_bounds_combo] = value - - # need to update history id after internal operation otherwise it might cause infinite change loop - async def _update_history_state_id_after_internal_change(self, history_state_id=None): - if history_state_id is None: # need to get new id after operation, it causes history change - history_state_id = await self.get_active_history_state_id() - if self.get_img_state_id is not None: # udpate all the change trackers with the new history state id - self.change_tracker = {k: history_state_id for k, v in self.change_tracker.items() if v == self.get_img_state_id} - self.get_img_state_id = history_state_id # it gets into a loop if get img state is not updated - - async def _update_layer_bounds_history_state_id(self, layer, use_layer_bounds): - layer_bounds_combo = f"{layer}{use_layer_bounds}" - latest_state_id = await self.get_active_history_state_id() - self.change_tracker[layer_bounds_combo] = latest_state_id +import time + +class PhotoshopInstance: + SPECIAL_LAYER_NEW_LAYER = '### New Layer ###' + SPECIAL_LAYER_USE_CANVAS = '### Use Canvas ###' + SPECIAL_LAYER_USE_SELECTION = '### Use Selection ###' + SPECIAL_LAYER_SAME_AS_LAYER = '### Same as Layer ###' + SPECIAL_LAYER_NAME_TO_ID = { + SPECIAL_LAYER_USE_CANVAS: 0, + SPECIAL_LAYER_USE_SELECTION: -1, + SPECIAL_LAYER_NEW_LAYER: -2, + SPECIAL_LAYER_SAME_AS_LAYER: -3 + } + + def __init__(self, sdppp, sid): + self.sdppp = sdppp + self.sid = sid + self.layers = [] + self.reset_change_tracker() + + def get_layers(self): + return list(map(lambda layer: f"{layer['name']} (id:{layer['id']})", self.layers)) + + def get_base_layers(self): + raw_layer_strs = self.get_layers() + layer_strs = list(raw_layer_strs) + layer_strs.insert(0, self.SPECIAL_LAYER_USE_CANVAS) + return layer_strs + + def get_bounds_layers(self): + bounds_strs = list(self.get_layers()) + bounds_strs.insert(0, self.SPECIAL_LAYER_USE_CANVAS) + bounds_strs.insert(1, self.SPECIAL_LAYER_USE_SELECTION) + bounds_strs.insert(1, self.SPECIAL_LAYER_SAME_AS_LAYER) + return bounds_strs + + def get_set_layers(self): + raw_layer_strs = self.get_layers() + set_layer_strs = list(raw_layer_strs) + set_layer_strs.insert(0, self.SPECIAL_LAYER_NEW_LAYER) + return set_layer_strs + + def layer_name_to_id(self, layer_name, refrence_id=None): + id = 0 + if layer_name == self.SPECIAL_LAYER_SAME_AS_LAYER: + id = refrence_id + elif self.SPECIAL_LAYER_NAME_TO_ID.get(layer_name, None) is not None: + id = self.SPECIAL_LAYER_NAME_TO_ID[layer_name] + else: + layer_name_and_id_split = layer_name.split('(id:') + id = int(layer_name_and_id_split.pop().strip()[:-1]) + return id + + async def get_image(self, layer_id, bounds_id=False): + result = await self.sdppp.sio.call('get_image', data={'layer_id': layer_id, 'use_layer_bounds': bounds_id}, to=self.sid) + if not result: + return None, None + if 'error' in result: + raise Exception('sdppp PS side error:' + result['error']) + history_state_id = await self._update_layer_bounds_history_state_id(layer_id, bounds_id) + await self._update_history_state_id_after_internal_change(history_state_id) + + return result['upload_name'], result['layer_opacity'] + + async def send_images(self, image_ids, layer_id=""): + result = await self.sdppp.sio.call('send_images', data={'image_ids': image_ids, 'layer_id': layer_id}, to=self.sid) + await self._update_history_state_id_after_internal_change() + return result + + async def get_active_history_state_id(self): + result = await self.sdppp.sio.call('get_active_history_state_id', data={}, to=self.sid) + if result is None: + return 0 + if 'error' in result: + raise Exception('sdppp get_active_history_state_id PS side error:' + result['error']) + id = result.get('history_state_id', 0) + return id + + + # === + def reset_change_tracker(self): + self.get_img_state_id = 0 + self.change_tracker = {} + self.comfyui_last_value_tracker = {} + + # for apis calls + async def is_ps_history_changed(self): + current_id = await self.get_active_history_state_id() + return self.get_img_state_id != current_id and self.get_img_state_id < current_id + + async def check_layer_bounds_combo_changed(self, layer, use_layer_bounds): + layer_bounds_combo = f"{layer}{use_layer_bounds}" + layer_bounds_history_state_id = self.change_tracker.get(layer_bounds_combo, None) + if layer_bounds_history_state_id is None: + return True + latest_state_id = await self.get_active_history_state_id() + if latest_state_id > layer_bounds_history_state_id: + return True, latest_state_id + #didn't change, use last value + comfyui_last_value = self.comfyui_last_value_tracker.get(layer_bounds_combo, None) or layer_bounds_history_state_id + return False, comfyui_last_value + + # have to track this value because comfyui determines if the value is changed by comparing it with the last value. + # can't use the real history id because sending images changes the history id, and comfyui will think the value is changed when it's not + def update_comfyui_last_value(self, layer, use_layer_bounds, value): + layer_bounds_combo = f"{layer}{use_layer_bounds}" + self.comfyui_last_value_tracker[layer_bounds_combo] = value + + # need to update history id after internal operation otherwise it might cause infinite change loop + async def _update_history_state_id_after_internal_change(self, history_state_id=None): + if history_state_id is None: # need to get new id after operation, it causes history change + history_state_id = await self.get_active_history_state_id() + if self.get_img_state_id is not None: # udpate all the change trackers with the new history state id + self.change_tracker = {k: history_state_id for k, v in self.change_tracker.items() if v == self.get_img_state_id} + self.get_img_state_id = history_state_id # it gets into a loop if get img state is not updated + + async def _update_layer_bounds_history_state_id(self, layer, use_layer_bounds): + layer_bounds_combo = f"{layer}{use_layer_bounds}" + latest_state_id = await self.get_active_history_state_id() + self.change_tracker[layer_bounds_combo] = latest_state_id return latest_state_id \ No newline at end of file