Merge pull request #1 from ComfyAssets/width-and-height

Width and height
This commit is contained in:
Vito
2025-06-14 11:05:25 -07:00
committed by GitHub
12 changed files with 1906 additions and 6 deletions
+33 -3
View File
@@ -119,8 +119,10 @@ release-test: quality-check test
@test -d examples || (echo "❌ examples directory missing" && exit 1)
@echo "✅ Package structure validated"
@echo "Checking documentation..."
@test -f examples/documentation/resolution_calculator.md || (echo "❌ Tool documentation missing" && exit 1)
@test -f examples/workflows/resolution_calculator_example.json || (echo "❌ Example workflow missing" && exit 1)
@test -f examples/documentation/resolution_calculator.md || (echo "❌ Resolution Calculator documentation missing" && exit 1)
@test -f examples/workflows/resolution_calculator_example.json || (echo "❌ Resolution Calculator workflow missing" && exit 1)
@test -f examples/documentation/width_height_selector.md || (echo "❌ Width Height Selector documentation missing" && exit 1)
@test -f examples/workflows/width_height_selector_example.json || (echo "❌ Width Height Selector workflow missing" && exit 1)
@echo "✅ Documentation validated"
@echo "🎉 Release validation completed!"
@@ -146,4 +148,32 @@ ci: quality-check test
# Tool-specific commands (can be extended for new tools)
test-resolution-calculator:
@echo "Testing Resolution Calculator specifically..."
@python -c "import sys, os; sys.path.insert(0, os.getcwd()); from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode; import torch; node = ResolutionCalculatorNode(); scenarios = [('SDXL Portrait', torch.randn(1, 1216, 832, 3), 1.5), ('FLUX Square', torch.randn(1, 1024, 1024, 3), 2.0), ('User Scenario', torch.randn(1, 1216, 832, 3), 1.53)]; [print(f'✅ {name}: {image.shape[2]}×{image.shape[1]} → {node.calculate_resolution(scale, image=image)[0]}×{node.calculate_resolution(scale, image=image)[1]} ({scale}x)') for name, image, scale in scenarios]; print('🎉 Resolution Calculator tests completed!')"
@python -c "import sys, os; sys.path.insert(0, os.getcwd()); from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode; import torch; node = ResolutionCalculatorNode(); scenarios = [('SDXL Portrait', torch.randn(1, 1216, 832, 3), 1.5), ('FLUX Square', torch.randn(1, 1024, 1024, 3), 2.0), ('User Scenario', torch.randn(1, 1216, 832, 3), 1.53)]; [print(f'✅ {name}: {image.shape[2]}×{image.shape[1]} → {node.calculate_resolution(scale, image=image)[0]}×{node.calculate_resolution(scale, image=image)[1]} ({scale}x)') for name, image, scale in scenarios]; print('🎉 Resolution Calculator tests completed!')"
test-width-height-selector:
@echo "Testing Width Height Selector specifically..."
@python -c "\
import sys, os; \
sys.path.insert(0, os.getcwd()); \
from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode; \
from kikotools.tools.width_height_selector.presets import PRESET_OPTIONS; \
print(f'Testing Width Height Selector with {len(PRESET_OPTIONS)} presets...'); \
node = WidthHeightSelectorNode(); \
scenarios = [ \
('SDXL Square', '1024×1024'), \
('FLUX HD', '1920×1080'), \
('SDXL Portrait', '832×1216'), \
('Custom Dimensions', 'custom'), \
('Ultra-Wide', '2560×1080') \
]; \
for name, preset in scenarios: \
if preset == 'custom': \
result = node.get_dimensions(preset, 1536, 768); \
else: \
result = node.get_dimensions(preset, 1024, 1024); \
print(f'✅ {name}: {preset} → {result[0]}×{result[1]}'); \
print('🎉 Width Height Selector tests completed!') \
"
test-all-tools: test-resolution-calculator test-width-height-selector
@echo "🎉 All tool-specific tests completed!"
+51 -2
View File
@@ -29,6 +29,21 @@ Calculate upscaled dimensions from image or latent inputs with precision.
- Ensure ComfyUI tensor compatibility
- Optimize batch processing workflows
#### 📏 Width Height Selector
Advanced preset-based dimension selection with visual swap button.
- **26 Curated Presets**: SDXL, FLUX, and Ultra-Wide optimized resolutions
- **Smart Categories**: Organized by model type and aspect ratio
- **Visual Swap Button**: Modern blue button for quick orientation changes
- **Intelligent Swapping**: Preset-aware orientation switching
- **Custom Support**: Manual dimension input with validation
**Use Cases:**
- Quick dimension selection for different models
- Consistent aspect ratios across workflows
- Mobile and ultra-wide format support
- Integration with upscaling pipelines
### 🔧 Architecture Highlights
- **Modular Design**: Each tool is self-contained and independently testable
@@ -70,6 +85,18 @@ Image Loader → Resolution Calculator → Upscaler
**Scale:** 1.5x
**Output:** 1248×1824 (ready for upscaling)
### Width Height Selector Example
```
Width Height Selector → EmptyLatentImage → Model
preset: "1920×1080" ↘ 1920×1080 ↗
[swap button]
```
**Preset:** FLUX HD (1920×1080)
**Output:** 1920×1080 (16:9 cinematic)
**Swap Button:** Click to get 1080×1920 (9:16 portrait)
### Common Workflows
<details>
@@ -107,9 +134,9 @@ Image Loader → Resolution Calculator → Upscaler
| Tool | Description | Status | Documentation |
|------|-------------|--------|---------------|
| **Resolution Calculator** | Calculate upscaled dimensions with model optimization | ✅ Complete | [Docs](examples/documentation/resolution_calculator.md) |
| **Width Height Selector** | Preset-based dimension selection with 26 curated options | ✅ Complete | [Docs](examples/documentation/width_height_selector.md) |
| **Batch Image Processor** | Process multiple images with consistent settings | 🚧 Planned | Coming Soon |
| **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | 🚧 Planned | Coming Soon |
| **Model Management Tools** | Efficient model loading and memory management | 🚧 Planned | Coming Soon |
### Technical Specifications
@@ -130,6 +157,27 @@ Image Loader → Resolution Calculator → Upscaler
- Validates input tensors
- Graceful error handling
#### Width Height Selector
**Inputs:**
- `preset` (DROPDOWN): 26 preset options + custom
- `width` (INT): 64-8192, step 8, default 1024
- `height` (INT): 64-8192, step 8, default 1024
**Outputs:**
- `width` (INT): Selected or calculated width
- `height` (INT): Selected or calculated height
**UI Features:**
- Visual blue swap button in bottom-right corner
- Intelligent preset switching when swapping
- Modern hover effects and cursor feedback
**Preset Categories:**
- SDXL Presets (9): 1024×1024 to 1536×640 (~1MP optimized)
- FLUX Presets (8): 1920×1080 to 1152×1728 (high resolution)
- Ultra-Wide (8): 2560×1080 to 768×2304 (modern ratios)
## 🛠️ Development
### Prerequisites
@@ -250,7 +298,8 @@ MIT License - see [LICENSE](LICENSE) file for details.
## 📈 Stats
- **Nodes**: 1 (Resolution Calculator)
- **Nodes**: 2 (Resolution Calculator, Width Height Selector)
- **Presets**: 26 curated resolution presets
- **Test Coverage**: 100%
- **Python Version**: 3.8+
- **ComfyUI Compatibility**: Latest
+7 -1
View File
@@ -5,4 +5,10 @@ All nodes are grouped under the "ComfyAssets" category
from .kikotools import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
# Tell ComfyUI where to find our JavaScript extensions
WEB_DIRECTORY = "./web"
# Print startup message
print("\033[94m[ComfyUI-KikoTools] Loaded with swap button support!\033[0m")
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
@@ -0,0 +1,195 @@
# Width Height Selector Documentation
## Overview
The Width Height Selector is a ComfyUI node that provides preset-based dimension selection with swap functionality, optimized for SDXL and FLUX models. It offers 26 carefully curated resolution presets plus custom dimension support.
## Features
### 🎯 **Preset Categories**
- **SDXL Presets** (9): ~1MP optimized resolutions
- **FLUX Presets** (8): Higher resolution options
- **Ultra-Wide Presets** (8): Modern aspect ratios
- **Custom**: Manual dimension input
### 🔄 **Smart Swap Logic**
- Visual swap button in bottom-right corner
- Intelligent orientation switching for presets
- Simple value swap for custom dimensions
- Maintains proper aspect ratios
### 🎨 **Enhanced UI**
- Modern blue swap button with hover effects
- Hidden swap parameter (controlled by button)
- Clean interface with visual feedback
### ✅ **Built-in Validation**
- Ensures divisible-by-8 constraint
- Auto-sanitization of invalid inputs
- Proper error handling and fallbacks
## Node Interface
### Inputs
- **preset**: Dropdown with 26 preset options + custom
- **width**: Custom width (64-8192, step 8)
- **height**: Custom height (64-8192, step 8)
### Outputs
- **width**: Integer width value
- **height**: Integer height value
## Preset Reference
### SDXL Presets (~1 Megapixel)
| Preset | Dimensions | Aspect Ratio | Use Case |
|--------|------------|--------------|----------|
| 1024×1024 | 1024×1024 | 1:1 | Square, base SDXL |
| 896×1152 | 896×1152 | 7:9 | Moderate portrait |
| 832×1216 | 832×1216 | 13:19 | Standard portrait |
| 768×1344 | 768×1344 | 4:7 | Tall portrait |
| 640×1536 | 640×1536 | 5:12 | Very tall portrait |
| 1152×896 | 1152×896 | 9:7 | Moderate landscape |
| 1216×832 | 1216×832 | 19:13 | Standard landscape |
| 1344×768 | 1344×768 | 7:4 | Wide landscape |
| 1536×640 | 1536×640 | 12:5 | Very wide landscape |
### FLUX Presets (High Resolution)
| Preset | Dimensions | Aspect Ratio | Use Case |
|--------|------------|--------------|----------|
| 1920×1080 | 1920×1080 | 16:9 | Full HD, best quality/speed |
| 1536×1536 | 1536×1536 | 1:1 | High-res square |
| 1280×768 | 1280×768 | 5:3 | Cinematic wide |
| 768×1280 | 768×1280 | 3:5 | Mobile optimized |
| 1440×1080 | 1440×1080 | 4:3 | Classic aspect ratio |
| 1080×1440 | 1080×1440 | 3:4 | Classic portrait |
| 1728×1152 | 1728×1152 | 3:2 | Photography standard |
| 1152×1728 | 1152×1728 | 2:3 | Portrait photography |
### Ultra-Wide Presets (Modern Ratios)
| Preset | Dimensions | Aspect Ratio | Use Case |
|--------|------------|--------------|----------|
| 2560×1080 | 2560×1080 | 64:27 | Ultra-wide gaming |
| 2048×768 | 2048×768 | 8:3 | Wide cinematic |
| 1792×768 | 1792×768 | 7:3 | Panoramic |
| 2304×768 | 2304×768 | 3:1 | Banner landscape |
| 1080×2560 | 1080×2560 | 27:64 | Mobile ultra-tall |
| 768×2048 | 768×2048 | 3:8 | Vertical cinematic |
| 768×1792 | 768×1792 | 3:7 | Vertical panoramic |
| 768×2304 | 768×2304 | 1:3 | Banner portrait |
## Usage Examples
### Basic Usage
1. **Select Preset**: Choose from dropdown (e.g., "1920×1080")
2. **Connect Outputs**: Link width/height to your target nodes
3. **Use Swap Button**: Click blue button in bottom-right corner to swap orientation
### Custom Dimensions
1. **Set Preset**: Select "custom"
2. **Enter Dimensions**: Input width and height manually
3. **Validation**: Automatic sanitization to divisible-by-8
### Orientation Swapping
1. **Choose Preset**: Any preset (e.g., "1920×1080")
2. **Click Swap Button**: Blue button in bottom-right corner of node
3. **Result**: Gets 1080×1920 (landscape → portrait) or switches to equivalent preset if available
### Integration with Resolution Calculator
```
Width Height Selector → EmptyLatentImage → Resolution Calculator
```
1. Set base dimensions with Width Height Selector
2. Create latent image with those dimensions
3. Calculate upscaled dimensions with Resolution Calculator
## Common Workflows
### SDXL Portrait Generation
```
Width Height Selector (832×1216) → EmptyLatentImage → SDXL Pipeline
```
- Perfect for portrait generation
- Optimized ~1MP resolution
- 13:19 aspect ratio
### FLUX HD Generation
```
Width Height Selector (1920×1080) → EmptyLatentImage → FLUX Pipeline
```
- Best quality/speed balance
- Full HD resolution
- 16:9 cinematic aspect ratio
### Ultra-Wide Panoramic
```
Width Height Selector (2560×1080) → EmptyLatentImage → Pipeline
```
- Modern ultra-wide format
- Great for panoramic scenes
- 64:27 gaming aspect ratio
### Upscaling Workflow
```
Width Height Selector → EmptyLatentImage → Resolution Calculator → Upscaler
```
1. Base dimensions from selector
2. Generate at base resolution
3. Calculate upscale dimensions
4. Feed to upscaler node
## Tips and Best Practices
### Model Optimization
- **SDXL**: Use SDXL presets for best results (~1MP)
- **FLUX**: Use FLUX presets for optimal quality/speed
- **Custom**: Ensure divisible by 8 for all models
### Aspect Ratio Considerations
- **Portrait**: 3:4, 2:3, 13:19 work well for people
- **Landscape**: 16:9, 19:13, 7:4 for scenes and objects
- **Square**: 1:1 for centered compositions
- **Ultra-wide**: 21:9+ for panoramic and cinematic shots
### Memory Management
- Higher resolutions use more VRAM
- SDXL presets are memory-efficient
- FLUX presets require more resources
- Ultra-wide presets need substantial VRAM
### Workflow Integration
- Always connect width/height outputs
- Use swap button for quick orientation changes
- Combine with Resolution Calculator for upscaling
- Link to EmptyLatentImage for generation
## Troubleshooting
### Common Issues
- **Invalid dimensions**: Auto-sanitized to nearest valid values
- **Memory errors**: Use lower resolution presets
- **Orientation wrong**: Use the blue swap button instead of manual input
- **Swap button not visible**: Ensure node is not collapsed
### Error Handling
- Invalid presets fall back to custom dimensions
- Out-of-range values are clamped and rounded
- Division-by-8 constraint automatically enforced
## Technical Details
### Validation Rules
- Width/Height: 64-8192 pixels
- Must be divisible by 8
- Positive integers only
### Swap Button Implementation
- JavaScript-based visual interface
- Intelligent preset switching when possible
- Falls back to custom dimensions when needed
- Modern blue styling with hover effects
### Preset Organization
- Categorized by model optimization
- Sorted by aspect ratio within categories
- Comprehensive tooltips for each preset
@@ -0,0 +1,422 @@
{
"last_node_id": 12,
"last_link_id": 12,
"nodes": [
{
"id": 1,
"type": "WidthHeightSelector",
"pos": [100, 100],
"size": [300, 180],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "width",
"type": "INT",
"links": [1, 7],
"shape": 3,
"slot_index": 0
},
{
"name": "height",
"type": "INT",
"links": [2, 8],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "WidthHeightSelector"
},
"widgets_values": [
"1920×1080",
1024,
1024
],
"title": "Width Height Selector (FLUX HD)"
},
{
"id": 2,
"type": "WidthHeightSelector",
"pos": [100, 350],
"size": [300, 180],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "width",
"type": "INT",
"links": [3, 9],
"shape": 3,
"slot_index": 0
},
{
"name": "height",
"type": "INT",
"links": [4, 10],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "WidthHeightSelector"
},
"widgets_values": [
"832×1216",
1024,
1024
],
"title": "Width Height Selector (SDXL Portrait) - Use swap button!"
},
{
"id": 3,
"type": "WidthHeightSelector",
"pos": [100, 600],
"size": [300, 180],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "width",
"type": "INT",
"links": [5, 11],
"shape": 3,
"slot_index": 0
},
{
"name": "height",
"type": "INT",
"links": [6, 12],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "WidthHeightSelector"
},
"widgets_values": [
"custom",
1536,
768
],
"title": "Width Height Selector (Custom 2:1)"
},
{
"id": 4,
"type": "EmptyLatentImage",
"pos": [500, 100],
"size": [300, 100],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "width",
"type": "INT",
"link": 1,
"widget": {
"name": "width"
}
},
{
"name": "height",
"type": "INT",
"link": 2,
"widget": {
"name": "height"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
1920,
1080,
1
],
"title": "FLUX HD Latent (1920×1080)"
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [500, 350],
"size": [300, 100],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "width",
"type": "INT",
"link": 3,
"widget": {
"name": "width"
}
},
{
"name": "height",
"type": "INT",
"link": 4,
"widget": {
"name": "height"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
832,
1216,
1
],
"title": "SDXL Portrait Latent (832×1216)"
},
{
"id": 6,
"type": "EmptyLatentImage",
"pos": [500, 600],
"size": [300, 100],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "width",
"type": "INT",
"link": 5,
"widget": {
"name": "width"
}
},
{
"name": "height",
"type": "INT",
"link": 6,
"widget": {
"name": "height"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
1536,
768,
1
],
"title": "Custom Latent (1536×768)"
},
{
"id": 7,
"type": "ResolutionCalculator",
"pos": [900, 100],
"size": [300, 150],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "width",
"type": "INT",
"links": [],
"shape": 3,
"slot_index": 0
},
{
"name": "height",
"type": "INT",
"links": [],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "ResolutionCalculator"
},
"widgets_values": [
1.5
],
"title": "Resolution Calculator Demo (1.5x)"
},
{
"id": 8,
"type": "ShowText",
"pos": [900, 350],
"size": [300, 100],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": null,
"widget": {
"name": "text"
}
}
],
"outputs": [],
"properties": {
"Node name for S&R": "ShowText"
},
"widgets_values": [
"Width Height Selector outputs clean INT values ready for any ComfyUI node"
],
"title": "Usage Note"
},
{
"id": 9,
"type": "ShowText",
"pos": [900, 500],
"size": [300, 150],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": null,
"widget": {
"name": "text"
}
}
],
"outputs": [],
"properties": {
"Node name for S&R": "ShowText"
},
"widgets_values": [
"Instructions:\n1. Select presets from dropdown\n2. Use blue swap button (bottom-right) to change orientation\n3. Choose 'custom' for manual dimensions\n4. Connect width/height to any node needing dimensions"
],
"title": "How to Use"
},
{
"id": 10,
"type": "PrimitiveNode",
"pos": [900, 700],
"size": [300, 100],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [7, 8, 9, 10, 11, 12],
"shape": 3,
"slot_index": 0,
"widget": {
"name": "value"
}
}
],
"properties": {
"Node name for S&R": "PrimitiveNode"
},
"widgets_values": [
1024
],
"title": "Connected Outputs Demo"
}
],
"links": [
[1, 1, 0, 4, 0, "INT"],
[2, 1, 1, 4, 1, "INT"],
[3, 2, 0, 5, 0, "INT"],
[4, 2, 1, 5, 1, "INT"],
[5, 3, 0, 6, 0, "INT"],
[6, 3, 1, 6, 1, "INT"],
[7, 1, 0, 10, 0, "INT"],
[8, 1, 1, 10, 0, "INT"],
[9, 2, 0, 10, 0, "INT"],
[10, 2, 1, 10, 0, "INT"],
[11, 3, 0, 10, 0, "INT"],
[12, 3, 1, 10, 0, "INT"]
],
"groups": [
{
"title": "Width Height Selector Examples",
"bounding": [80, 40, 360, 780],
"color": "#3f789e",
"font_size": 24,
"locked": false
},
{
"title": "Example Usage in Workflows",
"bounding": [480, 40, 360, 280],
"color": "#8A5A2B",
"font_size": 24,
"locked": false
},
{
"title": "Documentation & Demos",
"bounding": [880, 40, 360, 780],
"color": "#8B5A8C",
"font_size": 24,
"locked": false
}
],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
},
"groupNodes": {},
"info": {
"name": "width_height_selector_example",
"author": "ComfyUI-KikoTools",
"description": "Demonstrates Width Height Selector with swap button functionality and various preset options. Shows integration with EmptyLatentImage and other nodes.",
"version": "2.0",
"created": "2024-12-14",
"modified": "2024-12-14"
}
},
"version": 0.4
}
+3
View File
@@ -4,14 +4,17 @@ Handles automatic discovery and registration of all ComfyAssets tools
"""
from .tools.resolution_calculator import ResolutionCalculatorNode
from .tools.width_height_selector import WidthHeightSelectorNode
# ComfyUI node registration mappings
NODE_CLASS_MAPPINGS = {
"ResolutionCalculator": ResolutionCalculatorNode,
"WidthHeightSelector": WidthHeightSelectorNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ResolutionCalculator": "Resolution Calculator",
"WidthHeightSelector": "Width Height Selector",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
@@ -0,0 +1,5 @@
"""Width Height Selector tool for ComfyUI-KikoTools."""
from .node import WidthHeightSelectorNode
__all__ = ["WidthHeightSelectorNode"]
@@ -0,0 +1,264 @@
"""Core logic for Width Height Selector tool."""
from typing import Tuple
from math import gcd
from .presets import PRESET_OPTIONS
def get_preset_dimensions(
preset: str, custom_width: int, custom_height: int
) -> Tuple[int, int]:
"""
Get dimensions from preset name or use custom dimensions.
Args:
preset: Preset name (e.g., "1024×1024", "custom")
custom_width: Custom width value
custom_height: Custom height value
Returns:
Tuple of (width, height) as integers
"""
if preset == "custom" or preset not in PRESET_OPTIONS:
return custom_width, custom_height
return PRESET_OPTIONS[preset]
def apply_swap_logic(width: int, height: int, swap_enabled: bool) -> Tuple[int, int]:
"""
Apply swap logic to dimensions.
Args:
width: Original width
height: Original height
swap_enabled: Whether to swap dimensions
Returns:
Tuple of (width, height) after potential swap
"""
if swap_enabled:
return height, width
return width, height
def calculate_aspect_ratio(width: int, height: int) -> str:
"""
Calculate and format aspect ratio as a string.
Args:
width: Image width
height: Image height
Returns:
Formatted aspect ratio string (e.g., "16:9", "4:3", "1:1")
"""
if width <= 0 or height <= 0:
return "Invalid"
# Calculate greatest common divisor to simplify ratio
ratio_gcd = gcd(width, height)
ratio_width = width // ratio_gcd
ratio_height = height // ratio_gcd
return f"{ratio_width}:{ratio_height}"
def validate_dimensions(width: int, height: int) -> bool:
"""
Validate that dimensions meet ComfyUI requirements.
Args:
width: Image width to validate
height: Image height to validate
Returns:
True if dimensions are valid, False otherwise
"""
# Check positive values
if width <= 0 or height <= 0:
return False
# Check divisible by 8 (ComfyUI requirement)
if width % 8 != 0 or height % 8 != 0:
return False
# Check reasonable bounds (64 minimum, 8192 maximum)
if not (64 <= width <= 8192) or not (64 <= height <= 8192):
return False
return True
def sanitize_dimensions(width: int, height: int) -> Tuple[int, int]:
"""
Sanitize dimensions to meet ComfyUI requirements.
Rounds to nearest multiple of 8 and clamps to valid range.
Args:
width: Raw width value
height: Raw height value
Returns:
Tuple of sanitized (width, height)
"""
# Clamp to valid range
width = max(64, min(8192, width))
height = max(64, min(8192, height))
# Round to nearest multiple of 8
width = ((width + 4) // 8) * 8
height = ((height + 4) // 8) * 8
return width, height
def get_dimension_info(
preset: str, width: int, height: int, swap_enabled: bool
) -> dict:
"""
Get comprehensive dimension information including metadata.
Args:
preset: Preset name
width: Width value
height: Height value
swap_enabled: Whether swap is enabled
Returns:
Dictionary with dimension info and metadata
"""
# Get base dimensions from preset or custom
base_width, base_height = get_preset_dimensions(preset, width, height)
# Apply swap if enabled
final_width, final_height = apply_swap_logic(base_width, base_height, swap_enabled)
# Calculate metadata
aspect_ratio = calculate_aspect_ratio(final_width, final_height)
is_valid = validate_dimensions(final_width, final_height)
is_square = final_width == final_height
is_landscape = final_width > final_height
is_portrait = final_height > final_width
# Calculate megapixels
megapixels = (final_width * final_height) / 1_000_000
return {
"width": final_width,
"height": final_height,
"aspect_ratio": aspect_ratio,
"is_valid": is_valid,
"is_square": is_square,
"is_landscape": is_landscape,
"is_portrait": is_portrait,
"megapixels": round(megapixels, 2),
"preset_used": preset,
"swap_applied": swap_enabled,
}
def format_dimension_string(width: int, height: int) -> str:
"""
Format dimensions as a readable string.
Args:
width: Image width
height: Image height
Returns:
Formatted dimension string (e.g., "1920×1080")
"""
return f"{width}×{height}"
def parse_dimension_string(dimension_str: str) -> Tuple[int, int]:
"""
Parse dimension string back to width/height integers.
Args:
dimension_str: Dimension string (e.g., "1920×1080", "1920x1080")
Returns:
Tuple of (width, height) as integers
Raises:
ValueError: If dimension string cannot be parsed
"""
# Handle both × and x separators
if "×" in dimension_str:
parts = dimension_str.split("×")
elif "x" in dimension_str:
parts = dimension_str.split("x")
else:
raise ValueError(f"Invalid dimension string format: {dimension_str}")
if len(parts) != 2:
raise ValueError(f"Dimension string must have exactly 2 parts: {dimension_str}")
try:
width = int(parts[0].strip())
height = int(parts[1].strip())
return width, height
except ValueError as e:
raise ValueError(f"Could not parse dimensions from {dimension_str}: {e}")
def get_optimal_scale_factor(
current_width: int, current_height: int, target_width: int, target_height: int
) -> float:
"""
Calculate optimal scale factor to get from current to target dimensions.
Args:
current_width: Current image width
current_height: Current image height
target_width: Target width
target_height: Target height
Returns:
Scale factor as float
"""
if current_width <= 0 or current_height <= 0:
return 1.0
width_scale = target_width / current_width
height_scale = target_height / current_height
# Return the average scale factor
return (width_scale + height_scale) / 2.0
def suggest_similar_presets(width: int, height: int, max_suggestions: int = 3) -> list:
"""
Suggest similar presets based on current dimensions.
Args:
width: Current width
height: Current height
max_suggestions: Maximum number of suggestions to return
Returns:
List of preset names sorted by similarity
"""
if width <= 0 or height <= 0:
return []
current_ratio = width / height
suggestions = []
for preset_name, (preset_width, preset_height) in PRESET_OPTIONS.items():
if preset_name == "custom":
continue
preset_ratio = preset_width / preset_height
ratio_diff = abs(current_ratio - preset_ratio)
suggestions.append((preset_name, ratio_diff))
# Sort by similarity (smallest ratio difference first)
suggestions.sort(key=lambda x: x[1])
# Return just the preset names
return [preset[0] for preset in suggestions[:max_suggestions]]
@@ -0,0 +1,207 @@
"""Width Height Selector node for ComfyUI."""
from typing import Tuple
from ...base.base_node import ComfyAssetsBaseNode
from .logic import (
get_preset_dimensions,
calculate_aspect_ratio,
validate_dimensions,
sanitize_dimensions,
)
from .presets import (
PRESET_OPTIONS,
PRESET_DESCRIPTIONS,
get_model_recommendation,
)
class WidthHeightSelectorNode(ComfyAssetsBaseNode):
"""
Width Height Selector node for selecting image dimensions.
Provides preset-based dimension selection with swap functionality,
optimized for SDXL and FLUX models with comprehensive aspect ratio support.
"""
@classmethod
def INPUT_TYPES(cls):
"""Define the input types for the ComfyUI node."""
# Get all preset options excluding the custom tuple
preset_keys = [key for key in PRESET_OPTIONS.keys()]
return {
"required": {
"preset": (
preset_keys,
{
"default": "custom",
"tooltip": "Select from optimized resolution presets or use custom dimensions. "
"SDXL presets are ~1MP, FLUX presets are higher resolution, "
"Ultra-wide presets support modern aspect ratios.",
},
),
"width": (
"INT",
{
"default": 1024,
"min": 64,
"max": 8192,
"step": 8,
"tooltip": "Custom width in pixels (must be multiple of 8). "
"Used when preset is 'custom' or as fallback for invalid presets.",
},
),
"height": (
"INT",
{
"default": 1024,
"min": 64,
"max": 8192,
"step": 8,
"tooltip": "Custom height in pixels (must be multiple of 8). "
"Used when preset is 'custom' or as fallback for invalid presets.",
},
),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "get_dimensions"
CATEGORY = "ComfyAssets"
def get_dimensions(self, preset: str, width: int, height: int) -> Tuple[int, int]:
"""
Get width and height dimensions with preset and swap support.
Args:
preset: Selected preset name or "custom"
width: Custom width value
height: Custom height value
Returns:
Tuple of (width, height)
"""
try:
# Get base dimensions from preset or custom input
final_width, final_height = get_preset_dimensions(preset, width, height)
# Sanitize dimensions to ensure they meet ComfyUI requirements
final_width, final_height = sanitize_dimensions(final_width, final_height)
# Validate final dimensions
if not validate_dimensions(final_width, final_height):
# This should not happen after sanitization, but handle gracefully
self.handle_error(
f"Generated invalid dimensions: {final_width}×{final_height}. "
f"Using fallback dimensions 1024×1024."
)
final_width, final_height = 1024, 1024
return (final_width, final_height)
except Exception as e:
# Handle any unexpected errors gracefully
error_msg = (
f"Error processing dimensions: {str(e)}. Using fallback 1024×1024."
)
self.handle_error(error_msg)
return (1024, 1024)
def get_preset_info(self, preset: str) -> str:
"""
Get descriptive information about a preset.
Args:
preset: Preset name
Returns:
Description string for the preset
"""
if preset == "custom":
return "Custom dimensions - use the width and height inputs below"
if preset in PRESET_DESCRIPTIONS:
return PRESET_DESCRIPTIONS[preset]
# Fallback for unknown presets
if preset in PRESET_OPTIONS:
width, height = PRESET_OPTIONS[preset]
aspect_ratio = calculate_aspect_ratio(width, height)
return f"{preset} - {aspect_ratio} aspect ratio"
return f"Unknown preset: {preset}"
def get_model_optimization(self, preset: str) -> str:
"""
Get model optimization information for a preset.
Args:
preset: Preset name
Returns:
Optimization information string
"""
return get_model_recommendation(preset)
def validate_inputs(self, preset: str, width: int, height: int) -> bool:
"""
Validate node inputs.
Args:
preset: Preset name
width: Width value
height: Height value
Returns:
True if inputs are valid
"""
# Check if preset exists or is custom
if preset != "custom" and preset not in PRESET_OPTIONS:
return False
# For custom preset, validate dimensions
if preset == "custom":
if not validate_dimensions(width, height):
return False
return True
@classmethod
def get_preset_list(cls) -> list:
"""
Get list of available presets for external use.
Returns:
List of preset names
"""
return list(PRESET_OPTIONS.keys())
@classmethod
def get_preset_dimensions_static(cls, preset: str) -> Tuple[int, int]:
"""
Static method to get preset dimensions without node instance.
Args:
preset: Preset name
Returns:
Tuple of (width, height) or (0, 0) if invalid
"""
if preset in PRESET_OPTIONS:
return PRESET_OPTIONS[preset]
return (0, 0)
def __str__(self) -> str:
"""String representation of the node."""
return f"WidthHeightSelectorNode(presets={len(PRESET_OPTIONS)})"
def __repr__(self) -> str:
"""Detailed string representation of the node."""
return (
f"WidthHeightSelectorNode("
f"presets={len(PRESET_OPTIONS)}, "
f"category='{self.CATEGORY}', "
f"function='{self.FUNCTION}'"
f")"
)
@@ -0,0 +1,152 @@
"""Preset definitions for Width Height Selector."""
from typing import Dict, Tuple
# SDXL optimized presets (~1 megapixel, dimensions divisible by 8)
SDXL_PRESETS: Dict[str, Tuple[int, int]] = {
# Square
"1024×1024": (1024, 1024), # 1:1 - Base SDXL resolution
# Portrait ratios
"896×1152": (896, 1152), # 7:9 - Moderate portrait
"832×1216": (832, 1216), # 13:19 - Standard portrait
"768×1344": (768, 1344), # 4:7 - Tall portrait
"640×1536": (640, 1536), # 5:12 - Very tall portrait
# Landscape ratios
"1152×896": (1152, 896), # 9:7 - Moderate landscape
"1216×832": (1216, 832), # 19:13 - Standard landscape
"1344×768": (1344, 768), # 7:4 - Wide landscape
"1536×640": (1536, 640), # 12:5 - Very wide landscape
}
# FLUX optimized presets (higher resolution, flexible ratios)
FLUX_PRESETS: Dict[str, Tuple[int, int]] = {
# Recommended high-quality resolutions
"1920×1080": (1920, 1080), # 16:9 - Full HD landscape
"1536×1536": (1536, 1536), # 1:1 - High-res square
"1280×768": (1280, 768), # 5:3 - Wide landscape
"768×1280": (768, 1280), # 3:5 - Tall portrait
# Alternative quality resolutions
"1440×1080": (1440, 1080), # 4:3 - Classic aspect ratio
"1080×1440": (1080, 1440), # 3:4 - Classic portrait
"1728×1152": (1728, 1152), # 3:2 - Photography standard
"1152×1728": (1152, 1728), # 2:3 - Portrait photography
}
# Ultra-wide and modern aspect ratios
ULTRA_WIDE_PRESETS: Dict[str, Tuple[int, int]] = {
# Ultra-wide landscape (21:9 and variants)
"2560×1080": (2560, 1080), # 64:27 - Ultra-wide gaming
"2048×768": (2048, 768), # 8:3 - Wide cinematic
"1792×768": (1792, 768), # 7:3 - Panoramic
# Ultra-wide portrait
"1080×2560": (1080, 2560), # 27:64 - Mobile ultra-tall
"768×2048": (768, 2048), # 3:8 - Vertical cinematic
"768×1792": (768, 1792), # 3:7 - Vertical panoramic
# Extreme ratios
"2304×768": (2304, 768), # 3:1 - Banner landscape
"768×2304": (768, 2304), # 1:3 - Banner portrait
}
# Combined preset options for ComfyUI dropdown
PRESET_OPTIONS: Dict[str, Tuple[int, int]] = {
"custom": (0, 0), # Special case for custom dimensions
**SDXL_PRESETS,
**FLUX_PRESETS,
**ULTRA_WIDE_PRESETS,
}
# Organized preset categories for better UX
PRESET_CATEGORIES = {
"Custom": ["custom"],
"SDXL Square": ["1024×1024"],
"SDXL Portrait": ["896×1152", "832×1216", "768×1344", "640×1536"],
"SDXL Landscape": ["1152×896", "1216×832", "1344×768", "1536×640"],
"FLUX Recommended": ["1920×1080", "1536×1536", "1280×768", "768×1280"],
"FLUX Alternative": ["1440×1080", "1080×1440", "1728×1152", "1152×1728"],
"Ultra-Wide Landscape": ["2560×1080", "2048×768", "1792×768", "2304×768"],
"Ultra-Wide Portrait": ["1080×2560", "768×2048", "768×1792", "768×2304"],
}
# Preset descriptions for tooltips
PRESET_DESCRIPTIONS = {
# SDXL presets
"1024×1024": "SDXL base resolution - perfect square",
"896×1152": "SDXL portrait 7:9 - moderate portrait",
"832×1216": "SDXL portrait 13:19 - standard portrait",
"768×1344": "SDXL portrait 4:7 - tall portrait",
"640×1536": "SDXL portrait 5:12 - very tall portrait",
"1152×896": "SDXL landscape 9:7 - moderate landscape",
"1216×832": "SDXL landscape 19:13 - standard landscape",
"1344×768": "SDXL landscape 7:4 - wide landscape",
"1536×640": "SDXL landscape 12:5 - very wide landscape",
# FLUX presets
"1920×1080": "FLUX Full HD 16:9 - best quality/speed balance",
"1536×1536": "FLUX high-res square - premium quality",
"1280×768": "FLUX 5:3 landscape - cinematic wide",
"768×1280": "FLUX 3:5 portrait - mobile optimized",
"1440×1080": "FLUX 4:3 classic - traditional aspect ratio",
"1080×1440": "FLUX 3:4 portrait - classic portrait",
"1728×1152": "FLUX 3:2 photo - photography standard",
"1152×1728": "FLUX 2:3 portrait - portrait photography",
# Ultra-wide presets
"2560×1080": "Ultra-wide 64:27 - gaming/panoramic",
"2048×768": "Wide cinematic 8:3 - movie aspect",
"1792×768": "Panoramic 7:3 - landscape vista",
"2304×768": "Banner 3:1 - extreme wide banner",
"1080×2560": "Mobile ultra-tall 27:64 - modern phones",
"768×2048": "Vertical cinematic 3:8 - portrait video",
"768×1792": "Vertical panoramic 3:7 - tall vista",
"768×2304": "Vertical banner 1:3 - extreme tall banner",
}
# Model-specific recommendations
MODEL_RECOMMENDATIONS = {
"SDXL": list(SDXL_PRESETS.keys()),
"FLUX": list(FLUX_PRESETS.keys()),
"Ultra-Wide": list(ULTRA_WIDE_PRESETS.keys()),
}
def get_preset_category(preset_name: str) -> str:
"""Get the category for a given preset name."""
for category, presets in PRESET_CATEGORIES.items():
if preset_name in presets:
return category
return "Unknown"
def get_model_recommendation(preset_name: str) -> str:
"""Get model recommendation for a given preset."""
if preset_name in SDXL_PRESETS:
return "Optimized for SDXL"
elif preset_name in FLUX_PRESETS:
return "Optimized for FLUX"
elif preset_name in ULTRA_WIDE_PRESETS:
return "Modern ultra-wide ratios"
else:
return "Custom dimensions"
def validate_preset_dimensions() -> bool:
"""Validate that all presets meet ComfyUI requirements."""
all_presets = {**SDXL_PRESETS, **FLUX_PRESETS, **ULTRA_WIDE_PRESETS}
for preset_name, (width, height) in all_presets.items():
# Check divisible by 8
if width % 8 != 0 or height % 8 != 0:
print(
f"ERROR: {preset_name} dimensions not divisible by 8: {width}×{height}"
)
return False
# Check reasonable bounds
if not (64 <= width <= 8192) or not (64 <= height <= 8192):
print(f"ERROR: {preset_name} dimensions out of bounds: {width}×{height}")
return False
return True
# Validate presets on import
if not validate_preset_dimensions():
raise ValueError("Preset validation failed - check console for details")
@@ -0,0 +1,263 @@
"""Tests for Width Height Selector tool."""
import pytest
from unittest.mock import Mock
from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode
from kikotools.tools.width_height_selector.logic import (
get_preset_dimensions,
calculate_aspect_ratio,
validate_dimensions,
)
from kikotools.tools.width_height_selector.presets import (
PRESET_OPTIONS,
SDXL_PRESETS,
FLUX_PRESETS,
ULTRA_WIDE_PRESETS,
)
class TestWidthHeightSelectorNode:
"""Test the WidthHeightSelectorNode class."""
def setup_method(self):
"""Set up test fixtures."""
self.node = WidthHeightSelectorNode()
def test_node_structure(self):
"""Test that node has required ComfyUI structure."""
# Test INPUT_TYPES
input_types = self.node.INPUT_TYPES()
assert "required" in input_types
assert "preset" in input_types["required"]
assert "width" in input_types["required"]
assert "height" in input_types["required"]
# Test return types
assert self.node.RETURN_TYPES == ("INT", "INT")
assert self.node.RETURN_NAMES == ("width", "height")
assert self.node.FUNCTION == "get_dimensions"
assert self.node.CATEGORY == "ComfyAssets"
def test_custom_dimensions(self):
"""Test custom dimensions."""
result = self.node.get_dimensions(preset="custom", width=1920, height=1080)
assert result == (1920, 1080)
def test_sdxl_square_preset(self):
"""Test SDXL square preset."""
result = self.node.get_dimensions(
preset="1024×1024",
width=512, # Should be ignored
height=512, # Should be ignored
)
assert result == (1024, 1024)
def test_sdxl_portrait_preset(self):
"""Test SDXL portrait preset."""
result = self.node.get_dimensions(preset="832×1216", width=512, height=512)
assert result == (832, 1216)
def test_sdxl_landscape_preset(self):
"""Test SDXL landscape preset."""
result = self.node.get_dimensions(preset="1216×832", width=512, height=512)
assert result == (1216, 832)
def test_flux_preset(self):
"""Test FLUX preset."""
result = self.node.get_dimensions(preset="1920×1080", width=512, height=512)
assert result == (1920, 1080)
def test_ultra_wide_preset(self):
"""Test ultra-wide preset."""
result = self.node.get_dimensions(preset="2560×1080", width=512, height=512)
assert result == (2560, 1080)
def test_all_presets_available(self):
"""Test that all presets are available in INPUT_TYPES."""
input_types = self.node.INPUT_TYPES()
available_presets = input_types["required"]["preset"][0]
# Check that all major preset categories are available
assert "custom" in available_presets
assert "1024×1024" in available_presets # SDXL square
assert "832×1216" in available_presets # SDXL portrait
assert "1216×832" in available_presets # SDXL landscape
assert "1920×1080" in available_presets # FLUX
assert "2560×1080" in available_presets # Ultra-wide
def test_invalid_preset_fallback(self):
"""Test handling of invalid preset."""
# Should fall back to custom dimensions
result = self.node.get_dimensions(
preset="invalid_preset", width=800, height=600
)
assert result == (800, 600)
class TestPresetLogic:
"""Test the preset logic functions."""
def test_get_preset_dimensions_custom(self):
"""Test getting custom dimensions."""
width, height = get_preset_dimensions("custom", 1920, 1080)
assert width == 1920
assert height == 1080
def test_get_preset_dimensions_sdxl(self):
"""Test getting SDXL preset dimensions."""
width, height = get_preset_dimensions("1024×1024", 512, 512)
assert width == 1024
assert height == 1024
def test_get_preset_dimensions_flux(self):
"""Test getting FLUX preset dimensions."""
width, height = get_preset_dimensions("1920×1080", 512, 512)
assert width == 1920
assert height == 1080
def test_get_preset_dimensions_invalid(self):
"""Test getting dimensions for invalid preset."""
width, height = get_preset_dimensions("invalid", 800, 600)
assert width == 800
assert height == 600
class TestDimensionValidation:
"""Test dimension validation."""
def test_validate_dimensions_valid(self):
"""Test validation of valid dimensions."""
assert validate_dimensions(1024, 1024) is True
assert validate_dimensions(1920, 1080) is True
assert validate_dimensions(832, 1216) is True
def test_validate_dimensions_divisible_by_8(self):
"""Test that dimensions must be divisible by 8."""
assert validate_dimensions(1024, 1024) is True # Both divisible by 8
assert validate_dimensions(1025, 1024) is False # Width not divisible by 8
assert validate_dimensions(1024, 1025) is False # Height not divisible by 8
assert validate_dimensions(1025, 1025) is False # Neither divisible by 8
def test_validate_dimensions_minimum_size(self):
"""Test minimum dimension requirements."""
assert validate_dimensions(64, 64) is True # Minimum allowed
assert validate_dimensions(32, 64) is False # Width too small
assert validate_dimensions(64, 32) is False # Height too small
assert validate_dimensions(32, 32) is False # Both too small
def test_validate_dimensions_maximum_size(self):
"""Test maximum dimension requirements."""
assert validate_dimensions(8192, 8192) is True # Maximum allowed
assert validate_dimensions(8200, 8192) is False # Width too large
assert validate_dimensions(8192, 8200) is False # Height too large
assert validate_dimensions(8200, 8200) is False # Both too large
class TestPresetDefinitions:
"""Test preset definitions."""
def test_sdxl_presets_structure(self):
"""Test SDXL presets are properly defined."""
assert "1024×1024" in SDXL_PRESETS
assert "832×1216" in SDXL_PRESETS
assert "1216×832" in SDXL_PRESETS
# Check dimensions are tuples
for preset, dims in SDXL_PRESETS.items():
assert isinstance(dims, tuple)
assert len(dims) == 2
assert isinstance(dims[0], int)
assert isinstance(dims[1], int)
def test_flux_presets_structure(self):
"""Test FLUX presets are properly defined."""
assert "1920×1080" in FLUX_PRESETS
assert "1536×1536" in FLUX_PRESETS
# Check dimensions are tuples
for preset, dims in FLUX_PRESETS.items():
assert isinstance(dims, tuple)
assert len(dims) == 2
assert isinstance(dims[0], int)
assert isinstance(dims[1], int)
def test_ultra_wide_presets_structure(self):
"""Test ultra-wide presets are properly defined."""
assert "2560×1080" in ULTRA_WIDE_PRESETS
# Check dimensions are tuples
for preset, dims in ULTRA_WIDE_PRESETS.items():
assert isinstance(dims, tuple)
assert len(dims) == 2
assert isinstance(dims[0], int)
assert isinstance(dims[1], int)
def test_preset_options_combined(self):
"""Test that PRESET_OPTIONS combines all presets correctly."""
assert "custom" in PRESET_OPTIONS
# Check SDXL presets are included
for preset in SDXL_PRESETS:
assert preset in PRESET_OPTIONS
# Check FLUX presets are included
for preset in FLUX_PRESETS:
assert preset in PRESET_OPTIONS
# Check ultra-wide presets are included
for preset in ULTRA_WIDE_PRESETS:
assert preset in PRESET_OPTIONS
def test_all_presets_divisible_by_8(self):
"""Test that all preset dimensions are divisible by 8."""
for preset_dict in [SDXL_PRESETS, FLUX_PRESETS, ULTRA_WIDE_PRESETS]:
for preset_name, (width, height) in preset_dict.items():
assert width % 8 == 0, f"{preset_name} width {width} not divisible by 8"
assert (
height % 8 == 0
), f"{preset_name} height {height} not divisible by 8"
def test_preset_dimensions_within_limits(self):
"""Test that all preset dimensions are within acceptable limits."""
for preset_dict in [SDXL_PRESETS, FLUX_PRESETS, ULTRA_WIDE_PRESETS]:
for preset_name, (width, height) in preset_dict.items():
assert 64 <= width <= 8192, f"{preset_name} width {width} out of range"
assert (
64 <= height <= 8192
), f"{preset_name} height {height} out of range"
class TestEdgeCases:
"""Test edge cases and error conditions."""
def test_zero_dimensions(self):
"""Test handling of zero dimensions."""
assert validate_dimensions(0, 1024) is False
assert validate_dimensions(1024, 0) is False
assert validate_dimensions(0, 0) is False
def test_negative_dimensions(self):
"""Test handling of negative dimensions."""
assert validate_dimensions(-100, 1024) is False
assert validate_dimensions(1024, -100) is False
assert validate_dimensions(-100, -100) is False
def test_very_large_dimensions(self):
"""Test handling of very large dimensions."""
assert validate_dimensions(10000, 1024) is False
assert validate_dimensions(1024, 10000) is False
assert validate_dimensions(10000, 10000) is False
def test_aspect_ratio_edge_cases(self):
"""Test aspect ratio calculation edge cases."""
# Very wide aspect ratio
ratio = calculate_aspect_ratio(3840, 1080)
assert ratio == "32:9"
# Very tall aspect ratio
ratio = calculate_aspect_ratio(1080, 3840)
assert ratio == "9:32"
# Prime number dimensions
ratio = calculate_aspect_ratio(1920, 1080)
assert ratio == "16:9"
+304
View File
@@ -0,0 +1,304 @@
// ComfyUI-KikoTools - Width Height Selector with Swap Button
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "comfyassets.WidthHeightSelector",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name === "WidthHeightSelector") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
if (onNodeCreated) onNodeCreated.apply(this, []);
// Track button click state for visual feedback
this.swapButtonPressed = false;
// Override preset callback to update width/height widgets when preset changes
const presetWidget = this.widgets.find(w => w.name === "preset");
if (presetWidget) {
const originalCallback = presetWidget.callback;
presetWidget.callback = function(value, graphcanvas, node, pos, event) {
// Call original callback first
if (originalCallback) {
originalCallback.call(this, value, graphcanvas, node, pos, event);
}
// Update width/height widgets based on preset
const widthWidget = node.widgets.find(w => w.name === "width");
const heightWidget = node.widgets.find(w => w.name === "height");
if (widthWidget && heightWidget && value !== "custom") {
// Define all available presets from our preset system
const presetDimensions = {
// SDXL Presets
"1024×1024": [1024, 1024], "896×1152": [896, 1152], "832×1216": [832, 1216],
"768×1344": [768, 1344], "640×1536": [640, 1536], "1152×896": [1152, 896],
"1216×832": [1216, 832], "1344×768": [1344, 768], "1536×640": [1536, 640],
// FLUX Presets
"1920×1080": [1920, 1080], "1536×1536": [1536, 1536], "1280×768": [1280, 768],
"768×1280": [768, 1280], "1440×1080": [1440, 1080], "1080×1440": [1080, 1440],
"1728×1152": [1728, 1152], "1152×1728": [1152, 1728],
// Ultra-Wide Presets
"2560×1080": [2560, 1080], "2048×768": [2048, 768], "1792×768": [1792, 768],
"2304×768": [2304, 768], "1080×2560": [1080, 2560], "768×2048": [768, 2048],
"768×1792": [768, 1792], "768×2304": [768, 2304]
};
if (presetDimensions[value]) {
const [w, h] = presetDimensions[value];
widthWidget.value = w;
heightWidget.value = h;
// Trigger widget callbacks to update the UI
if (widthWidget.callback) {
widthWidget.callback(w, graphcanvas, node, pos, event);
}
if (heightWidget.callback) {
heightWidget.callback(h, graphcanvas, node, pos, event);
}
}
}
};
}
// Add swap functionality
this.swapDimensions = function() {
const widthWidget = this.widgets.find(w => w.name === "width");
const heightWidget = this.widgets.find(w => w.name === "height");
const presetWidget = this.widgets.find(w => w.name === "preset");
if (widthWidget && heightWidget && presetWidget) {
// Handle preset swapping first
if (presetWidget.value !== "custom") {
const currentPreset = presetWidget.value;
// Parse current preset dimensions (handle both × and x separators)
let w, h;
if (currentPreset.includes('×')) {
[w, h] = currentPreset.split('×').map(v => parseInt(v));
} else if (currentPreset.includes('x')) {
[w, h] = currentPreset.split('x').map(v => parseInt(v));
} else {
return; // Invalid preset format
}
const swappedPreset = `${h}×${w}`;
// Define all available presets from our preset system
const availablePresets = [
"custom",
// SDXL Presets
"1024×1024", "896×1152", "832×1216", "768×1344", "640×1536",
"1152×896", "1216×832", "1344×768", "1536×640",
// FLUX Presets
"1920×1080", "1536×1536", "1280×768", "768×1280",
"1440×1080", "1080×1440", "1728×1152", "1152×1728",
// Ultra-Wide Presets
"2560×1080", "2048×768", "1792×768", "2304×768",
"1080×2560", "768×2048", "768×1792", "768×2304"
];
if (availablePresets.includes(swappedPreset)) {
// Swapped preset exists, use it
presetWidget.value = swappedPreset;
widthWidget.value = h;
heightWidget.value = w;
if (presetWidget.callback) {
presetWidget.callback(swappedPreset, this, presetWidget);
}
if (widthWidget.callback) {
widthWidget.callback(h, this, widthWidget);
}
if (heightWidget.callback) {
heightWidget.callback(w, this, heightWidget);
}
} else {
// Swapped preset doesn't exist, switch to custom and swap manual values
presetWidget.value = "custom";
widthWidget.value = h;
heightWidget.value = w;
if (presetWidget.callback) {
presetWidget.callback("custom", this, presetWidget);
}
if (widthWidget.callback) {
widthWidget.callback(h, this, widthWidget);
}
if (heightWidget.callback) {
heightWidget.callback(w, this, heightWidget);
}
}
} else {
// Custom preset - just swap the width and height values
const tempWidth = widthWidget.value;
widthWidget.value = heightWidget.value;
heightWidget.value = tempWidth;
// Trigger widget change events
if (widthWidget.callback) {
widthWidget.callback(widthWidget.value, this, widthWidget);
}
if (heightWidget.callback) {
heightWidget.callback(heightWidget.value, this, heightWidget);
}
}
// Mark the graph as changed
this.graph?.setDirtyCanvas(true, true);
}
};
};
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
if (onDrawForeground) {
onDrawForeground.apply(this, arguments);
}
if (this.flags.collapsed) return;
// Draw swap button with consistent spacing from widgets
const swapButtonSize = 24;
const margin = 6;
const swapButtonX = this.size[0] - swapButtonSize - margin;
// Calculate button position based on widget spacing rather than bottom margin
// Estimate widget area height and add consistent spacing
const estimatedWidgetHeight = 90; // Approximate height for 3 widgets
const topMargin = 35; // Space from top to first widget
const buttonSpacing = 10; // Space between last widget and button
const swapButtonY = topMargin + estimatedWidgetHeight + buttonSpacing;
// Button background - change color based on pressed state
if (this.swapButtonPressed) {
// Darker when pressed
ctx.fillStyle = "rgba(30, 120, 200, 0.9)"; // Darker blue when clicked
} else {
// Normal state
ctx.fillStyle = "rgba(66, 165, 245, 0.8)"; // Material blue
}
ctx.beginPath();
ctx.roundRect(swapButtonX, swapButtonY, swapButtonSize, swapButtonSize, 4);
ctx.fill();
// Button border with subtle highlight
ctx.strokeStyle = this.swapButtonPressed ? "rgba(20, 100, 180, 1.0)" : "rgba(33, 150, 243, 0.9)";
ctx.lineWidth = 1;
ctx.stroke();
// Draw swap icon - modern double arrow design
ctx.strokeStyle = "rgba(255, 255, 255, 0.95)";
ctx.lineWidth = 2;
ctx.lineCap = "round";
const centerX = swapButtonX + 12;
const centerY = swapButtonY + 12;
// Top arrow (pointing right) - width to height
ctx.beginPath();
ctx.moveTo(centerX - 7, centerY - 3);
ctx.lineTo(centerX + 5, centerY - 3);
ctx.stroke();
// Top arrow head
ctx.beginPath();
ctx.moveTo(centerX + 5, centerY - 3);
ctx.lineTo(centerX + 2, centerY - 5);
ctx.moveTo(centerX + 5, centerY - 3);
ctx.lineTo(centerX + 2, centerY - 1);
ctx.stroke();
// Bottom arrow (pointing left) - height to width
ctx.beginPath();
ctx.moveTo(centerX + 5, centerY + 3);
ctx.lineTo(centerX - 7, centerY + 3);
ctx.stroke();
// Bottom arrow head
ctx.beginPath();
ctx.moveTo(centerX - 7, centerY + 3);
ctx.lineTo(centerX - 4, centerY + 1);
ctx.moveTo(centerX - 7, centerY + 3);
ctx.lineTo(centerX - 4, centerY + 5);
ctx.stroke();
// Add subtle tooltip text when hovering (if we had hover state)
// This could be extended with hover detection for better UX
};
const onMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function (e) {
// Check if click is on swap button
const swapButtonSize = 24;
const margin = 6;
const swapButtonX = this.pos[0] + this.size[0] - swapButtonSize - margin;
// Use same positioning logic as drawing
const estimatedWidgetHeight = 90;
const topMargin = 35;
const buttonSpacing = 10;
const swapButtonY = this.pos[1] + topMargin + estimatedWidgetHeight + buttonSpacing;
if (
e.canvasX >= swapButtonX &&
e.canvasX <= swapButtonX + swapButtonSize &&
e.canvasY >= swapButtonY &&
e.canvasY <= swapButtonY + swapButtonSize
) {
// Visual feedback - set button as pressed
this.swapButtonPressed = true;
this.setDirtyCanvas(true, true);
// Execute swap
this.swapDimensions();
// Reset button state after a short delay for visual feedback
setTimeout(() => {
this.swapButtonPressed = false;
this.setDirtyCanvas(true, true);
}, 150);
return true; // Consume the event
}
// Call original onMouseDown if not clicking swap button
if (onMouseDown) {
return onMouseDown.apply(this, arguments);
}
};
// Optional: Add hover effect for better user feedback
const onMouseMove = nodeType.prototype.onMouseMove;
nodeType.prototype.onMouseMove = function (e) {
// Check if hovering over swap button
const swapButtonSize = 24;
const margin = 6;
const swapButtonX = this.pos[0] + this.size[0] - swapButtonSize - margin;
// Use same positioning logic as drawing
const estimatedWidgetHeight = 90;
const topMargin = 35;
const buttonSpacing = 10;
const swapButtonY = this.pos[1] + topMargin + estimatedWidgetHeight + buttonSpacing;
const isHovering = (
e.canvasX >= swapButtonX &&
e.canvasX <= swapButtonX + swapButtonSize &&
e.canvasY >= swapButtonY &&
e.canvasY <= swapButtonY + swapButtonSize
);
// Update cursor style for better UX (safely)
if (isHovering && this.graph && this.graph.canvas && this.graph.canvas.canvas) {
this.graph.canvas.canvas.style.cursor = "pointer";
} else if (this.graph && this.graph.canvas && this.graph.canvas.canvas) {
this.graph.canvas.canvas.style.cursor = "default";
}
// Call original onMouseMove
if (onMouseMove) {
return onMouseMove.apply(this, arguments);
}
};
}
},
});