diff --git a/README.MD b/README.MD
index 0679b8a..1e3def1 100644
--- a/README.MD
+++ b/README.MD
@@ -70,6 +70,7 @@ When this error has occurred, please check the network environment.
## Update
**If the dependency package error after updating, please reinstall the relevant dependency packages.
+* Commit [LoadPSD](#LoadPSD) node, It read the psd format, and output layer images.
* Commit [SegformerB2ClothesUltra](#SegformerB2ClothesUltra) node, it used to segment character clothing. The model segmentation code is from[StartHua](https://github.com/StartHua/Comfyui_segformer_b2_clothes), thanks to the original author.
* [SaveImagePlus](#SaveImagePlus) node adds the output workflow to the json function, supports ```%date``` and ```%time``` to embeddint date or time to path and filename, and adds the preview switch.
* Commit [SaveImagePlus](#SaveImagePlus) node,It can customize the directory where the picture is saved, add a timestamp to the file name, select the save format, set the image compression rate, set whether to save the workflow, and optionally add invisible watermarks to the picture.
@@ -1152,6 +1153,23 @@ Node Options:
* image: The input QR code image.
* pre_blur: Pre-blurring, you can try to adjust this value for QR codes that are difficult to identify.
+### LoadPSD
+
+Load the PSD format file and export the layers.
+
+Node Options:
+
+* image: Here is a list of *.psd files under ```ComfyUI/input```, where previously loaded psd images can be selected.
+* file_path: The complete path and file name of the psd file.
+* include_hidden_layer: whether include hidden layers.
+* find_layer_by: The method for finding layers can be selected by layer key number or layer name. Layer groups are treated as one layer.
+* layer_index: The layer key number, where 0 is the bottom layer, is incremented sequentially. If include_hiddenlayer is set to false, hidden layers are not counted. Set to -1 to output the top layer.
+* layer_name: Layer name. Note that capitalization and punctuation must match exactly.
+
+Outputs:
+flat_image: PSD preview image.
+layer_iamge: Find the layer output.
+all_layers: Batch images containing all layers.
# LayerMask
diff --git a/README_CN.MD b/README_CN.MD
index d57fba7..2e71152 100644
--- a/README_CN.MD
+++ b/README_CN.MD
@@ -70,6 +70,7 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git
## 更新说明
**如果本插件更新后出现依赖包错误,请重新安装相关依赖包。
+* 添加 [LoadPSD](#LoadPSD) 节点, 读取psd格式并输出图层图片。
* 添加 [SegformerB2ClothesUltra](#SegformerB2ClothesUltra)节点,用于分割人物服装。模型分割代码来自[StartHua](https://github.com/StartHua/Comfyui_segformer_b2_clothes),感谢原作者。
* [SaveImagePlus](#SaveImagePlus)节点增加输出工作流为json功能,支持使用```%date```和```%time```在路径和文件名嵌入时间,增加预览开关。
* 添加 [SaveImagePlus](#SaveImagePlus)节点,可自定义保存图片的目录,文件名增加时间戳,选择保存格式,设置图片压缩率,设置是否保存工作流,以及可选给图片添加隐形水印。
@@ -1138,6 +1139,23 @@ cropped_mask: 裁切后的遮罩。
* image: 输入二维码图片。
* pre_blur: 预模糊,对难以识别的二维码可以尝试调整此数值。
+### LoadPSD
+
+加载PSD格式文件,并导出图层。
+
+节点选项说明:
+
+* image: 这里列出了```ComfyUI/input```下的*.psd文件,之前加载过的psd图片可以从这里选择。
+* file_path: psd文件的完整路径以及文件名。
+* include_hidden_layer: 是否包括隐藏图层。
+* find_layer_by: 查找图层的方法,可选择按图层索引编号或者图层名称查找。图层组被作为一个图层对待。
+* layer_index: 图层索引编号,0是最下面的图层,依次递增。如果include_hidden_layer设置为false,隐藏的图层不计入。设为-1则输出最上层的图层。
+* layer_name: 图层名称。注意大小写和标点符号必须完全匹配。
+
+输出:
+flat_image: psd预览图。
+layer_iamge: 查找的图层输出。
+all_layers: 包含全部图层的批量图片。
# LayerMask
diff --git a/image/load_image_example.jpg b/image/load_image_example.jpg
new file mode 100644
index 0000000..c132eb1
Binary files /dev/null and b/image/load_image_example.jpg differ
diff --git a/image/load_image_node.jpg b/image/load_image_node.jpg
new file mode 100644
index 0000000..b12cc2b
Binary files /dev/null and b/image/load_image_node.jpg differ
diff --git a/py/imagefunc.py b/py/imagefunc.py
index 3912121..38c48f1 100644
--- a/py/imagefunc.py
+++ b/py/imagefunc.py
@@ -1246,11 +1246,20 @@ def decode_watermark(image:Image, watermark_image_size:int=94) -> Image:
except Exception as e:
log(f"blind watermark extract fail, {e}")
ret_image = Image.new("RGB", (64, 64), color="black")
-
ret_image = normalize_gray(ret_image)
-
return ret_image
+def generate_text_image(width:int, height:int, text:str, font_file:str, text_scale:float=1, font_color:str="#FFFFFF",) -> Image:
+ image = Image.new("RGBA", (width, height), (0, 0, 0, 0))
+ draw = ImageDraw.Draw(image)
+ font_size = int(width / len(text) * text_scale)
+ font = ImageFont.truetype(font_file, font_size)
+ bbox = draw.textbbox((0, 0), text, font=font)
+ text_width, text_height = bbox[2] - bbox[0], bbox[3] - bbox[1]
+ x = int((width - text_width) / 2)
+ y = int((height - text_height) / 2) - int(font_size / 2)
+ draw.text((x, y), text, font=font, fill=font_color)
+ return image
'''Mask Functions'''
@lru_cache(maxsize=1, typed=False)
@@ -1616,6 +1625,21 @@ def Hex_to_HSV_255level(inhex:str) -> list:
HSV = colorsys.rgb_to_hsv(RGB[0] / 255.0, RGB[1] / 255.0, RGB[2] / 255.0)
return [int(x * 255) for x in HSV]
+
+def HSV_255level_to_Hex(HSV: list) -> str:
+ if len(HSV) != 3 or any((not isinstance(v, int) or v < 0 or v > 255) for v in HSV):
+ raise ValueError('Invalid HSV values, each value should be an integer between 0 and 255')
+
+ H, S, V = HSV
+ RGB = tuple(int(x * 255) for x in hsv_to_rgb(H / 255.0, S / 255.0, V / 255.0))
+
+ # Convert RGB values to hexadecimal format
+ hex_r = format(RGB[0], '02x')
+ hex_g = format(RGB[1], '02x')
+ hex_b = format(RGB[2], '02x')
+
+ return '#' + hex_r + hex_g + hex_b
+
'''Value Functions'''
def step_value(start_value, end_value, total_step, step) -> float: # 按当前步数在总步数中的位置返回比例值
diff --git a/py/loadpsd.py b/py/loadpsd.py
new file mode 100644
index 0000000..2ae83d5
--- /dev/null
+++ b/py/loadpsd.py
@@ -0,0 +1,101 @@
+import os
+import numpy as np
+import torch
+import folder_paths
+import node_helpers
+from nodes import LoadImage
+from PIL import Image,ImageOps,ImageSequence,ImageDraw,ImageFont
+from psd_tools import PSDImage
+from psd_tools.api.layers import Layer
+from .imagefunc import pil2tensor, log, FONT_DICT, generate_text_image
+
+
+class LoadPSD(LoadImage):
+ @classmethod
+ def INPUT_TYPES(s):
+ input_dir = folder_paths.get_input_directory()
+ files = [f for f in os.listdir(input_dir) if os.path.isfile(
+ os.path.join(input_dir, f)) and f.endswith(".psd")]
+ fine_layer_method = ["layer_index", "layer_name"]
+ return {"required":{
+ "image": (sorted(files), {"image_upload": True}),
+ "file_path": ("STRING", {"default": ""}),
+ "include_hidden_layer": ("BOOLEAN", {"default": False}),
+ "find_layer_by": (fine_layer_method,),
+ "layer_index": ("INT", {"default": 0, "min": -1, "max": 999, "step": 1}),
+ "layer_name": ("STRING", {"default": ""}),
+ },
+ "optional": {
+ }
+ }
+
+
+ RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE",)
+ RETURN_NAMES = ("flat_image", "layer_image", "all_layers",)
+ FUNCTION = "load_psd"
+ CATEGORY = '😺dzNodes/LayerUtility'
+
+ def load_psd(self, image, file_path, include_hidden_layer, layer_index, find_layer_by, layer_name,):
+
+ NODE_NAME = 'LoadPSD'
+ number_of_layers = 1
+ layer_image = []
+ all_layers = []
+ if file_path == "":
+ psd_file_path = folder_paths.get_annotated_filepath(image)
+ else:
+ psd_file_path = folder_paths.get_annotated_filepath(file_path)
+ flat_image = Image.open(psd_file_path).convert("RGB")
+ width, height = flat_image.size
+ if image.endswith(".psd"):
+ log(f"{NODE_NAME} -> Loading PSD file: {psd_file_path}")
+ psd_image = PSDImage.open(psd_file_path)
+ layers = []
+ for layer in psd_image:
+ if include_hidden_layer:
+ if not layer.is_visible():
+ layer.visible = True
+ layers.append(layer)
+ else:
+ if layer.is_visible():
+ layers.append(layer)
+
+ number_of_layers = len(layers)
+ for i in range(number_of_layers):
+ layer_canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0))
+ layer_canvas.paste(layers[i].composite(), layers[i].bbox)
+ all_layers.append(pil2tensor(layer_canvas))
+ if find_layer_by == "layer_name":
+ if layers[i].name == layer_name:
+ layer_image.append(pil2tensor(layer_canvas))
+ log(f"{NODE_NAME} -> Layer {i} : {layer.name} found.")
+ elif find_layer_by == "layer_index":
+ if i == layer_index:
+ layer_image.append(pil2tensor(layer_canvas))
+ log(f"{NODE_NAME} -> Layer {i} : {layer.name} found.")
+
+ text = "Layer Not Found!"
+ font_file = list(FONT_DICT.values())[0]
+ empty_layer_image = generate_text_image(flat_image.width, flat_image.height, text, font_file, font_color="#F01000")
+ if layer_image == []:
+ if layer_index == -1:
+ layer_image.append(all_layers[-1])
+ elif find_layer_by == "layer_name":
+ log(f'{NODE_NAME} -> Layer "{layer_name}" not found, top layer will be output.', message_type="warning")
+ elif find_layer_by == "layer_index":
+ log(f'{NODE_NAME} -> Layer index {layer_index} not found, top layer will be output.', message_type="warning")
+ layer_image.append(pil2tensor(empty_layer_image))
+ else:
+ layer_image.append(pil2tensor(flat_image))
+ all_layers.append(pil2tensor(flat_image))
+
+ return (torch.cat([pil2tensor(flat_image),], dim=0), torch.cat(layer_image, dim=0), torch.cat(all_layers, dim=0),)
+
+
+NODE_CLASS_MAPPINGS = {
+ "LayerUtility: LoadPSD": LoadPSD,
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerUtility: LoadPSD": "LayerUtility: Load PSD",
+}
\ No newline at end of file
diff --git a/py/save_image_plus.py b/py/save_image_plus.py
index 1e5236c..a4dc206 100644
--- a/py/save_image_plus.py
+++ b/py/save_image_plus.py
@@ -1,4 +1,5 @@
import os.path
+import shutil
from PIL.PngImagePlugin import PngInfo
import datetime
from .imagefunc import *
@@ -24,7 +25,7 @@ class SaveImagePlus:
"meta_data": ("BOOLEAN", {"default": False}),
"blind_watermark": ("STRING", {"default": ""}),
"save_workflow_as_json": ("BOOLEAN", {"default": False}),
- "preview": ("BOOLEAN", {"default": False}),
+ "preview": ("BOOLEAN", {"default": True}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
@@ -94,10 +95,15 @@ class SaveImagePlus:
preview_filename = ""
- if not os.path.exists(custom_path):
- if custom_path != "":
- raise FileNotFoundError("custom_path is not a valid path")
- else:
+ if custom_path != "":
+ if not os.path.exists(custom_path):
+ try:
+ os.makedirs(custom_path)
+ except Exception as e:
+ log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.",
+ message_type='warning')
+ raise FileNotFoundError(f"cannot create custom_path {custom_path}, {e}")
+
full_output_folder = os.path.normpath(custom_path)
# save preview image to temp_dir
if os.path.isdir(temp_dir):
@@ -106,7 +112,8 @@ class SaveImagePlus:
os.makedirs(temp_dir)
except Exception as e:
print(e)
- log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.", message_type='warning')
+ log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.",
+ message_type='warning')
try:
preview_filename = os.path.join(generate_random_name('saveimage_preview_', '_temp', 16) + '.png')
img.save(os.path.join(temp_dir, preview_filename))
@@ -133,7 +140,7 @@ class SaveImagePlus:
if img.mode == "RGBA":
img = img.convert("RGB")
img.save(image_file_name, quality=quality)
- log(f"Saving image to {image_file_name}")
+ log(f"{NODE_NAME} -> Saving image to {image_file_name}")
if save_workflow_as_json:
try:
diff --git a/pyproject.toml b/pyproject.toml
index 7046373..4436be5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,9 +1,9 @@
[project]
name = "comfyui_layerstyle"
description = "A set of nodes for ComfyUI it generate image like Adobe Photoshop's Layer Style. the Drop Shadow is first completed node, and follow-up work is in progress."
-version = "1.0.0"
+version = "1.0.1"
license = "LICENSE"
-dependencies = ["numpy", "pillow", "torch", "matplotlib", "Scipy", "scikit_image", "opencv-contrib-python", "pymatting", "segment_anything", "timm", "addict", "yapf", "colour-science", "wget", "mediapipe", "loguru", "typer_config", "fastapi", "rich", "google-generativeai", "diffusers", "omegaconf", "tqdm", "transformers", "kornia", "image-reward", "ultralytics", "blend_modes", "blind-watermark", "qrcode", "pyzbar"]
+dependencies = ["numpy", "pillow", "torch", "matplotlib", "Scipy", "scikit_image", "opencv-contrib-python", "pymatting", "segment_anything", "timm", "addict", "yapf", "colour-science", "wget", "mediapipe", "loguru", "typer_config", "fastapi", "rich", "google-generativeai", "diffusers", "omegaconf", "tqdm", "transformers", "kornia", "image-reward", "ultralytics", "blend_modes", "blind-watermark", "qrcode", "pyzbar", "psd-tools"]
[project.urls]
Repository = "https://github.com/chflame163/ComfyUI_LayerStyle"
diff --git a/requirements.txt b/requirements.txt
index ff570b5..9ff21a2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -29,3 +29,4 @@ blend_modes
blind-watermark
qrcode
pyzbar
+psd-tools