Initial commit: exact resolution maths for latent-space models

Two nodes addressing distinct problems with ComfyUI's built-in
Resolution Selector (comfy_extras/nodes_resolution.py).

ResolutionSelectorMP
  Core computes `megapixels * 1024 * 1024`, so every target is inflated
  4.86% in area: 2.36 MP at 9:16 yields 1184x2096 = 2.48 real MP. This
  uses 10^6.

  Core also rounds each axis onto the grid independently, which drifts
  the aspect ratio (that same result is 0.5649, not 0.5625). With
  exact_ratio on, this solves instead on the lattice of sizes that are
  simultaneously grid-aligned and exactly on-ratio: for a reduced ratio
  a:b and alignment m those are (a*L*n, b*L*n) where

      L = lcm(m / gcd(a, m), m / gcd(b, m))

  9:16 at m=16 gives the lattice 144n x 256n, and n=8 lands on
  1152x2048 -- exact 9:16, on the grid, 2.36 MP. solve_nearest()
  preserves core's behaviour behind exact_ratio=False for when the
  lattice is too coarse.

ResolutionAlignToGrid
  Snaps arbitrary dimensions onto the grid (1920x1080 -> 1920x1088),
  with round/up/down.

Alignment is vae_spatial_downscale * dit_patch_size, verified against
the ComfyUI source rather than assumed: 16 for Krea 2 (Wan21 inherits
spacial_downscale_ratio 8, patch 2), 32 for MiniMax H3
(spacial_downscale_ratio 16, patch_size (1,2,2)), 64 for SDXL/SD1.5.
Off-grid dimensions get circular-padded by
comfy.ldm.common_dit.pad_to_patch_size -- the pad row is a wrapped copy
of the top of the image, folded into the same patch token as the real
bottom row, which decodes to a band of artifacts along the bottom edge.
Padding is only ever appended, so the artifact is never top/left.

