Skip to content

Repository files navigation

Upscale Crop Universal — ComfyUI custom node

Upscale and crop in one ComfyUI node — with the upscale model built in.

Six ways to ask for a size, all of them working downwards as well as up. Then, optionally, a crop stage that cuts the exact format out of the result — drag the window in the live preview to place it. The upscale model is picked by a widget straight from your models/upscale_models folder, so there is no Load Upscale Model node to wire up.

Everything runs in torch and handles batches.

Why this one

The whole job is one node. Getting "upscale with a model, land on an exact size, cut it to 4:5" out of stock ComfyUI takes a chain:

stock ComfyUI     Load Upscale Model ─► Upscale Image (using Model) ─►
                  Upscale Image By ─► Image Crop ─► …and compute the numbers yourself

this node         Upscale Crop Universal

The model is a widget, not a socket. Every comparable pack — PlagueKind, Studio-nodes, Deno2026 — does resize and crop well, and none of them upscale with a model at all. Here method defaults to model and the file comes straight from models/upscale_models. No loader node, no dangling UPSCALE_MODEL wire across your graph.

It knows when not to use the model. Ask for a downscale and the model is skipped entirely rather than burning VRAM to upscale something you are about to shrink. Ask for 2× with a 4× model and it upscales once, then comes back down — instead of leaving you with a needlessly huge intermediate. No model selected? It falls back to lanczos and says so in info instead of failing your queue.

One model pass, on purpose. Running a 4× model twice gives 16× and stacks the artifacts. There is no pass-count widget to get wrong, and no tile size to babysit — tiling reuses ComfyUI's own and halves itself if VRAM runs short.

Latent and image, each sized on its own terms. Connect either or both. A latent is never assumed to be the image divided by eight, and the crop is applied in normalised coordinates so the two come out framed identically. multiple_of divides by 8 on the latent path, because a latent cell is eight pixels — snap a latent to 8 directly and you have rounded it eight times too coarsely.

It is also your downscaler. Every size mode works below 1.0. Most "upscale" nodes refuse or misbehave going down; area is here precisely because it is the right kernel for it.

You see the crop before you queue. The node draws the surviving window over your image with the discarded area dimmed, live as you drag. The numbers come from the same functions the node runs, so the preview cannot drift from the result — that is pinned by tests.

Comparison, fairly: these packs are good at what they do, and the pieces they share with this one are worth a look if the model being built in is not what you need. Packs move fast — this was checked in August 2026.

The two stages

             ┌─────────── UPSCALE ───────────┐  ┌──── CROP ─────┐
image ──────►│ percent · factor · shortest   │─►│ 4:5 1:1 3:2   │──► image
latent ─────►│ longest · megapixels          │  │ DIN 16:9 …    │──► latent
             │ aspect always preserved       │  │ off by default│──► width/height/info
             ├───────────────────────────────┴──┴───────────────┤
             │ custom — an exact box, covered then cropped to it │
             └──────────────────────────────────────────────────┘

Upscale sets the size and never changes the shape. Crop takes the format out of it, and only ever cuts — nothing is resampled a second time.

Asking for a size

target_mode what it pins when it is the natural one
scale_percent 200% = double you think in percent
scale_factor ×2.0 = double you think in multipliers
shortest_side min(w, h) feeding a model with a minimum edge
longest_side max(w, h) fitting a print or a screen
megapixels total pixel count staying inside a VRAM or upload budget
custom an exact box the output resolution is non-negotiable

Percent and factor are the same arithmetic, offered both ways because which one feels natural depends on the task. Every mode downscales when you give it a value below the current size — set scale_factor to 0.5 and this is your downscaler.

The first five keep the aspect ratio. custom is the exception, and the one to reach for when the output resolution is not negotiable: it scales until the target_width × target_height box is covered, then crops to it, so you get exactly those dimensions from any source shape.

The numbers you type are the output size. The crop is not optional there and crop_ratio is hidden — typing a resolution and not getting it back would be no use, and a shape picker could only argue with the box. crop_position and the offsets stay, because those decide which part of the image fills the box, not its size.

