Files
SparknightLLC-ComfyUI-Image…/__init__.py
T
ThereforeGames 892f24b5cc Rework ImageAutosize for ComfyUI v3 API
Rebuild ImageAutosize to use ComfyUI V3 node API: add a proper ComfyExtension entrypoint and io.Schema-based node. Rename display to "Image/Mask Autosize", accept IMAGE or MASK inputs, and replace multiplier output with scale_x/scale_y. Add constraint_priority (min_size/max_size), anchored crop handling, divisible-by rounding protection, and new interpolation modes. Update README, add CHANGELOG, bump package version to 0.2.0, refresh workflow image, and include comprehensive unit tests for sizing, cropping, and mask behavior.
2026-07-31 03:10:07 -04:00

218 lines
5.2 KiB
Python

import comfy.utils
from comfy_api.latest import ComfyExtension, io
SCALE_METHODS = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
CONSTRAINT_PRIORITIES = ["min_size", "max_size"]
CROP_MODES = [
"none",
"center",
"top",
"bottom",
"left",
"right",
"top_left",
"top_right",
"bottom_left",
"bottom_right",
]
def _calculate_target_dimensions(
width: int,
height: int,
max_size: int,
min_size: int,
constraint_priority: str,
divisible_by: int,
) -> tuple[int, int]:
max_scale = max_size / max(width, height)
min_scale = min_size / min(width, height)
if constraint_priority == "min_size":
scale = max(max_scale, min_scale)
else:
scale = min(min_scale, max_scale)
target_width = max(divisible_by, round(width * scale / divisible_by) * divisible_by)
target_height = max(divisible_by, round(height * scale / divisible_by) * divisible_by)
return target_width, target_height
def _get_crop_origin(
width: int,
height: int,
target_width: int,
target_height: int,
crop_mode: str,
) -> tuple[int, int]:
if crop_mode in ("left", "top_left", "bottom_left"):
x = 0
elif crop_mode in ("right", "top_right", "bottom_right"):
x = width - target_width
else:
x = (width - target_width) // 2
if crop_mode in ("top", "top_left", "top_right"):
y = 0
elif crop_mode in ("bottom", "bottom_left", "bottom_right"):
y = height - target_height
else:
y = (height - target_height) // 2
return x, y
class ImageAutosize(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
image_type = io.MatchType.Template("image_type", [io.Image, io.Mask])
return io.Schema(
node_id="ImageAutosize",
display_name="Image/Mask Autosize",
category="image",
description="Automatically resizes an image or mask for diffusion workflows.",
search_aliases=[
"autosize",
"image mask autosize",
"auto resize",
"resize to multiple",
"resize image",
"resize mask",
],
inputs=[
io.MatchType.Input(
"image",
template=image_type,
tooltip="The image or mask to resize.",
),
io.Int.Input(
"max_size",
default=1280,
min=1,
max=8192,
step=1,
tooltip="Longer-dimension target used to calculate one candidate resize scale.",
),
io.Int.Input(
"min_size",
default=512,
min=1,
max=4096,
step=1,
tooltip="Shorter-dimension target used to calculate one candidate resize scale.",
),
io.Int.Input(
"divisible_by",
default=32,
min=1,
max=8192,
step=1,
tooltip="Rounds both output dimensions to the nearest multiple of this value.",
),
io.Combo.Input(
"interpolation_mode",
options=SCALE_METHODS,
default="lanczos",
tooltip="Interpolation algorithm used for resizing.",
),
io.Combo.Input(
"crop_mode",
options=CROP_MODES,
default="center",
tooltip="Preserves aspect ratio by cropping from this position. None stretches to the output dimensions.",
),
io.Combo.Input(
"constraint_priority",
options=CONSTRAINT_PRIORITIES,
default="min_size",
tooltip="Chooses the candidate scale. min_size uses the larger scale; max_size uses the smaller scale. Divisibility rounding runs afterward.",
),
],
outputs=[
io.MatchType.Output(template=image_type, display_name="resized"),
io.Int.Output(display_name="width"),
io.Int.Output(display_name="height"),
io.Float.Output(display_name="scale_x"),
io.Float.Output(display_name="scale_y"),
],
)
@classmethod
def execute(
cls,
image: io.Image.Type | io.Mask.Type,
max_size: int,
min_size: int,
constraint_priority: str,
divisible_by: int,
interpolation_mode: str,
crop_mode: str,
) -> io.NodeOutput:
is_image = len(image.shape) == 4
if is_image:
_, height, width, _ = image.shape
samples = image.movedim(-1, 1)
else:
_, height, width = image.shape
samples = image.unsqueeze(1)
target_width, target_height = _calculate_target_dimensions(
width,
height,
max_size,
min_size,
constraint_priority,
divisible_by,
)
resize_width = target_width
resize_height = target_height
if crop_mode != "none":
scale = max(target_width / width, target_height / height)
resize_width = max(target_width, round(width * scale))
resize_height = max(target_height, round(height * scale))
if resize_width != width or resize_height != height:
samples = comfy.utils.common_upscale(
samples,
resize_width,
resize_height,
interpolation_mode,
"disabled",
)
if len(samples.shape) == 3:
samples = samples.unsqueeze(1)
if crop_mode != "none":
x, y = _get_crop_origin(
resize_width,
resize_height,
target_width,
target_height,
crop_mode,
)
samples = samples[:, :, y:y + target_height, x:x + target_width]
if is_image:
output = samples.movedim(1, -1)
else:
output = samples.squeeze(1)
return io.NodeOutput(
output,
target_width,
target_height,
resize_width / width,
resize_height / height,
)
class ImageAutosizeExtension(ComfyExtension):
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [ImageAutosize]
async def comfy_entrypoint() -> ImageAutosizeExtension:
return ImageAutosizeExtension()