From 7d0660f4e43e1926f8bdb77c95934f3b1b20c6b0 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Thu, 1 Jan 2026 20:43:58 -0500 Subject: [PATCH] image crop widget --- comfy_api/latest/_io.py | 17 +++++++++++++++++ comfy_extras/nodes_images.py | 12 +++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 764fa8b2b..c6b622b6f 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -1113,6 +1113,22 @@ class DynamicSlot(ComfyTypeI): out_dict[input_type][finalized_id] = value out_dict["dynamic_paths"][finalized_id] = finalize_prefix(curr_prefix, curr_prefix[-1]) +@comfytype(io_type="IMAGECROP") +class ImageCrop(ComfyTypeI): + """Widget for visual image cropping with x, y, width, height controls.""" + Type = dict + + class Input(WidgetInput): + def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, + socketless: bool=True, + default: dict=None): + super().__init__(id, display_name, optional, tooltip, None, default, socketless) + if default is None: + self.default = {"x": 0, "y": 0, "width": 512, "height": 512} + + def as_dict(self): + return super().as_dict() + DYNAMIC_INPUT_LOOKUP: dict[str, Callable[[dict[str, Any], dict[str, Any], tuple[str, dict[str, Any]], str, list[str] | None], None]] = {} def register_dynamic_input_func(io_type: str, func: Callable[[dict[str, Any], dict[str, Any], tuple[str, dict[str, Any]], str, list[str] | None], None]): DYNAMIC_INPUT_LOOKUP[io_type] = func @@ -1958,4 +1974,5 @@ __all__ = [ "add_to_dict_v1", "add_to_dict_v3", "V3Data", + "ImageCrop", ] diff --git a/comfy_extras/nodes_images.py b/comfy_extras/nodes_images.py index ce21caade..77ab78056 100644 --- a/comfy_extras/nodes_images.py +++ b/comfy_extras/nodes_images.py @@ -26,16 +26,18 @@ class ImageCrop(IO.ComfyNode): category="image/transform", inputs=[ IO.Image.Input("image"), - IO.Int.Input("width", default=512, min=1, max=nodes.MAX_RESOLUTION, step=1), - IO.Int.Input("height", default=512, min=1, max=nodes.MAX_RESOLUTION, step=1), - IO.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION, step=1), - IO.Int.Input("y", default=0, min=0, max=nodes.MAX_RESOLUTION, step=1), + IO.ImageCrop.Input("crop_region"), ], outputs=[IO.Image.Output()], ) @classmethod - def execute(cls, image, width, height, x, y) -> IO.NodeOutput: + def execute(cls, image, crop_region) -> IO.NodeOutput: + x = crop_region.get("x", 0) + y = crop_region.get("y", 0) + width = crop_region.get("width", 512) + height = crop_region.get("height", 512) + x = min(x, image.shape[2] - 1) y = min(y, image.shape[1] - 1) to_x = width + x