| import math | |
| import torch | |
| import numpy as np | |
| from PIL import Image | |
| def tensor_to_pil(img_tensor): | |
| """ | |
| Converts a ComfyUI IMAGE tensor [H, W, C] float32 in range 0..1 | |
| into a PIL image. | |
| """ | |
| img = img_tensor.cpu().numpy() | |
| img = np.clip(img * 255.0, 0, 255).astype(np.uint8) | |
| return Image.fromarray(img) | |
| def pil_to_tensor(img_pil): | |
| """ | |
| Converts a PIL image into a ComfyUI IMAGE tensor [H, W, C] float32 0..1. | |
| """ | |
| arr = np.array(img_pil).astype(np.float32) / 255.0 | |
| return torch.from_numpy(arr) | |
| def fit_inside(src_w, src_h, max_w, max_h): | |
| """ | |
| Resizes while preserving aspect ratio so the source fits entirely inside | |
| the target box, without cropping. | |
| """ | |
| if src_w <= 0 or src_h <= 0: | |
| return 1, 1 | |
| scale = min(max_w / src_w, max_h / src_h) | |
| new_w = max(1, int(round(src_w * scale))) | |
| new_h = max(1, int(round(src_h * scale))) | |
| return new_w, new_h | |
| def aligned_offset(container_size, content_size, align): | |
| """ | |
| Returns the position offset for start / center / end alignment. | |
| """ | |
| if align == "start": | |
| return 0 | |
| elif align == "end": | |
| return max(0, container_size - content_size) | |
| return max(0, (container_size - content_size) // 2) | |
| def paste_with_alpha(dst, src_rgba, xy): | |
| """ | |
| Pastes an image onto another, preserving alpha if present. | |
| """ | |
| if src_rgba.mode == "RGBA": | |
| dst.paste(src_rgba, xy, src_rgba.split()[-1]) | |
| else: | |
| dst.paste(src_rgba, xy) | |
| def add_white_padding(img_rgba, pad_px=16): | |
| new_w = img_rgba.width + pad_px * 2 | |
| new_h = img_rgba.height + pad_px * 2 | |
| canvas = Image.new("RGBA", (new_w, new_h), (255, 255, 255, 255)) | |
| canvas.paste(img_rgba, (pad_px, pad_px), img_rgba if img_rgba.mode == "RGBA" else None) | |
| return canvas | |