Files

195 lines
6.9 KiB
Python

import os
import numpy as np
import torch
from PIL import Image, ImageDraw, ImageFont, ImageOps
try:
import folder_paths
_FONT_DIRS = [os.path.join(folder_paths.base_path, "fonts")]
except Exception:
_FONT_DIRS = []
# A couple of common system font locations as a fallback when no bundled
# .ttf is found, so the node still renders labels instead of erroring.
_FALLBACK_FONTS = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"C:\\Windows\\Fonts\\arial.ttf",
]
def _tensor_to_pil(image_tensor):
arr = image_tensor.detach().cpu().numpy() if isinstance(image_tensor, torch.Tensor) else image_tensor
if arr.ndim == 4:
arr = arr[0]
arr = (np.clip(arr, 0.0, 1.0) * 255.0).astype(np.uint8)
return Image.fromarray(arr).convert("RGB")
def _pil_to_tensor(img):
arr = np.array(img.convert("RGB")).astype(np.float32) / 255.0
return torch.from_numpy(arr).unsqueeze(0)
def _load_font(font_size):
for path in _FONT_DIRS:
if os.path.isdir(path):
for f in os.listdir(path):
if f.lower().endswith(".ttf"):
try:
return ImageFont.truetype(os.path.join(path, f), font_size)
except Exception:
continue
for path in _FALLBACK_FONTS:
if os.path.isfile(path):
try:
return ImageFont.truetype(path, font_size)
except Exception:
continue
return ImageFont.load_default()
def _text_panel(width, height, text, font_size, font_color, bg_color):
panel = Image.new("RGB", (width, height), bg_color)
draw = ImageDraw.Draw(panel)
font = _load_font(font_size)
lines = text.split("\n")
line_heights = []
for line in lines:
bbox = draw.textbbox((0, 0), line, font=font)
line_heights.append(bbox[3] - bbox[1])
total_h = sum(line_heights)
y = max(0, (height - total_h) // 2)
for line, lh in zip(lines, line_heights):
bbox = draw.textbbox((0, 0), line, font=font)
lw = bbox[2] - bbox[0]
x = max(0, (width - lw) // 2)
draw.text((x, y), line, font=font, fill=font_color)
y += lh
return panel
def _resize_to_match(img, width, height):
if img.size != (width, height):
return img.resize((width, height), Image.BICUBIC)
return img
class SupersideImageCompareNode:
"""
Superside Image Compare: in-house replacement for Comfyroll's
"CR Simple Image Compare". Stacks each image with a labeled text
footer, then places both panels side by side with a border - same
layout/labels as the original (text1/text2 are free labels, not
hardcoded "BEFORE"/"AFTER").
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text1": ("STRING", {"multiline": True, "default": "BEFORE"}),
"text2": ("STRING", {"multiline": True, "default": "AFTER"}),
"footer_height": ("INT", {"default": 100, "min": 0, "max": 1024}),
"font_size": ("INT", {"default": 50, "min": 0, "max": 1024}),
"mode": (["normal", "dark"],),
"border_thickness": ("INT", {"default": 20, "min": 0, "max": 1024}),
},
"optional": {
"image1": ("IMAGE",),
"image2": ("IMAGE",),
},
}
CATEGORY = "Superside"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "layout"
DESCRIPTION = "Side-by-side labeled before/after comparison - in-house equivalent of Comfyroll's CR Simple Image Compare."
def layout(self, text1, text2, footer_height, font_size, mode, border_thickness,
image1=None, image2=None):
font_color = "white" if mode == "dark" else "black"
bg_color = "black" if mode == "dark" else "white"
outline_thickness = border_thickness // 2
border_thickness = border_thickness // 2
if image1 is not None and image2 is not None:
img1 = _tensor_to_pil(image1)
img2 = _tensor_to_pil(image2)
image_width, image_height = img1.size
img2 = _resize_to_match(img2, image_width, image_height)
halves = []
for img, text in ((img1, text1), (img2, text2)):
if footer_height > 0:
panel = _text_panel(image_width, footer_height, text, font_size, font_color, bg_color)
combined = Image.new("RGB", (image_width, image_height + footer_height), bg_color)
combined.paste(img, (0, 0))
combined.paste(panel, (0, image_height))
else:
combined = img
if outline_thickness > 0:
combined = ImageOps.expand(combined, outline_thickness, fill=bg_color)
halves.append(combined)
h = max(h.size[1] for h in halves)
total_w = sum(h.size[0] for h in halves)
result_img = Image.new("RGB", (total_w, h), bg_color)
x = 0
for half in halves:
result_img.paste(half, (x, 0))
x += half.size[0]
else:
result_img = Image.new("RGB", (512, 512), bg_color)
if border_thickness > 0:
result_img = ImageOps.expand(result_img, border_thickness, bg_color)
return (_pil_to_tensor(result_img),)
class SupersideImageComparerNode:
"""
Superside Image Comparer: simplified in-house replacement for
rgthree's "Image Comparer". rgthree's node is a custom interactive
slide/click widget implemented in frontend JS (web/comparer.js) - that
interactivity can't be replicated by a python-only node. This node
instead produces a static side-by-side image (same visual comparison,
no in-canvas slider), which is enough for downstream use in a pipeline.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_a": ("IMAGE",),
"image_b": ("IMAGE",),
},
"optional": {
"gap": ("INT", {"default": 8, "min": 0, "max": 256}),
},
}
CATEGORY = "Superside"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "compare"
DESCRIPTION = (
"Static side-by-side comparison of two images - simplified in-house "
"equivalent of rgthree's Image Comparer (no interactive slider; that "
"requires the original package's frontend JS widget)."
)
def compare(self, image_a, image_b, gap=8):
img_a = _tensor_to_pil(image_a)
img_b = _tensor_to_pil(image_b)
img_b = _resize_to_match(img_b, img_a.size[0], img_a.size[1])
total_w = img_a.size[0] + gap + img_b.size[0]
h = max(img_a.size[1], img_b.size[1])
canvas = Image.new("RGB", (total_w, h), "black")
canvas.paste(img_a, (0, 0))
canvas.paste(img_b, (img_a.size[0] + gap, 0))
return (_pil_to_tensor(canvas),)