That is not the same as crop_ratio = custom, which never rescales — it only cuts the largest window of that shape out of whatever the upscale produced. Ask both for 3000×3000 from a portrait source and only the target mode gives you a 3000×3000 square.

multiple_of snaps the result so it survives a VAE. Type any number — 8 suits SD, SDXL and Flux, 64 some tiling and video workflows. Under 1 switches snapping off and gives you the exact size you asked for, which is fine when the image is going straight to disk.

The crop stage

Off by default, so the node is a pure upscaler until you want a format.

format
4:5 social portrait, 8×10 print — the default
1:1 square
4:3 classic monitor, Four Thirds
3:2 35 mm, most DSLRs
DIN √2 — A4 and the whole A series
16:10 widescreen monitor
16:9 HD video
2:1 univisium
21:9 cinemascope
custom the target_width × target_height box above

Each format is written the way people say it — 4:5 is portrait, 16:9 is landscape — and crop_orientation flips whichever you pick, so there are no mirrored duplicates in the list. Every crop takes the largest window of its shape that fits — a named format, or a custom box, which shrinks proportionally rather than clamping per axis when you ask for more than the image has.

crop_position (center / top / bottom / left / right / random) plus crop_offset_x and crop_offset_y decide what survives — the JPS controls, with one offset per axis. Each is clamped to the slack the anchor leaves, so the window can never wander off the image.

Or just drag it. The window in the live preview is draggable: grab it, drop it, and the offsets update to match. Double-click puts it back to the anchor. Dragging writes into those same two widgets rather than storing the position somewhere else, so a dragged crop still saves and reproduces like any other setting.

random places the window anywhere it fits, driven by crop_seed — for varying the framing across a batch or building a dataset. It is seeded rather than free-running, so the preview shows the window you will actually get and a result you liked can be reproduced. Set crop_seed's control-after-generate to randomize for a fresh one each run.

crop_orientation defaults to auto, which follows the frame: a portrait shot gets a portrait crop, a landscape one gets landscape. On a mixed batch that keeps more of every frame than either fixed setting can, since half of them would be the wrong way round for it.

The model

method defaults to model, and upscale_model lists your models/upscale_models folder directly. One pass at the model's native scale, then a single resize onto the target — so a 4× model asked for a 2× result does not leave you with a needlessly huge intermediate. Tiling reuses ComfyUI's own tiled_scale and halves the tile if VRAM runs short.

It gets out of the way when it should: a pure downscale skips the model entirely, and a missing or unselected model falls back to lanczos. Either way the info output says what actually happened rather than failing silently.

The other methods — lanczos, bicubic, bilinear, nearest, area — are plain kernels. lanczos is the sharpest non-model choice; area is the best for heavy downscaling.

Image and latent

Both inputs are optional; connect either or both. Each is sized from its own dimensions rather than assuming the latent is the image divided by eight, and the crop is applied in normalised 0–1 coordinates so the two come out framed identically. Whichever you did not connect comes back as a small well-formed placeholder, so the output sockets are always safe to leave dangling.

Connecting neither is an error rather than a silent no-op — the node would have nothing to do.

Outputs

image, latent, width, height, info — where info is a one-line readout of the sizes, the method actually used, the crop applied, and any fallback that kicked in.

The node

The node with crop switched on: two blocks, and the crop preview showing what survives

Only the widgets that currently apply are drawn. Switching target_mode swaps the size field, method reveals or hides the model picker, and crop folds the whole second block away — so the node stays about six rows tall however much it can do.

The panel at the bottom is the live crop preview: your own image with the discarded area dimmed and the surviving window framed, updating as you drag. It needs one full run first, since the image only exists inside the node while it executes.

Install

Clone into ComfyUI/custom_nodes/ and restart ComfyUI. No dependencies beyond ComfyUI itself (spandrel, used to load upscale models, already ships with it).

Tests

python tests/run_tests.py

63 tests, no pytest needed — the portable ComfyUI python does not have it. They run against a stubbed folder_paths and never touch a real model, so they work anywhere torch does.

About

Comfyui Custon Node upscaler with universal settings ,crop and upscale

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages