JPEG is Fine
4:4:4 saves that don't eat your colors or your workflow
- images
- images
Sooner or later you generate a few thousand images, look at your output/ folder, and decide PNG was a mistake. The stock Save Image node only writes PNG, and you're not hand-converting at that volume. But the PNG is your master file - it carries the whole node graph in its text chunks, so dragging it back onto the canvas rebuilds the workflow. JPEG throws that away by definition. That tension is why this pack exists.
JPEG is Fine is a one-node pack (handle a5ah1, MIT, v1.1.0) that writes straight to .jpeg - no intermediate PNG, no conversion step - with proper chroma handling, and stashes the workflow JSON in the file's EXIF plus an optional .json beside it. That's the part a plain "save as JPEG" node won't give you.
Why 4:4:4 is the actual feature
JPEG normally stores brightness at full resolution and color at a fraction of it. That's chroma subsampling, and the default almost everywhere is 4:2:0 - chroma at quarter resolution. Fine for photographs, terrible for diffusion output, which is full of saturated reds, pinks and neon edges that live in the chroma channels. That's where the smeared, bleeding color in compressed AI images comes from.
This node hardcodes subsampling=0, i.e. 4:4:4, full-resolution chroma. There is no dropdown for it. You pay in file size - bigger than the same quality setting in 4:2:0 - but still several times smaller than the PNG it replaces.
Two more choices are baked in: optimized Huffman tables (pixel-identical, just packed tighter) and baseline rather than progressive. The pack measured optimize at ~2% smaller files against progressive's ~4%, while progressive costs about 2.2× the decode time and isn't readable by baseline-only hardware decoders. Giving up 2% for universal compatibility is the right call.
The inputs that matter
Wire your image into images. Then:
quality- default 92, range 1–100. 92 is the pack's "visually lossless for most content" setting; reach for 95–98 only if you're seeing banding or blocking in gradients, and accept the file-size jump.filename_prefix- base name. Files come out asimage-0001.jpeg, where the counter auto-increments from the highest matching file already in the folder. It never overwrites, ever.directory- subfolder under ComfyUI's output folder;/nests. Absolute paths, drive letters and..raise an error, same guardrail as the built-in node.save_workflow_json-Subdirectory(default),Sidecar, orDisabled. Your archive strategy lives here; see below.
delimiter (default -) and counter_digits (default 4) are cosmetic - changing the padding width does not restart your numbering, because the scan matches {prefix}{delimiter}{number}.jpeg regardless of how many digits are in the number.
The single output, images, is your input passed through unchanged. The node is still an output node, so it runs whether or not that output is connected - output nodes always run, which is how ComfyUI's cache works backward from them. Connect it only if you want to chain something after the save.
Installing it
ComfyUI Manager: search JPEG is Fine, install, restart. Manual:
cd ComfyUI/custom_nodes
git clone https://github.com/a5ah1/jpeg-is-fine
Then restart ComfyUI and refresh the browser. No pip step - requirements.txt says so in as many words; the node only uses Pillow and numpy, both already in ComfyUI.
Where people get burned
The embedded workflow is not draggable. ComfyUI's frontend loads graphs from PNG, WebP and a few other formats - not JPEG. Drop your saved .jpeg onto the canvas and you get a Load Image node, not your pipeline. The EXIF JSON is for archiving and other tools; the exported .json is how you reopen the workflow. Set save_workflow_json to Sidecar (same folder, same basename) or Subdirectory (a {prefix}_workflows/ folder, so several save nodes can share a directory without colliding), and drag that back in.
Huge workflows get their EXIF trimmed. JPEG's EXIF segment tops out around 64KB. Past ~60KB of JSON the node drops the workflow key and embeds just the prompt, with a console warning; past that it embeds nothing rather than unparseable truncated JSON. The .json export is always complete - one more reason not to leave it on Disabled.
Changing the prefix resets to 0001. The counter scan is filename-pattern-based, so a new filename_prefix or delimiter starts a fresh sequence in that folder - and it always resumes from the highest existing counter, so gaps in the numbering never get filled.
Illegal characters get quietly swapped. Prefixes wired in from another node - a checkpoint name with a \, a 12:30 timestamp - get those characters replaced with _ and a console warning instead of an error. Usually what you want.
Batches cost time. JPEG encoding is real CPU work, and it's the one complaint this category always attracts. This one is a single Pillow save per image with no format negotiation - lean, but not stock-PNG fast on a 500-image batch.
One last thing: there's no A1111-style parameters string in these files, only JSON in the ImageDescription EXIF tag. A host that auto-detects generation data to link models won't find anything here. This node is for your archive, not the upload parser.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| images | IMAGE | Images to save. JPEG can't store transparency, so transparent areas are filled with white. | |
| filename_prefix | STRING | image | Base name for saved files. Characters not allowed in filenames (/ \ : * ? " < > |) are replaced with _. Use 'directory' for subfolders. |
| directory | STRING | Subfolder inside the ComfyUI output folder (use / for nested folders). Leave empty to save to the output folder itself. Absolute paths and '..' are not allowed. | |
| delimiter | STRING | - | Text between the prefix and the counter. Characters not allowed in filenames (/ \ : * ? " < > |) are replaced with _. |
| counter_digits | INT | 41–8 | — |
| quality | INT | 921–100 | — |
| save_workflow_json | COMBO | Subdirectory | 3 options: Disabled, Sidecar, Subdirectory |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | The input images, unchanged. Connect only if you need to chain further nodes. |