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)