Verified: ruff clean; both schemas construct and both execute() paths
run; a 4000-case fuzz over random ratio/alignment/target triples asserts
w % m == 0, h % m == 0 and w*b == h*a (zero ratio drift); core's formula
reimplemented and reproduces the observed 1184x2096 exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdgKVDqEbD14EJ4RC3GPmg
This commit is contained in:
dreevelle
2026-08-09 13:51:36 +01:00
co-authored by Claude Opus 5
commit 2b7a1fecae
8 changed files with 594 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Files kept in git but not shipped in the registry package.
CLAUDE.md
.github/
+21
View File
@@ -0,0 +1,21 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v7
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+10
View File
@@ -0,0 +1,10 @@
__pycache__/
*.py[cod]
*.egg-info/
.eggs/
build/
dist/
.venv/
venv/
.DS_Store
*.log
+108
View File
@@ -0,0 +1,108 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A single-file ComfyUI custom node package. All code lives in `__init__.py`; there
is no build step, test suite, or linter config. Two nodes,
`ResolutionSelectorMP` and `ResolutionAlignToGrid`, both pure integer maths with
no torch/numpy involvement.
Registered through ComfyUI's **V3 schema API**, not the legacy
`NODE_CLASS_MAPPINGS` dict: `comfy_entrypoint()` returns a `ComfyExtension` whose
`get_node_list()` yields `IO.ComfyNode` subclasses. Inputs are declared in
`define_schema()` and arrive as named kwargs to `execute()`.
## Local environment
- ComfyUI checkout: `/home/dreevelle/comfy/ComfyUI`
- Python venv: `/home/dreevelle/comfy-env/bin/python`
- `ComfyUI/custom_nodes/ComfyUI-ResolutionTools` is a **symlink to this working
tree**, so edits are live on the next server restart:
```bash
ln -sfn /home/dreevelle/Projects/ComfyUI-ResolutionTools \
/home/dreevelle/comfy/ComfyUI/custom_nodes/ComfyUI-ResolutionTools
```
This is the pre-publish testing setup. Once the package is on the registry,
swap the symlink for an independent clone of the GitHub remote (the
FastImageSequence arrangement) so the installed copy tracks releases rather
than the working tree.
## Verifying changes
`__init__.py` imports `comfy_api.latest` at module scope, so it needs ComfyUI on
`sys.path` — but the maths functions are module-level and dependency-free, so
they can be exercised directly:
```bash
cd /home/dreevelle/comfy/ComfyUI && PYTHONPATH=/home/dreevelle/comfy/ComfyUI \
/home/dreevelle/comfy-env/bin/python -c "
import importlib.util
spec = importlib.util.spec_from_file_location('rt', '/home/dreevelle/Projects/ComfyUI-ResolutionTools/__init__.py')
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print(m.solve_exact(9, 16, 16, 2.36e6)) # -> (1152, 2048)
"
```
Two invariants any change to `solve_exact` must preserve, and which are worth
re-fuzzing over random `(ratio, alignment, target)` triples:
1. `width % alignment == 0 and height % alignment == 0`
2. `width * h_ratio == height * w_ratio` (ratio held exactly, no drift)
Known-good anchors: `9:16 @ m=16, 2.36 MP -> 1152x2048`;
`16:9 @ m=32, 1.33 MP -> 1536x864`; `21:9 @ m=16, 2.37 MP -> 2352x1008`.
## Why the maths is what it is
**Real megapixels.** Core's `comfy_extras/nodes_resolution.py:78` uses
`megapixels * 1024 * 1024`. That's the SDXL-era convention where "1 MP" meant
1024x1024. This package deliberately diverges to `* 1_000_000`. Do not "fix"
this to match core — the divergence is the point, and the README documents it.
**Lattice solving.** `lattice_step()` computes the smallest scale increment that
keeps both axes on the alignment grid while holding an exact ratio. Core rounds
each axis independently, which drifts the ratio (9:16 at 2.36 MP becomes 0.5649
instead of 0.5625). `solve_nearest()` preserves core's behaviour for when the
lattice is too coarse; it is opt-in via `exact_ratio=False`.
**Alignment = VAE spatial downscale x DiT patch size.** Verified against the
ComfyUI source rather than assumed:
- Krea 2 -> 16. `latent_formats.LatentFormat.spacial_downscale_ratio = 8`
(inherited by `Wan21`, which `supported_models.Krea2` uses) x `patch=2` in
`comfy/ldm/krea2/model.py`.
- MiniMax H3 -> 32. `latent_formats.MiniMaxH3Video.spacial_downscale_ratio = 16`
x `patch_size=(1, 2, 2)` in `comfy/ldm/minimax/model.py`.
- SDXL/SD1.5 -> 64. UNet, no patchify; three downsample stages on an 8x VAE.
The failure mode when unaligned is `comfy.ldm.common_dit.pad_to_patch_size`,
which pads with `padding_mode="circular"` and appends only at the end — hence a
bottom/right band, never top/left. If a future model changes VAE or patch size,
the alignment table in the README and `ALIGNMENT_TOOLTIP` both need updating.
## Conventions
- Version is duplicated in `__version__` (`__init__.py`) and `[project].version`
(`pyproject.toml`) — bump both.
- `dependencies` stays empty; this package is stdlib-only by design.
- Node IDs (`ResolutionSelectorMP`, `ResolutionAlignToGrid`) are the public API.
Renaming one breaks every saved workflow that uses it.
## Releasing
Intended for the Comfy Registry as `comfyui-resolution-tools` under publisher
`dreevelle`, same setup as ComfyUI-FastImageSequence.
**Changing `[project].version` in `pyproject.toml` on `main` publishes a
release.** `.github/workflows/publish_action.yml` watches that path and runs
`Comfy-Org/publish-node-action` with the `REGISTRY_ACCESS_TOKEN` repo secret.
Don't touch the version field unless a release is intended; republishing an
existing version fails with `400 The node version already exists`.
`.comfyignore` keeps `CLAUDE.md` and `.github/` out of the published archive.
`comfy node validate` runs the registry checks locally; `comfy node pack`
produces the exact archive that would be uploaded.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 dreevelle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+125
View File
@@ -0,0 +1,125 @@
# ComfyUI-ResolutionTools
Resolution maths for latent-space models, done exactly.
Two nodes, no dependencies:
| Node | Does |
|---|---|
| **Resolution Selector (Real MP)** | aspect ratio + real megapixels → grid-aligned width/height, ratio held exact |
| **Align Resolution to Grid** | snap an arbitrary size onto the grid (1920×1080 → 1920×1088) |
## Why
### 1. "Megapixels" usually isn't megapixels
ComfyUI's built-in Resolution Selector computes `megapixels * 1024 * 1024`. That's
**mebipixels** — every value you type is inflated 4.86% in area. Ask for 2.36 MP at
9:16 and you get 1184×2096 = 2.48 real MP.
This node uses `megapixels * 1_000_000`. Ask for 2.36 and you get 2.36.
### 2. Rounding each axis independently drifts the aspect ratio
That same 1184×2096 has a ratio of 0.5649. True 9:16 is 0.5625. Both axes were
individually snapped to a multiple of 16, and the ratio fell between the cracks.
With `exact_ratio` on (the default), this node solves on the **lattice of sizes that
are simultaneously grid-aligned and exactly on-ratio**. For a reduced ratio `a:b` and
alignment `m`, those are exactly `(a·L·n, b·L·n)` where
```
L = lcm(m / gcd(a, m), m / gcd(b, m))
```
so it picks `n = round(sqrt(target_px / (a·b·L²)))`. For 9:16 at m=16 the lattice is
`144n × 256n`, and n=8 gives **1152×2048** — exact 9:16, on the grid, 2.36 MP.
Turn `exact_ratio` off to get the old independent-rounding behaviour when you'd rather
hit the area target precisely and don't care about a hair of ratio drift.
### 3. Getting alignment wrong bands the bottom of your image
A latent-space DiT reaches pixels through two reductions:
```
pixels ──/ VAE spatial downscale ──> latent ──/ DiT patch size ──> tokens
```
Both axes must be multiples of `vae_downscale × patch_size`:
| Models | Alignment | |
|---|---|---|
| Krea 2, Flux, SD3, Qwen-Image, Wan | **16** | 8 × 2 |
| MiniMax H3 | **32** | 16 × 2 |
| SDXL, SD1.5 (UNet, 3 downsample stages) | **64** | universally safe |
Miss it and `comfy.ldm.common_dit.pad_to_patch_size` pads the latent up to the patch
grid using **circular** padding — the pad row is a wrapped copy of the *top* of the
image. That fabricated row gets folded into the same patch token as the real bottom
row, a combination the model never saw in training, so the final latent row decodes to
garbage: a band along the bottom edge, 8 px tall on Krea 2, 16 px on H3. Padding is
only ever appended, so the artifact is always bottom/right, never top/left.
This is why 1920×1080 bands on Krea 2 (1080/8 = 135 latent rows, odd) and 1920×1088
doesn't (1088/8 = 136, even). The built-in selector defaults `multiple` to 8, which
walks you straight into it.
## Reference resolutions
Krea 2 (alignment 16, ~2.36 MP — Turbo's sanctioned 1K–2K band):
| Ratio | Resolution | Real MP |
|---|---|---|
| 1:1 | 1536 × 1536 | 2.36 |
| 16:9 | 2048 × 1152 | 2.36 |
| 9:16 | 1152 × 2048 | 2.36 |
| 3:2 | 1824 × 1216 | 2.22 |
| 4:3 | 1728 × 1296 | 2.24 |
| 21:9 | 2352 × 1008 | 2.37 |
MiniMax H3 (alignment 32; native canvas is 768 short edge / 1.03 MP, so treat anything
above ~1.3 MP as extrapolation):
| Ratio | Resolution | Real MP |
|---|---|---|
| 16:9 | 1024 × 576 | 0.59 |
| 16:9 | 1536 × 864 | 1.33 |
| ~1.75:1 | 1344 × 768 | 1.03 (node default, exactly the trained canvas) |
Note that on a 32-grid, exact 16:9 only exists at `512n × 288n` — none of the commonly
circulated H3 resolution tables are actually 16:9.
## Nodes
### Resolution Selector (Real MP)
**Inputs** — `aspect_ratio` (11 presets + Custom), `megapixels` (real, 10⁶),
`alignment` (default 16), `exact_ratio` (default on), `custom_w` / `custom_h` (advanced,
for the Custom ratio).
**Outputs** — `width`, `height`, `megapixels` (what it actually produced), `label`
(`"1152x2048"`, useful for filename prefixes).
### Align Resolution to Grid
**Inputs** — `width`, `height`, `alignment`, `mode` (`round` / `up` / `down`).
**Outputs** — `width`, `height`, `changed` (true if anything was off-grid), `label`.
Use `up` when you must not lose framing, `down` when you must not exceed a budget.
## Install
Via ComfyUI-Manager, or:
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/dreevelle/ComfyUI-ResolutionTools
```
No dependencies, no restart quirks — it's pure stdlib maths.
## License
MIT
+289
View File
@@ -0,0 +1,289 @@
"""ComfyUI-ResolutionTools.
Resolution maths for latent-space models, done exactly.
Two nodes:
``ResolutionSelectorMP``
Aspect ratio + a *real* megapixel target (10^6 pixels, not 1024^2) ->
width/height that land on the model's patch grid **without drifting the
aspect ratio**. Rounding each axis independently -- what the built-in
Resolution Selector does -- turns 9:16 into 0.5649 instead of 0.5625. This
solves on the lattice of grid-aligned exact-ratio sizes instead.
``ResolutionAlignToGrid``
Snap an arbitrary width/height (e.g. 1920x1080) to the same grid
(-> 1920x1088), which is what stops the bottom-edge banding described below.
Why alignment matters
---------------------
A latent-space DiT reaches pixels through two reductions::
pixels --/ VAE spatial downscale --> latent --/ DiT patch size --> tokens
so both axes must be multiples of ``vae_downscale * patch_size``:
========================================== =========
Krea 2 / Flux / SD3 / Qwen-Image / Wan 16 (8 x 2)
MiniMax H3 32 (16 x 2)
SDXL / SD1.5 (UNet, 3 downsample stages) 64
========================================== =========
Miss it and ComfyUI's ``comfy.ldm.common_dit.pad_to_patch_size`` pads the latent
up to the patch grid with **circular** padding -- the pad row is a wrapped copy
of the top of the image. That fake row gets folded into the same patch token as
the real bottom row, which the model never saw in training, so the final latent
row decodes to a band of garbage at the bottom edge. One latent row = 8 px on
Krea 2, 16 px on H3. Padding is only ever appended, so the artifact is always
bottom/right, never top/left.
Self-contained on purpose (nothing imported from ``comfy_extras``) so a ComfyUI
core update can't silently change the maths.
"""
import math
from enum import Enum
from typing_extensions import override
from comfy_api.latest import ComfyExtension, IO
__version__ = "1.0.0"
# ---------------------------------------------------------------------------
# Aspect ratios. Values are (w, h) integer pairs, reduced before use.
# ---------------------------------------------------------------------------
class AspectRatio(str, Enum):
SQUARE = "1:1 (Square)"
PHOTO_V = "2:3 (Portrait Photo)"
PHOTO_H = "3:2 (Photo)"
STANDARD_V = "3:4 (Portrait Standard)"
STANDARD_H = "4:3 (Standard)"
SOCIAL_V = "4:5 (Portrait Social)"
SOCIAL_H = "5:4 (Social)"
WIDESCREEN_V = "9:16 (Portrait Widescreen)"
WIDESCREEN_H = "16:9 (Widescreen)"
ULTRAWIDE_V = "9:21 (Portrait Ultrawide)"
ULTRAWIDE_H = "21:9 (Ultrawide)"
CUSTOM = "Custom (use custom_w / custom_h)"
ASPECT_RATIOS: dict[AspectRatio, tuple[int, int]] = {
AspectRatio.SQUARE: (1, 1),
AspectRatio.PHOTO_V: (2, 3),
AspectRatio.PHOTO_H: (3, 2),
AspectRatio.STANDARD_V: (3, 4),
AspectRatio.STANDARD_H: (4, 3),
AspectRatio.SOCIAL_V: (4, 5),
AspectRatio.SOCIAL_H: (5, 4),
AspectRatio.WIDESCREEN_V: (9, 16),
AspectRatio.WIDESCREEN_H: (16, 9),
AspectRatio.ULTRAWIDE_V: (9, 21),
AspectRatio.ULTRAWIDE_H: (21, 9),
}
ALIGNMENT_TOOLTIP = (
"Pixel grid both axes must land on = VAE spatial downscale x DiT patch size. "
"16: Krea 2 / Flux / SD3 / Qwen-Image / Wan. "
"32: MiniMax H3. "
"64: SDXL / SD1.5, and universally safe. "
"Getting this wrong causes a band of artifacts along the bottom/right edge."
)
# ---------------------------------------------------------------------------
# Core maths
# ---------------------------------------------------------------------------
def lattice_step(w_ratio: int, h_ratio: int, alignment: int) -> int:
"""Smallest scale increment keeping an exact ratio on the alignment grid.
For a reduced ratio a:b, sizes are (a*s, b*s). ``a*s`` is a multiple of
``m`` only when ``s`` is a multiple of ``m // gcd(a, m)``; same for ``b``.
Both hold together on multiples of the lcm of those two, so the valid
resolutions are exactly ``(a*L*n, b*L*n)`` for integer n >= 1.
9:16 at m=16 -> L=16 -> 144n x 256n (n=8 gives 1152x2048)
16:9 at m=32 -> L=32 -> 512n x 288n (n=3 gives 1536x864)
"""
return math.lcm(alignment // math.gcd(w_ratio, alignment),
alignment // math.gcd(h_ratio, alignment))
def solve_exact(w_ratio: int, h_ratio: int, alignment: int, target_pixels: float) -> tuple[int, int]:
"""Grid-aligned size with the ratio held exactly, area nearest the target."""
step = lattice_step(w_ratio, h_ratio, alignment)
# area(n) = w_ratio*h_ratio*step^2 * n^2, so solve for n and round.
n = max(1, round(math.sqrt(target_pixels / (w_ratio * h_ratio * step * step))))
return w_ratio * step * n, h_ratio * step * n
def solve_nearest(w_ratio: int, h_ratio: int, alignment: int, target_pixels: float) -> tuple[int, int]:
"""Grid-aligned size with each axis rounded independently.
Hits the area target more closely than :func:`solve_exact` at the cost of a
small aspect-ratio drift. Useful when the exact lattice is coarse.
"""
scale = math.sqrt(target_pixels / (w_ratio * h_ratio))
width = max(alignment, round(w_ratio * scale / alignment) * alignment)
height = max(alignment, round(h_ratio * scale / alignment) * alignment)
return width, height
def align_value(value: int, alignment: int, mode: str) -> int:
"""Snap one dimension onto the grid, never returning less than ``alignment``."""
if mode == "up":
snapped = math.ceil(value / alignment) * alignment
elif mode == "down":
snapped = math.floor(value / alignment) * alignment
else:
snapped = round(value / alignment) * alignment
return max(alignment, snapped)
# ---------------------------------------------------------------------------
# Nodes
# ---------------------------------------------------------------------------
class ResolutionSelectorMP(IO.ComfyNode):
"""Aspect ratio + real megapixels -> grid-aligned width/height."""
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="ResolutionSelectorMP",
display_name="Resolution Selector (Real MP)",
category="utilities",
description=(
"Width/height from an aspect ratio and a real megapixel target "
"(1 MP = 1,000,000 pixels), snapped to the model's patch grid "
"with the aspect ratio held exactly."
),
inputs=[
IO.Combo.Input(
"aspect_ratio",
options=AspectRatio,
default=AspectRatio.WIDESCREEN_H,
tooltip="Aspect ratio of the output. Portrait entries are listed before their landscape counterparts.",
),
IO.Float.Input(
"megapixels",
default=1.0,
min=0.01,
max=64.0,
step=0.01,
tooltip=(
"Target area in real megapixels: 1.0 = 1,000,000 pixels. "
"Note this is NOT the built-in Resolution Selector's unit, which is 1024x1024 = 1.049 MP."
),
),
IO.Int.Input(
"alignment",
default=16,
min=1,
max=256,
step=8,
tooltip=ALIGNMENT_TOOLTIP,
),
IO.Boolean.Input(
"exact_ratio",
default=True,
tooltip=(
"On: hold the aspect ratio exactly by snapping to the lattice of valid sizes, "
"accepting a slightly different area. Off: round each axis independently, "
"hitting the area target more closely but drifting the ratio."
),
),
IO.Int.Input(
"custom_w",
default=16,
min=1,
max=4096,
tooltip="Width term of the ratio, used only when aspect_ratio is Custom.",
advanced=True,
),
IO.Int.Input(
"custom_h",
default=9,
min=1,
max=4096,
tooltip="Height term of the ratio, used only when aspect_ratio is Custom.",
advanced=True,
),
],
outputs=[
IO.Int.Output(display_name="width", tooltip="Width in pixels, a multiple of alignment."),
IO.Int.Output(display_name="height", tooltip="Height in pixels, a multiple of alignment."),
IO.Float.Output(display_name="megapixels", tooltip="Real megapixels actually produced."),
IO.String.Output(display_name="label", tooltip='Dimensions as "1152x2048", handy for filename prefixes.'),
],
)
@classmethod
def execute(cls, aspect_ratio: str, megapixels: float, alignment: int,
exact_ratio: bool, custom_w: int, custom_h: int) -> IO.NodeOutput:
if aspect_ratio == AspectRatio.CUSTOM:
w_ratio, h_ratio = custom_w, custom_h
else:
w_ratio, h_ratio = ASPECT_RATIOS[AspectRatio(aspect_ratio)]
# Reduce, so 16:9 and 32:18 give the same lattice.
divisor = math.gcd(w_ratio, h_ratio)
w_ratio, h_ratio = w_ratio // divisor, h_ratio // divisor
target_pixels = megapixels * 1_000_000
solve = solve_exact if exact_ratio else solve_nearest
width, height = solve(w_ratio, h_ratio, alignment, target_pixels)
return IO.NodeOutput(width, height, (width * height) / 1_000_000, f"{width}x{height}")
class ResolutionAlignToGrid(IO.ComfyNode):
"""Snap an arbitrary width/height onto the model's patch grid."""
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="ResolutionAlignToGrid",
display_name="Align Resolution to Grid",
category="utilities",
description=(
"Snap width/height to a multiple of the model's patch grid, e.g. "
"1920x1080 -> 1920x1088 for Krea 2. Unaligned dimensions get "
"circular-padded in the DiT and band along the bottom/right edge."
),
inputs=[
IO.Int.Input("width", default=1920, min=1, max=16384),
IO.Int.Input("height", default=1080, min=1, max=16384),
IO.Int.Input("alignment", default=16, min=1, max=256, step=8, tooltip=ALIGNMENT_TOOLTIP),
IO.Combo.Input(
"mode",
options=["round", "up", "down"],
default="round",
tooltip="round: nearest multiple. up: never shrink (may add letterbox). down: never grow (may crop framing).",
),
],
outputs=[
IO.Int.Output(display_name="width"),
IO.Int.Output(display_name="height"),
IO.Boolean.Output(display_name="changed", tooltip="True if either dimension was off the grid."),
IO.String.Output(display_name="label"),
],
)
@classmethod
def execute(cls, width: int, height: int, alignment: int, mode: str) -> IO.NodeOutput:
new_w = align_value(width, alignment, mode)
new_h = align_value(height, alignment, mode)
changed = (new_w != width) or (new_h != height)
return IO.NodeOutput(new_w, new_h, changed, f"{new_w}x{new_h}")
class ResolutionToolsExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [ResolutionSelectorMP, ResolutionAlignToGrid]
async def comfy_entrypoint() -> ResolutionToolsExtension:
return ResolutionToolsExtension()
+17
View File
@@ -0,0 +1,17 @@
[project]
name = "comfyui-resolution-tools"
description = "Resolution maths for latent-space models, done exactly. Real megapixels (10^6, not 1024^2), patch-grid alignment for Krea 2 / MiniMax H3 / Flux / SDXL, and aspect ratios held exact instead of drifting on rounding."
version = "1.0.0"
license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
# Pure stdlib maths; everything else is provided by ComfyUI core.
dependencies = []
[project.urls]
Repository = "https://github.com/dreevelle/ComfyUI-ResolutionTools"
[tool.comfy]
PublisherId = "dreevelle"
DisplayName = "ComfyUI-ResolutionTools"
Icon = ""