Files
moonwhaler-comfyui-moonpack/labeled_load_image_node.py
moonwhaler ec583c07a4 feat: add crop support to Load Image (Labeled)
Adds crop_x/crop_y/crop_width/crop_height inputs, applied before the
label bar is composited, plus an Edit Crop popup for dragging the
region visually on the original image.
2026-08-20 17:16:42 +02:00

124 lines
5.2 KiB
Python

import numpy as np
try:
from PIL import Image, ImageOps
except ImportError: # pragma: no cover - PIL ships with ComfyUI
Image = None
ImageOps = None
try:
from ._label_ops import apply_crop, compose_labeled_image
except ImportError: # importable for tests without package context
from _label_ops import apply_crop, compose_labeled_image
CATEGORY = "MoonPack/image"
class LabeledLoadImage:
DESCRIPTION = (
"Loads an image and bakes an editable caption bar into it (e.g. 'Front View', "
"'Character A'), so a reference stays identifiable once it's combined into a "
"bigger Reference Concat/Grid layout. The bar is added as extra space at the "
"top or bottom - only the crop region (if set) affects the photo's extent, "
"the label bar itself never crops or resizes it further. A bold arrow in "
"the bar points back at the photo; text and arrow can each be turned off "
"independently. An optional border (same color as the bar) frames the whole "
"result. Use the node's Edit Crop button to select a region visually, and "
"the Preview button for an instant, exact look at the result before queuing."
)
SEARCH_ALIASES = ["load image", "label", "caption", "annotate", "reference", "text overlay"]
@classmethod
def INPUT_TYPES(cls):
import os
import folder_paths
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))]
files = folder_paths.filter_files_content_types(files, ["image"])
return {
"required": {
"image": (sorted(files), {"image_upload": True}),
"text": ("STRING", {
"default": "", "multiline": False,
"tooltip": "Caption drawn in the label bar. Auto-shrinks to fit, then truncates with an ellipsis.",
}),
"text_position": (["top", "bottom"], {
"default": "top",
"tooltip": "Which edge of the image the label bar is added to.",
}),
"show_label": ("BOOLEAN", {
"default": True,
"tooltip": "Draw the caption text in the bar.",
}),
"show_arrow": ("BOOLEAN", {
"default": True,
"tooltip": "Draw a bold arrow in the bar, pointing back at the photo.",
}),
"background_color": ("STRING", {
"default": "#000000",
"tooltip": "Label bar fill color and border color, as a hex code.",
}),
"text_color": ("STRING", {
"default": "#ffffff",
"tooltip": "Caption text and arrow color, as a hex code.",
}),
"border_width": ("INT", {
"default": 0, "min": 0, "max": 512, "step": 1,
"tooltip": "Inset border thickness in pixels, drawn in background_color around the whole result. 0 = no border.",
}),
"crop_x": ("INT", {
"default": 0, "min": 0, "max": 999999, "step": 1,
"tooltip": "Left edge of the crop region, in source-image pixels. Use the Edit Crop button to set this visually.",
}),
"crop_y": ("INT", {
"default": 0, "min": 0, "max": 999999, "step": 1,
"tooltip": "Top edge of the crop region, in source-image pixels. Use the Edit Crop button to set this visually.",
}),
"crop_width": ("INT", {
"default": 0, "min": 0, "max": 999999, "step": 1,
"tooltip": "Crop region width in pixels. 0 = everything to the right of crop_x (no crop, if crop_x/crop_y/crop_height are also 0).",
}),
"crop_height": ("INT", {
"default": 0, "min": 0, "max": 999999, "step": 1,
"tooltip": "Crop region height in pixels. 0 = everything below crop_y (no crop, if crop_x/crop_y/crop_width are also 0).",
}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "load_image"
CATEGORY = CATEGORY
def load_image(self, image, text, text_position, show_label, show_arrow,
background_color, text_color, border_width,
crop_x, crop_y, crop_width, crop_height):
import folder_paths
image_path = folder_paths.get_annotated_filepath(image)
img = Image.open(image_path)
img = ImageOps.exif_transpose(img).convert("RGB")
img = apply_crop(img, crop_x, crop_y, crop_width, crop_height)
composed = compose_labeled_image(
img, text, text_position, show_label, show_arrow,
background_color, text_color, border_width,
)
arr = np.array(composed).astype(np.float32) / 255.0
tensor = arr[None, ...]
import torch
return (torch.from_numpy(tensor),)
NODE_CLASS_MAPPINGS = {
"MoonPack_LabeledLoadImage": LabeledLoadImage,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MoonPack_LabeledLoadImage": "Load Image (Labeled)",
}