ANDRO Save EXR
The node that writes the file cv2.imwrite quietly never did
- images
- folder
A PNG can't carry what a float32 decode produces - the values above 1.0 and below 0.0, the hundreds of thousands of distinct levels instead of 256. That's what EXR is for, and Save EXR (float32) is the node that writes a proper float32 EXR sequence from your image batch. It's the end of the pipeline this pack is really aimed at: decode in float32 without the clamp, save it as something that can survive a grade in Resolve or a Nuke comp.
There's a genuinely interesting trap hidden in the implementation, and it's worth knowing because it explains why this node exists at all. OpenCV compiles the EXR codec in but leaves it disabled unless OPENCV_IO_ENABLE_OPENEXR=1 is set before cv2 is imported. No ComfyUI launcher sets that, so any node writing EXR through cv2.imwrite silently writes nothing - no error, no file, you find out later. The pack's README documents it bit ComfyUI-OCIO too. This node sidesteps the whole mess by writing through the OpenEXR module instead, and it verifies the file actually landed before reporting success. If it can't find OpenEXR, it falls back to cv2 (only if that env var happens to be set), then to a 32-bit float TIFF as last resort.
Inputs
images- the IMAGE batch. Wire it offVAEDecodeFloat32; every frame in the batch becomes one.exrfile in the sequence.filename_prefix(defaultfloat32/frame) - a folder-and-stem template, same spirit as stockSaveImage.float32/frameproducesfloat32/frame.00001.exr,frame.00002.exr, and so on. Give a long sequence its own subfolder - 121 frames of 1280×704 float32 EXR is roughly 1.2 GB.half_float(default off) - 16f vs 32f. Half still carries the out-of-range values, just with less precision. If you only need to survive one grade pass, half at half the disk space is a defensible trade; if you're keeping masters, leave it off.output_folder- absolute path, or empty for the ComfyUIoutput/directory. For 1.2 GB sequences, point it somewhere you control.
Output
Just one: folder, a STRING with where the sequence landed. The node also prints a report to the console with the frame count, the backend used (OpenEXR, cv2, or tifffile(.tif)), and the carried value range with the out-of-bounds percentages.
Install
The one node in this pack with a real dependency:
cd ComfyUI/custom_nodes
git clone https://github.com/AndreiOrehov/comfyui-vae-float32
pip install "OpenEXR>=3.3"
Restart ComfyUI. Or install via ComfyUI Manager and let it handle the requirements. Without OpenEXR the node still works - you just get float TIFF files instead, which are every bit as correct value-wise but annoy your compositor, who will ask why you didn't send EXR.
Where people get burned
- Forgetting the decode side. An EXR faithfully records whatever you feed it. Feed it the stock bf16 decode and you've got a beautiful float32 file carrying 77 levels - this is exactly the "10-bit output" trap the README calls out, where the container is right and the content is wrong. The complementary pair is
VAEDecodeFloat32→SaveEXRFloat32; one fixes what goes in, this fixes what it goes into. - Untagged output. Like ComfyUI's 10-bit video, these EXRs write with
color_primaries/transfer/spaceunknown. The values are scene-ish linear float, but a color-managed pipeline needs the tags or a transform declared. The pack is honest that a colour-managed writer is the missing piece for a fully clean VFX handoff. - The ▶ button. Same gotcha as the other output nodes in this pack - selecting the node and hitting the canvas-toolbar play button can queue only that node's branch. If your EXR folder is empty after a "successful" run, use a full Run first.
Inputs (13)
| Name | Type | Default | Description |
|---|---|---|---|
| images | IMAGE | The batch to write. Values are written exactly as they arrive - nothing is clamped, scaled or tone-mapped on the way out. | |
| filename_prefix | STRING | float32/frame | Subfolder and file stem. Frames are numbered from 1 as stem.00001.exr, so 'float32/frame' gives float32/frame.00001.exr. |
| half_float | BOOLEAN | false | 16f halves the file size and still carries everything outside [0,1] - what it costs is precision, roughly 11 bits of mantissa against 24. That is far above bfloat16, so a 16f EXR of a float32 decode keeps most of what the decode won. Use 32f when the frames are going into a grade that will stretch them hard, 16f when disk or a downstream tool argues. |
| output_folderopt | STRING | Absolute path to write to, e.g. D:/renders/shot_04. Empty means the ComfyUI output directory. Use this to drop a sequence straight into a project folder instead of fishing it out of output/ later; filename_prefix still applies inside it. | |
| write_metadataopt | BOOLEAN | true | Record how these pixels were made in the EXR header: measured range, how much sat outside [0,1], bit depth, and - if you wire the decode report in - the dtypes the decode actually ran in. Six months later the file still answers 'was this the float32 pass?' on its own. Needs the OpenEXR backend; the TIFF fallback drops it. |
| decode_reportopt | STRING | Wire ANDRO VAE Decode's range_report here and its contents are stored in the file's header. Optional - without it the metadata still carries everything measurable from the pixels themselves. | |
| clipped_layeropt | BOOLEAN | false | Add a second EXR layer named 'clipped' holding exactly what the stock [0,1] clamp would have deleted - zero everywhere it would have kept the value. The evidence then travels inside the file instead of in a screenshot. Costs roughly double the file size, and readers that ignore extra layers are unaffected. |
| colorspaceopt | COMBO | srgb_display | What these pixels ARE, written into the header. A decoded SDR generation is display-referred sRGB/Rec.709 gamma, not linear - that is what most VAEs produce (SD, Flux, Wan, LTX SDR), so srgb_display is the honest default. Two exceptions: LTX-2.5 HDR decodes are ACEScct, and the LTX-2.3 HDR IC-LoRA is LogC3. This writes a label - no pixel is converted. |
| colorspace_noteopt | STRING | Free text appended to the label, e.g. 'Flux.2 decode' or 'after OCIO ColorSpace to ACEScg'. | |
| start_frameopt | INT | 10–999999 | Number the first frame gets. 1 keeps the historic stem.00001.exr; VFX pipelines start a sequence at 1001. |
| paddingopt | INT | 51–8 | Digits in the frame number: 5 gives shot.00001.exr, and 4 with start_frame 1001 gives shot.1001.exr. A number too large for the padding is written in full rather than truncated. |
| shot_infoopt | STRING | Project / shot / artist / note. Free text; it goes into the EXR header as andro/shotInfo and into the manifest beside it. | |
| embed_workflowopt | BOOLEAN | true | Store the full workflow graph in the EXR header and in the sidecar manifest, so the file can rebuild the run that made it. Turn it off for confidential graphs - the summary fields (models, seeds, prompts, API settings, hash) are still written. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| folder | STRING | The folder actually written to - wire it onward to a node that reads the sequence back. |