diff --git a/README.md b/README.md index 4971c2e..e534693 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,68 @@ # ComfyUI-PredefinedResolution -The ComfyUI node for selecting closes resolution that matches the user input + +A ComfyUI custom node that automatically snaps input dimensions to predefined resolutions and aspect ratios. + +## Features + +- **Automatic Ratio Detection**: Analyzes input width/height and snaps to the closest standard aspect ratio (21:9, 16:9, 1:1, 9:16, 9:21) +- **Predefined Output Resolutions**: Choose from 720p, 1080p, 2K, or 4K output resolutions +- **Custom Resolution Support**: Define your own resolution presets for each aspect ratio +- **Ratio Outputs**: Returns both width/height and height/width ratios for downstream nodes + +## Installation + +1. Clone this repository into your ComfyUI custom_nodes folder: +```bash +cd ComfyUI/custom_nodes +git clone https://github.com/pipelet-ai/ComfyUI-PredefinedResolution.git +``` + +2. Restart ComfyUI + +## Usage + +### Inputs + +**Required:** +- `width` (INT): Input image width (64-8192) +- `height` (INT): Input image height (64-8192) +- `output_resolution`: Select from "720p", "1080p", "2K", "4K", or "Custom" +- `target_ratio`: Target aspect ratio (21:9, 16:9, 1:1, 9:16, 9:21) + +**Optional:** +- `width_height_scale` (FLOAT): Override the calculated ratio with a manual value (0.0 = use calculated ratio) +- `custom_21_9` (STRING): Custom resolution for 21:9 ratio (format: "width,height", e.g., "2560,1080") +- `custom_16_9` (STRING): Custom resolution for 16:9 ratio (default: "1920,1080") +- `custom_1_1` (STRING): Custom resolution for 1:1 ratio (default: "1080,1080") +- `custom_9_16` (STRING): Custom resolution for 9:16 ratio (default: "608,1080") +- `custom_9_21` (STRING): Custom resolution for 9:21 ratio (default: "464,1080") + +### Outputs + +- `width` (INT): Output width +- `height` (INT): Output height +- `width_height_ratio` (FLOAT): Width divided by height +- `height_width_ratio` (FLOAT): Height divided by width + +### Example + +**Input:** 720 x 800 (from an uploaded image) +- Detected ratio: Closest to 1:1 +- Output resolution: 1080p +- **Output:** 1080 x 1080, ratio: 1.0, 1.0 + +**Input:** 1920 x 800 +- Detected ratio: Closest to 21:9 +- Output resolution: 2K +- **Output:** 4608 x 2048, ratio: 2.25, 0.44 + +## How It Works + +1. The node calculates the aspect ratio from input width/height (or uses the optional `width_height_scale` if provided) +2. It finds the closest matching standard aspect ratio (21:9, 16:9, 1:1, 9:16, 9:21) +3. Based on the selected output resolution preset, it calculates the appropriate dimensions +4. Returns the final width, height, and both ratio values + +## License + +See LICENSE file for details. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..ba5ad6c --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .predefined_resolution_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/predefined_resolution_node.py b/predefined_resolution_node.py new file mode 100644 index 0000000..1b6204a --- /dev/null +++ b/predefined_resolution_node.py @@ -0,0 +1,184 @@ +import math + +class PredefinedResolutionNode: + RATIO_PRESETS = { + "21:9": 21/9, + "16:9": 16/9, + "1:1": 1.0, + "9:16": 9/16, + "9:21": 9/21 + } + + RESOLUTION_PRESETS = { + "720p": 720, + "1080p": 1080, + "2K": 2048, + "4K": 3840 + } + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "width": ("INT", { + "default": 512, + "min": 64, + "max": 8192, + "step": 1 + }), + "height": ("INT", { + "default": 512, + "min": 64, + "max": 8192, + "step": 1 + }), + "output_resolution": (["720p", "1080p", "2K", "4K", "Custom"], { + "default": "1080p" + }), + }, + "optional": { + "enable_21_9": ("BOOLEAN", { + "default": True + }), + "custom_21_9": ("STRING", { + "default": "2560x1080", + "multiline": False + }), + "enable_16_9": ("BOOLEAN", { + "default": True + }), + "custom_16_9": ("STRING", { + "default": "1920x1080", + "multiline": False + }), + "enable_1_1": ("BOOLEAN", { + "default": True + }), + "custom_1_1": ("STRING", { + "default": "1080x1080", + "multiline": False + }), + "enable_9_16": ("BOOLEAN", { + "default": True + }), + "custom_9_16": ("STRING", { + "default": "1080x1920", + "multiline": False + }), + "enable_9_21": ("BOOLEAN", { + "default": True + }), + "custom_9_21": ("STRING", { + "default": "1080x2560", + "multiline": False + }), + } + } + + RETURN_TYPES = ("INT", "INT", "FLOAT", "FLOAT") + RETURN_NAMES = ("width", "height", "width_height_ratio", "height_width_ratio") + FUNCTION = "calculate_resolution" + CATEGORY = "utils" + + def parse_custom_resolution(self, custom_string): + try: + parts = custom_string.strip().lower().split('x') + if len(parts) == 2: + width = int(parts[0].strip()) + height = int(parts[1].strip()) + return width, height + except: + pass + return None, None + + def get_resolution_for_ratio(self, ratio_name, output_resolution, + custom_21_9, custom_16_9, custom_1_1, + custom_9_16, custom_9_21): + if output_resolution == "Custom": + custom_map = { + "21:9": custom_21_9, + "16:9": custom_16_9, + "1:1": custom_1_1, + "9:16": custom_9_16, + "9:21": custom_9_21 + } + width, height = self.parse_custom_resolution(custom_map[ratio_name]) + if width is not None and height is not None: + return width, height + + target_ratio = self.RATIO_PRESETS[ratio_name] + base_resolution = self.RESOLUTION_PRESETS.get(output_resolution, 1080) + + if ratio_name == "1:1": + width = height = base_resolution + elif target_ratio > 1.0: + min_dimension = base_resolution + height = min_dimension + width = int(round(height * target_ratio)) + else: + min_dimension = base_resolution + width = min_dimension + height = int(round(width / target_ratio)) + + return width, height + + def find_closest_ratio(self, input_ratio, enabled_ratios): + min_diff = float('inf') + closest_ratio = None + + for ratio_name, ratio_value in self.RATIO_PRESETS.items(): + if ratio_name not in enabled_ratios: + continue + diff = abs(input_ratio - ratio_value) + if diff < min_diff: + min_diff = diff + closest_ratio = ratio_name + + if closest_ratio is None: + closest_ratio = "1:1" + + return closest_ratio + + def calculate_resolution(self, width, height, output_resolution, + enable_21_9=True, custom_21_9="2560x1080", + enable_16_9=True, custom_16_9="1920x1080", + enable_1_1=True, custom_1_1="1080x1080", + enable_9_16=True, custom_9_16="1080x1920", + enable_9_21=True, custom_9_21="1080x2560"): + + input_ratio = width / height if height > 0 else 1.0 + + enabled_ratios = set() + if enable_21_9: + enabled_ratios.add("21:9") + if enable_16_9: + enabled_ratios.add("16:9") + if enable_1_1: + enabled_ratios.add("1:1") + if enable_9_16: + enabled_ratios.add("9:16") + if enable_9_21: + enabled_ratios.add("9:21") + + if not enabled_ratios: + enabled_ratios.add("1:1") + + snapped_ratio = self.find_closest_ratio(input_ratio, enabled_ratios) + + output_width, output_height = self.get_resolution_for_ratio( + snapped_ratio, output_resolution, + custom_21_9, custom_16_9, custom_1_1, custom_9_16, custom_9_21 + ) + + width_height_ratio = output_width / output_height if output_height > 0 else 1.0 + height_width_ratio = output_height / output_width if output_width > 0 else 1.0 + + return (output_width, output_height, width_height_ratio, height_width_ratio) + +NODE_CLASS_MAPPINGS = { + "PredefinedResolution": PredefinedResolutionNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "PredefinedResolution": "Predefined Resolution" +}