242 lines
7.7 KiB
Python
242 lines
7.7 KiB
Python
import hashlib
|
|
import os
|
|
|
|
import numpy as np
|
|
import torch
|
|
|
|
from PIL import Image, ImageOps, ImageSequence
|
|
|
|
|
|
HEIC_EXTS = {".heic", ".heif"}
|
|
|
|
|
|
def _register_preview_route_if_possible() -> None:
|
|
"""Register /heic_preview once the PromptServer is available."""
|
|
try:
|
|
from aiohttp import web
|
|
from io import BytesIO
|
|
from server import PromptServer
|
|
|
|
instance = getattr(PromptServer, "instance", None)
|
|
if instance is None or not hasattr(instance, "routes"):
|
|
return
|
|
if getattr(instance, "_heic_preview_route_registered", False):
|
|
return
|
|
instance._heic_preview_route_registered = True
|
|
|
|
@instance.routes.get("/heic_preview")
|
|
async def heic_preview(request: web.Request):
|
|
filename = request.rel_url.query.get("filename")
|
|
if not filename:
|
|
return web.Response(status=400, text="filename is required")
|
|
|
|
folder_paths, _ = _get_comfy_modules()
|
|
if not folder_paths.exists_annotated_filepath(filename):
|
|
return web.Response(status=404, text="file not found")
|
|
|
|
image_path = folder_paths.get_annotated_filepath(filename)
|
|
_try_register_heif_opener()
|
|
try:
|
|
img = Image.open(image_path)
|
|
img = ImageOps.exif_transpose(img)
|
|
img = img.convert("RGBA")
|
|
except Exception as e:
|
|
return web.Response(status=500, text=f"failed to decode image: {e}")
|
|
|
|
bio = BytesIO()
|
|
img.save(bio, format="PNG")
|
|
return web.Response(body=bio.getvalue(), content_type="image/png")
|
|
except Exception:
|
|
return
|
|
|
|
|
|
def _get_comfy_modules():
|
|
try:
|
|
import folder_paths # provided by ComfyUI
|
|
import node_helpers # provided by ComfyUI
|
|
except Exception as e:
|
|
raise RuntimeError(
|
|
"This node must run inside a ComfyUI environment (missing folder_paths/node_helpers)."
|
|
) from e
|
|
|
|
return folder_paths, node_helpers
|
|
|
|
|
|
def _try_register_heif_opener() -> bool:
|
|
"""Register HEIF/HEIC opener for Pillow. Returns True if available."""
|
|
try:
|
|
from pillow_heif import register_heif_opener # type: ignore
|
|
|
|
register_heif_opener()
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _is_heic_path(name: str) -> bool:
|
|
_, ext = os.path.splitext(str(name))
|
|
return ext.lower() in HEIC_EXTS
|
|
|
|
|
|
def _list_heic_files_in_input_dir() -> list[str]:
|
|
folder_paths, _ = _get_comfy_modules()
|
|
input_dir = folder_paths.get_input_directory()
|
|
try:
|
|
files = [
|
|
f
|
|
for f in os.listdir(input_dir)
|
|
if os.path.isfile(os.path.join(input_dir, f))
|
|
]
|
|
except Exception:
|
|
return []
|
|
|
|
allowed_ext = {".heic", ".heif"}
|
|
out = []
|
|
for f in files:
|
|
_, ext = os.path.splitext(f)
|
|
if ext.lower() in allowed_ext:
|
|
out.append(f)
|
|
|
|
return sorted(out)
|
|
|
|
|
|
def _list_image_files_in_input_dir() -> list[str]:
|
|
"""Return all supported image files (PNG, JPG, WEBP, HEIC/HEIF)."""
|
|
folder_paths, _ = _get_comfy_modules()
|
|
input_dir = folder_paths.get_input_directory()
|
|
try:
|
|
files = [
|
|
f
|
|
for f in os.listdir(input_dir)
|
|
if os.path.isfile(os.path.join(input_dir, f))
|
|
]
|
|
except Exception:
|
|
return []
|
|
|
|
# Support all common image formats + HEIC
|
|
allowed_ext = {".png", ".jpg", ".jpeg", ".webp", ".heic", ".heif", ".bmp", ".gif"}
|
|
out = []
|
|
for f in files:
|
|
_, ext = os.path.splitext(f)
|
|
if ext.lower() in allowed_ext:
|
|
out.append(f)
|
|
|
|
return sorted(out)
|
|
|
|
|
|
class LoadImagePlusHEIC:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
_register_preview_route_if_possible()
|
|
files = _list_image_files_in_input_dir()
|
|
return {
|
|
"required": {
|
|
"image": (files, {"image_upload": True}),
|
|
},
|
|
}
|
|
|
|
CATEGORY = "image"
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK")
|
|
FUNCTION = "load_image"
|
|
|
|
def load_image(self, image):
|
|
if _is_heic_path(image) and not _try_register_heif_opener():
|
|
raise RuntimeError(
|
|
"HEIC/HEIF support is not available. Install dependency: pip install pillow-heif"
|
|
)
|
|
else:
|
|
_try_register_heif_opener()
|
|
|
|
folder_paths, node_helpers = _get_comfy_modules()
|
|
|
|
if not folder_paths.exists_annotated_filepath(image):
|
|
raise FileNotFoundError(f"Image not found in input path: {image}")
|
|
|
|
image_path = folder_paths.get_annotated_filepath(image)
|
|
|
|
try:
|
|
img = node_helpers.pillow(Image.open, image_path)
|
|
except Exception as e:
|
|
if _is_heic_path(image):
|
|
raise RuntimeError(
|
|
"Failed to open HEIC/HEIF image. Ensure pillow-heif is installed: pip install pillow-heif\n"
|
|
f"File: {image}\nError: {e}"
|
|
)
|
|
raise RuntimeError(f"Failed to open image: {image}\nError: {e}")
|
|
|
|
output_images = []
|
|
output_masks = []
|
|
w, h = None, None
|
|
|
|
excluded_formats = ["MPO"]
|
|
|
|
for i in ImageSequence.Iterator(img):
|
|
i = node_helpers.pillow(ImageOps.exif_transpose, i)
|
|
|
|
if i.mode == "I":
|
|
i = i.point(lambda i: i * (1 / 255))
|
|
image_rgb = i.convert("RGB")
|
|
|
|
if len(output_images) == 0:
|
|
w = image_rgb.size[0]
|
|
h = image_rgb.size[1]
|
|
|
|
if image_rgb.size[0] != w or image_rgb.size[1] != h:
|
|
continue
|
|
|
|
image_np = np.array(image_rgb).astype(np.float32) / 255.0
|
|
image_t = torch.from_numpy(image_np)[None,]
|
|
|
|
if "A" in i.getbands():
|
|
mask_np = np.array(i.getchannel("A")).astype(np.float32) / 255.0
|
|
mask_t = 1.0 - torch.from_numpy(mask_np)
|
|
elif i.mode == "P" and "transparency" in i.info:
|
|
mask_np = (
|
|
np.array(i.convert("RGBA").getchannel("A")).astype(np.float32) / 255.0
|
|
)
|
|
mask_t = 1.0 - torch.from_numpy(mask_np)
|
|
else:
|
|
mask_t = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
|
|
|
|
output_images.append(image_t)
|
|
output_masks.append(mask_t.unsqueeze(0))
|
|
|
|
if not output_images:
|
|
raise RuntimeError(f"No frames could be decoded from image: {image}")
|
|
|
|
if len(output_images) > 1 and getattr(img, "format", None) not in excluded_formats:
|
|
output_image = torch.cat(output_images, dim=0)
|
|
output_mask = torch.cat(output_masks, dim=0)
|
|
else:
|
|
output_image = output_images[0]
|
|
output_mask = output_masks[0]
|
|
|
|
return (output_image, output_mask)
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, image):
|
|
folder_paths, _ = _get_comfy_modules()
|
|
image_path = folder_paths.get_annotated_filepath(image)
|
|
m = hashlib.sha256()
|
|
with open(image_path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
|
m.update(chunk)
|
|
return m.digest().hex()
|
|
|
|
@classmethod
|
|
def VALIDATE_INPUTS(cls, image):
|
|
folder_paths, _ = _get_comfy_modules()
|
|
if not folder_paths.exists_annotated_filepath(image):
|
|
return "Invalid image file: {}".format(image)
|
|
return True
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"LoadImagePlusHEIC": LoadImagePlusHEIC,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"LoadImagePlusHEIC": "Load Image (HEIC)",
|
|
}
|