diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/Comfyui_Snap-Processing.iml b/.idea/Comfyui_Snap-Processing.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/Comfyui_Snap-Processing.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..24903c8 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,26 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..dd801c7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Snap_canvas.py b/Snap_canvas.py new file mode 100644 index 0000000..ca9af1b --- /dev/null +++ b/Snap_canvas.py @@ -0,0 +1,159 @@ +from PyQt5.QtCore import QEventLoop +from PyQt5.QtWidgets import QApplication +from PyQt5.QtGui import QImage +import time +import os +import random +import torch +from PIL import Image +import numpy as np +from io import BytesIO +from .ui.canvas_window import CanvasWindow + +class PyQtCanvasNode: + @staticmethod + def INPUT_TYPES(): + return { + "required": { + "image": ("IMAGE",), # Required image input + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + } + } + + RETURN_TYPES = ("IMAGE", "INT", "STRING") # Added "STRING" to the return types + FUNCTION = "activate_pyqt" + CATEGORY = "Snap Processing" + + def activate_pyqt(self, image, seed): + try: + # Use the user-provided seed value + print(f"Received seed: {seed}") + + # Convert tensor to QImage + qimage = self.tensor_to_qimage(image) + print("Converted tensor to QImage") + + # Run PyQt GUI and block the main thread until the user finishes + modified_image = self.run_pyqt_gui(qimage) + print("PyQt GUI closed") + + # Convert the modified QImage to a NumPy array + numpy_array = self.qimage_to_numpy(modified_image) + print("Converted QImage to NumPy array") + + # Define the save directory + save_directory = os.path.join(os.getcwd(), "ComfyUI", "custom_nodes", "Comfyui_Snap-Processing", "save") + if not os.path.exists(save_directory): + os.makedirs(save_directory) + + # Create a unique file name using the seed and current timestamp + timestamp = int(time.time()) + png_image_filename = f"output_image_seed{seed}_{timestamp}.png" + png_image_path = os.path.join(save_directory, png_image_filename) + self.numpy_to_png(numpy_array, png_image_path) + print(f"Saved image to {png_image_path}") + + # Get the absolute path for consistency + absolute_png_image_path = os.path.abspath(png_image_path) + + # Return the modified image, seed, and the file path + return (self.qimage_to_tensor(modified_image), seed, absolute_png_image_path) + except Exception as e: + print(f"Error in activate_pyqt: {e}") + raise e + + def run_pyqt_gui(self, input_image): + try: + app = QApplication.instance() + if app is None: + app = QApplication([]) # Create the global application + + # Always create a new CanvasWindow instance + dialog = CanvasWindow(input_image) + dialog.save_signal.connect(self.on_save) + dialog.close_signal.connect(self.on_close) + + print("PyQt CanvasWindow shown") + dialog.exec_() # Block until the dialog is closed + + print("Dialog closed") + modified_image = dialog.get_modified_image() + dialog.deleteLater() # Ensure the window is properly destroyed + print("CanvasWindow deleted") + return modified_image + except Exception as e: + print(f"Error in run_pyqt_gui: {e}") + raise e + + def on_save(self): + print("Image saved successfully") + + def on_close(self): + print("Window closed") + + def tensor_to_qimage(self, tensor): + if tensor.ndim == 4: + tensor = tensor.squeeze(0) + if tensor.ndim != 3: + raise ValueError(f"Expected tensor with 3 dimensions, but got {tensor.ndim} dimensions.") + + if tensor.shape[0] in [1, 3, 4]: + pass + elif tensor.shape[2] in [1, 3, 4]: + tensor = tensor.permute(2, 0, 1) + else: + raise ValueError("Tensor has unsupported shape.") + + num_channels = tensor.shape[0] + if num_channels == 1: + tensor = tensor.repeat(3, 1, 1) + elif num_channels == 2: + tensor = torch.cat([tensor, tensor[0:1, :, :]], dim=0) + elif num_channels == 4: + tensor = tensor[:3, :, :] + elif num_channels != 3: + raise ValueError(f"Unsupported number of channels: {num_channels}") + + image_data = tensor.mul(255).byte().cpu().numpy() + image_data = image_data.transpose(1, 2, 0) + + pil_image = Image.fromarray(image_data, 'RGB') + qimage = self.pil_image_to_qimage(pil_image) + return qimage + + def qimage_to_tensor(self, qimage): + qimage = qimage.convertToFormat(QImage.Format_RGB888) + width = qimage.width() + height = qimage.height() + + ptr = qimage.bits() + ptr.setsize(qimage.byteCount()) + image_np = np.array(ptr).reshape((height, width, 3)) + + tensor = torch.from_numpy(image_np).permute(2, 0, 1).float() / 255.0 + return tensor + + def qimage_to_numpy(self, qimage): + qimage = qimage.convertToFormat(QImage.Format_RGB888) + width = qimage.width() + height = qimage.height() + + ptr = qimage.bits() + ptr.setsize(qimage.byteCount()) + image_np = np.array(ptr).reshape((height, width, 3)) + + return image_np + + def numpy_to_png(self, numpy_array, save_path): + if numpy_array.dtype != np.uint8: + numpy_array = (numpy_array * 255).astype(np.uint8) + + image = Image.fromarray(numpy_array) + image.save(save_path, format='PNG') + + def pil_image_to_qimage(self, pil_image): + buffer = BytesIO() + pil_image.save(buffer, format="PNG") + image_bytes = buffer.getvalue() + qimage = QImage.fromData(image_bytes, "PNG") + return qimage diff --git a/Snapload.py b/Snapload.py new file mode 100644 index 0000000..4e49a33 --- /dev/null +++ b/Snapload.py @@ -0,0 +1,80 @@ +import torch +import hashlib +from pathlib import Path +from typing import Iterable +from PIL import Image, ImageOps +import numpy as np +import folder_paths +import os # 引入 os 库用于创建目录 + +class Snapload: + @classmethod + def INPUT_TYPES(s): + return {"required": + {"image": ("STRING", {"default": r"123"})}, + } + + CATEGORY = "Snap Processing" + + RETURN_TYPES = ("IMAGE", "MASK") + FUNCTION = "load_image" + + def load_image(self, image): + # 调用 _resolve_path 来获取图像路径 + image_path = Snapload._resolve_path(image) + + # 打开并处理图像 + i = Image.open(image_path) + i = ImageOps.exif_transpose(i) + image = i.convert("RGB") + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + + # 如果有 Alpha 通道,则生成掩码 + if 'A' in i.getbands(): + mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 + mask = 1. - torch.from_numpy(mask) + else: + # 否则生成一个全为零的掩码 + mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + + return (image, mask) + + # 将图像路径解析为相对路径,默认为指定的路径 + def _resolve_path(image) -> Path: + # 如果未提供图像路径,则使用默认路径 + if not image: + default_path = Path("ComfyUI/custom_nodes/Comfyui_Snap-Processing/save/output.png").resolve() + else: + default_path = Path(folder_paths.get_annotated_filepath(image)).resolve() + + # 创建默认保存目录,如果不存在 + save_directory = default_path.parent + if not save_directory.exists(): + os.makedirs(save_directory) + + # 返回默认路径或解析后的路径 + return default_path + + @classmethod + def IS_CHANGED(s, image): + # 获取图像路径并计算其哈希值,判断是否改变 + image_path = Snapload._resolve_path(image) + m = hashlib.sha256() + with open(image_path, 'rb') as f: + m.update(f.read()) + return m.digest().hex() + + @classmethod + def VALIDATE_INPUTS(s, image): + # 如果图像为空,则返回 True + if image is None: + return True + + # 验证图像路径是否存在 + image_path = Snapload._resolve_path(image) + if not image_path.exists(): + return "Invalid image path: {}".format(image_path) + + return True + diff --git a/__init__.py b/__init__.py index f25d75c..1bdef11 100644 --- a/__init__.py +++ b/__init__.py @@ -1,13 +1,19 @@ from .area_calculator import AreaCalculator +from .Snap_canvas import PyQtCanvasNode +from .Snapload import Snapload VERSION = "1.0" NODE_CLASS_MAPPINGS = { "AreaCalculator": AreaCalculator, + "PyQtCanvasNode": PyQtCanvasNode, + "Snapload": Snapload, } NODE_DISPLAY_NAME_MAPPINGS = { "AreaCalculator": "Snap Area", + "PyQtCanvasNode": "Snap Canvas", + "Snapload": "Snap load", } __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/__pycache__/Snap_canvas.cpython-311.pyc b/__pycache__/Snap_canvas.cpython-311.pyc new file mode 100644 index 0000000..b2e2d5d Binary files /dev/null and b/__pycache__/Snap_canvas.cpython-311.pyc differ diff --git a/__pycache__/Snapload.cpython-311.pyc b/__pycache__/Snapload.cpython-311.pyc new file mode 100644 index 0000000..d13256d Binary files /dev/null and b/__pycache__/Snapload.cpython-311.pyc differ diff --git a/__pycache__/__init__.cpython-311.pyc b/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a111703 Binary files /dev/null and b/__pycache__/__init__.cpython-311.pyc differ diff --git a/__pycache__/area_calculator.cpython-311.pyc b/__pycache__/area_calculator.cpython-311.pyc new file mode 100644 index 0000000..bb9f53a Binary files /dev/null and b/__pycache__/area_calculator.cpython-311.pyc differ diff --git a/assets/SnapCanvas.jpg b/assets/SnapCanvas.jpg new file mode 100644 index 0000000..45cd711 Binary files /dev/null and b/assets/SnapCanvas.jpg differ diff --git a/example/SnapCanvas.json b/example/SnapCanvas.json new file mode 100644 index 0000000..f46ab9f --- /dev/null +++ b/example/SnapCanvas.json @@ -0,0 +1,303 @@ +{ + "last_node_id": 17, + "last_link_id": 32, + "nodes": [ + { + "id": 3, + "type": "PreviewImage", + "pos": { + "0": 913.2920532226562, + "1": 353.52850341796875 + }, + "size": { + "0": 210, + "1": 246 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 2, + "label": "图像" + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + }, + { + "id": 2, + "type": "LoadImage", + "pos": { + "0": 307.2921142578125, + "1": 162.52877807617188 + }, + "size": { + "0": 315, + "1": 314.00006103515625 + }, + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 1 + ], + "shape": 3, + "label": "图像" + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "shape": 3, + "label": "遮罩" + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "1.png", + "image" + ] + }, + { + "id": 4, + "type": "Snapload", + "pos": { + "0": 663.2920532226562, + "1": 353.52850341796875 + }, + "size": { + "0": 210, + "1": 54 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "STRING", + "link": 3, + "widget": { + "name": "image" + }, + "label": "image" + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 2 + ], + "slot_index": 0, + "shape": 3, + "label": "IMAGE" + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "shape": 3, + "label": "MASK" + } + ], + "properties": { + "Node name for S&R": "Snapload" + }, + "widgets_values": [ + "123" + ] + }, + { + "id": 1, + "type": "PyQtCanvasNode", + "pos": { + "0": 653.2920532226562, + "1": 163.52877807617188 + }, + "size": { + "0": 315, + "1": 122 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 1, + "slot_index": 0, + "label": "image" + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [], + "slot_index": 0, + "shape": 3, + "label": "IMAGE" + }, + { + "name": "INT", + "type": "INT", + "links": null, + "slot_index": 1, + "shape": 3, + "label": "INT" + }, + { + "name": "STRING", + "type": "STRING", + "links": [ + 3 + ], + "slot_index": 2, + "shape": 3, + "label": "STRING" + } + ], + "properties": { + "Node name for S&R": "PyQtCanvasNode" + }, + "widgets_values": [ + 867571087443497, + "randomize" + ] + } + ], + "links": [ + [ + 1, + 2, + 0, + 1, + 0, + "IMAGE" + ], + [ + 2, + 4, + 0, + 3, + 0, + "IMAGE" + ], + [ + 3, + 1, + 2, + 4, + 0, + "STRING" + ], + [ + 23, + 10, + 2, + 13, + 1, + "VAE" + ], + [ + 24, + 10, + 1, + 15, + 0, + "CLIP" + ], + [ + 25, + 10, + 1, + 16, + 0, + "CLIP" + ], + [ + 26, + 11, + 0, + 5, + 0, + "MODEL" + ], + [ + 27, + 10, + 2, + 5, + 1, + "VAE" + ], + [ + 28, + 10, + 2, + 13, + 1, + "VAE" + ], + [ + 29, + 10, + 1, + 15, + 0, + "CLIP" + ], + [ + 30, + 10, + 1, + 16, + 0, + "CLIP" + ], + [ + 31, + 11, + 0, + 5, + 0, + "MODEL" + ], + [ + 32, + 10, + 2, + 5, + 1, + "VAE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.8769226950000005, + "offset": [ + -155.482269502211, + 130.50066223270278 + ] + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 08ed5ee..300ea5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -torch \ No newline at end of file +pyqt5 diff --git a/save/output.png b/save/output.png new file mode 100644 index 0000000..e5101fd Binary files /dev/null and b/save/output.png differ diff --git a/save/output_image.png b/save/output_image.png new file mode 100644 index 0000000..b5f736e Binary files /dev/null and b/save/output_image.png differ diff --git a/save/output_image_seed102188696742965_1727592804.png b/save/output_image_seed102188696742965_1727592804.png new file mode 100644 index 0000000..f975c12 Binary files /dev/null and b/save/output_image_seed102188696742965_1727592804.png differ diff --git a/save/output_image_seed165231885671399_1727680395.png b/save/output_image_seed165231885671399_1727680395.png new file mode 100644 index 0000000..1a9ee45 Binary files /dev/null and b/save/output_image_seed165231885671399_1727680395.png differ diff --git a/save/output_image_seed341874800874179_1727592868.png b/save/output_image_seed341874800874179_1727592868.png new file mode 100644 index 0000000..4862458 Binary files /dev/null and b/save/output_image_seed341874800874179_1727592868.png differ diff --git a/save/output_image_seed364149889408431_1727679313.png b/save/output_image_seed364149889408431_1727679313.png new file mode 100644 index 0000000..bd994ee Binary files /dev/null and b/save/output_image_seed364149889408431_1727679313.png differ diff --git a/save/output_image_seed473256537738175_1727592751.png b/save/output_image_seed473256537738175_1727592751.png new file mode 100644 index 0000000..d753361 Binary files /dev/null and b/save/output_image_seed473256537738175_1727592751.png differ diff --git a/save/output_image_seed518072437279080_1727592788.png b/save/output_image_seed518072437279080_1727592788.png new file mode 100644 index 0000000..990c729 Binary files /dev/null and b/save/output_image_seed518072437279080_1727592788.png differ diff --git a/save/output_image_seed554760322076771_1727679945.png b/save/output_image_seed554760322076771_1727679945.png new file mode 100644 index 0000000..fe9cbd5 Binary files /dev/null and b/save/output_image_seed554760322076771_1727679945.png differ diff --git a/save/output_image_seed575326176570079_1727593596.png b/save/output_image_seed575326176570079_1727593596.png new file mode 100644 index 0000000..bd9d9ec Binary files /dev/null and b/save/output_image_seed575326176570079_1727593596.png differ diff --git a/save/output_image_seed612536980664326_1727592812.png b/save/output_image_seed612536980664326_1727592812.png new file mode 100644 index 0000000..f7d26fa Binary files /dev/null and b/save/output_image_seed612536980664326_1727592812.png differ diff --git a/save/output_image_seed688705875868338_1727681556.png b/save/output_image_seed688705875868338_1727681556.png new file mode 100644 index 0000000..7b20442 Binary files /dev/null and b/save/output_image_seed688705875868338_1727681556.png differ diff --git a/save/output_image_seed703932794060439_1727593988.png b/save/output_image_seed703932794060439_1727593988.png new file mode 100644 index 0000000..c938c7d Binary files /dev/null and b/save/output_image_seed703932794060439_1727593988.png differ diff --git a/save/output_image_seed727213827209089_1727593767.png b/save/output_image_seed727213827209089_1727593767.png new file mode 100644 index 0000000..0e70400 Binary files /dev/null and b/save/output_image_seed727213827209089_1727593767.png differ diff --git a/save/output_image_seed75858144998317_1727592796.png b/save/output_image_seed75858144998317_1727592796.png new file mode 100644 index 0000000..6cd7ae6 Binary files /dev/null and b/save/output_image_seed75858144998317_1727592796.png differ diff --git a/save/output_image_seed808646150378520_1727594070.png b/save/output_image_seed808646150378520_1727594070.png new file mode 100644 index 0000000..783d9ad Binary files /dev/null and b/save/output_image_seed808646150378520_1727594070.png differ diff --git a/save/output_image_seed857148206117389_1727679325.png b/save/output_image_seed857148206117389_1727679325.png new file mode 100644 index 0000000..f89fbde Binary files /dev/null and b/save/output_image_seed857148206117389_1727679325.png differ diff --git a/save/output_image_seed92050565502526_1727593606.png b/save/output_image_seed92050565502526_1727593606.png new file mode 100644 index 0000000..27cb025 Binary files /dev/null and b/save/output_image_seed92050565502526_1727593606.png differ diff --git a/ui/__pycache__/__init__.cpython-311.pyc b/ui/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..cbfa077 Binary files /dev/null and b/ui/__pycache__/__init__.cpython-311.pyc differ diff --git a/ui/__pycache__/canvas_window.cpython-311.pyc b/ui/__pycache__/canvas_window.cpython-311.pyc new file mode 100644 index 0000000..7266110 Binary files /dev/null and b/ui/__pycache__/canvas_window.cpython-311.pyc differ diff --git a/ui/__pycache__/resizable_pixmap_item.cpython-311.pyc b/ui/__pycache__/resizable_pixmap_item.cpython-311.pyc new file mode 100644 index 0000000..fccc785 Binary files /dev/null and b/ui/__pycache__/resizable_pixmap_item.cpython-311.pyc differ diff --git a/ui/canvas_window.py b/ui/canvas_window.py new file mode 100644 index 0000000..6126d5e --- /dev/null +++ b/ui/canvas_window.py @@ -0,0 +1,226 @@ +import os +import time +from PyQt5.QtWidgets import ( + QDialog, QPushButton, QVBoxLayout, QLineEdit, QHBoxLayout, + QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsItem +) +from PyQt5.QtGui import QPixmap, QImage, QColor, QPainter +from PyQt5.QtCore import pyqtSignal, Qt, QRectF + + +class CanvasWindow(QDialog): + save_signal = pyqtSignal() + close_signal = pyqtSignal() + + def __init__(self, input_image): + super().__init__() + + self.setWindowTitle("PyQt5 Image Canvas") + self.input_image = input_image + self.canvas_width = 512 + self.canvas_height = 512 + self.modified_image = None + + self.scene = QGraphicsScene() + self.view = QGraphicsView(self.scene) + self.view.setStyleSheet("border: 1px solid black;") + self.view.setFixedSize(self.canvas_width + 2, self.canvas_height + 2) + + self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + self.canvas = QImage(self.canvas_width, self.canvas_height, QImage.Format_RGB888) + self.canvas.fill(QColor('white')) + + self.canvas_pixmap_item = QGraphicsPixmapItem(QPixmap.fromImage(self.canvas)) + self.scene.addItem(self.canvas_pixmap_item) + + scaled_pixmap = QPixmap.fromImage(self.input_image).scaled( + self.canvas_width, self.canvas_height, Qt.KeepAspectRatio, Qt.SmoothTransformation + ) + self.input_pixmap_item = ResizablePixmapItem(scaled_pixmap, self.canvas_width, self.canvas_height) + self.scene.addItem(self.input_pixmap_item) + + save_button = QPushButton("Save and Close", self) + save_button.clicked.connect(self.save_and_close) + + self.canvas_width_input = QLineEdit(self) + self.canvas_width_input.setPlaceholderText("Canvas Width") + self.canvas_width_input.setText(str(self.canvas_width)) + self.canvas_height_input = QLineEdit(self) + self.canvas_height_input.setPlaceholderText("Canvas Height") + self.canvas_height_input.setText(str(self.canvas_height)) + + size_button = QPushButton("Set Canvas Size", self) + size_button.clicked.connect(self.set_canvas_size) + + hbox = QHBoxLayout() + hbox.addWidget(self.canvas_width_input) + hbox.addWidget(self.canvas_height_input) + hbox.addWidget(size_button) + + vbox = QVBoxLayout() + vbox.addWidget(self.view) + vbox.addLayout(hbox) + vbox.addWidget(save_button) + + self.setLayout(vbox) + + self.scene.setSceneRect(0, 0, self.canvas_width, self.canvas_height) + self.view.setSceneRect(0, 0, self.canvas_width, self.canvas_height) + + def save_and_close(self): + self.save_image() + self.accept() + + def save_image(self): + image = QImage(self.canvas_width, self.canvas_height, QImage.Format_RGB888) + image.fill(QColor('white')) + + painter = QPainter(image) + target_rect = QRectF(0, 0, self.canvas_width, self.canvas_height) + source_rect = self.scene.sceneRect() + self.scene.render(painter, target_rect, source_rect) + painter.end() + + self.modified_image = image + + save_directory = os.path.join(os.getcwd(), "ComfyUI", "custom_nodes", "Comfyui_Snap-Processing", "save") + if not os.path.exists(save_directory): + os.makedirs(save_directory) + + timestamp = int(time.time() * 1000) + save_path = os.path.join(save_directory, "output.png") + image.save(save_path) + self.save_signal.emit() + + def closeEvent(self, event): + self.close_signal.emit() + event.accept() + + def set_canvas_size(self): + try: + width = int(self.canvas_width_input.text()) + height = int(self.canvas_height_input.text()) + except ValueError: + return + + self.canvas_width = width + self.canvas_height = height + + self.canvas = QImage(self.canvas_width, self.canvas_height, QImage.Format_RGB888) + self.canvas.fill(QColor('white')) + self.canvas_pixmap_item.setPixmap(QPixmap.fromImage(self.canvas)) + + self.scene.setSceneRect(0, 0, self.canvas_width, self.canvas_height) + self.view.setSceneRect(0, 0, self.canvas_width, self.canvas_height) + self.view.setFixedSize(self.canvas_width + 2, self.canvas_height + 2) + + def get_modified_image(self): + return self.modified_image + + +class ResizablePixmapItem(QGraphicsPixmapItem): + def __init__(self, pixmap, canvas_width, canvas_height): + super().__init__(pixmap) + self.canvas_width = canvas_width + self.canvas_height = canvas_height + self.original_pixmap = pixmap + self.setFlags( + QGraphicsItem.ItemIsSelectable | + QGraphicsItem.ItemIsMovable | + QGraphicsItem.ItemSendsGeometryChanges + ) + self.setAcceptHoverEvents(True) + self.is_resizing = False + self.resize_handle_size = 10 + self.aspect_ratio = pixmap.width() / pixmap.height() + self.setCursor(Qt.ArrowCursor) + self.min_scale = 0.1 + self.max_scale = 10 + + def hoverMoveEvent(self, event): + if self.is_in_resize_area(event.pos()): + self.setCursor(Qt.SizeFDiagCursor) + else: + self.setCursor(Qt.ArrowCursor) + super().hoverMoveEvent(event) + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton and self.is_in_resize_area(event.pos()): + self.is_resizing = True + self.resize_start_pos = event.pos() + event.accept() + else: + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + if self.is_resizing: + offset = event.pos() - self.resize_start_pos + self.resize_image(offset) + else: + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + if self.is_resizing: + self.is_resizing = False + event.accept() + else: + super().mouseReleaseEvent(event) + + def boundingRect(self): + return QRectF(0, 0, self.pixmap().width(), self.pixmap().height()) + + def resize_image(self, offset): + scale_factor = 0.1 + new_width = self.boundingRect().width() + offset.x() * scale_factor + new_height = new_width / self.aspect_ratio + + self.prepareGeometryChange() + + if 10 < new_width <= self.canvas_width * 2 and 10 < new_height <= self.canvas_height * 2: + scaled_pixmap = self.original_pixmap.scaled( + int(new_width), int(new_height), + Qt.KeepAspectRatio, Qt.SmoothTransformation + ) + self.setPixmap(scaled_pixmap) + + self.update() + + self.prepareGeometryChange() + + def is_in_resize_area(self, pos): + rect = self.boundingRect() + resize_rect = QRectF( + rect.right() - self.resize_handle_size, + rect.bottom() - self.resize_handle_size, + self.resize_handle_size, + self.resize_handle_size + ) + return resize_rect.contains(pos) + + def wheelEvent(self, event): + try: + if hasattr(event, 'angleDelta'): + delta = event.angleDelta().y() + elif hasattr(event, 'delta'): + delta = event.delta() + else: + delta = 0 + + factor = 1.1 if delta > 0 else 0.9 + + self.resize_with_factor(factor) + except AttributeError: + pass + + def resize_with_factor(self, factor): + new_width = self.boundingRect().width() * factor + new_height = new_width / self.aspect_ratio + + if self.min_scale * self.canvas_width <= new_width <= self.max_scale * self.canvas_width and \ + self.min_scale * self.canvas_height <= new_height <= self.max_scale * self.canvas_height: + scaled_pixmap = self.original_pixmap.scaled( + int(new_width), int(new_height), Qt.KeepAspectRatio, Qt.SmoothTransformation + ) + self.setPixmap(scaled_pixmap)