{"record":{"id":"664c83935d6ad9f3","repo":"Comfy-Org/ComfyUI","slug":"no-file-format-bit-depth-encoder-for-num-chan","errorCode":null,"errorMessage":"No {file_format}/{bit_depth} encoder for {num_channels}-channel images: supported channel counts are 1 (grayscale), 3 (RGB) and 4 (RGBA).","messagePattern":"No (.+?)/(.+?) encoder for (.+?)-channel images: supported channel counts are 1 \\(grayscale\\), 3 \\(RGB\\) and 4 \\(RGBA\\)\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"comfy_extras/nodes_images.py","lineNumber":1115,"sourceCode":"    For EXR the input is interpreted according to `colorspace` and converted\n    to scene-linear (EXR's convention) before writing:\n\n      \"sRGB\"   → input is sRGB-encoded Rec. 709; apply inverse sRGB EOTF.\n      \"HDR\"    → input is HLG-encoded Rec. 2020 (BT.2100); apply inverse HLG\n                 OETF to get scene-linear, per BT.2100 Note 5a.\n      \"linear\" → input is already scene-linear (Rec. 709 primaries); write\n                 through unchanged. Use this for renderer/compositor output.\n\n    For PNG, colorspace selection does not modify pixels — PNG is delivered\n    sRGB-encoded and there is no PNG path for wide-gamut HDR in this node.\n    \"\"\"\n    if img_tensor.ndim == 2:\n        img_tensor = img_tensor.unsqueeze(-1)  # Some nodes emit grayscale as (H, W) with no channel dim, mask-style.\n    height, width, num_channels = img_tensor.shape\n\n    spec = _FORMAT_SPECS.get((file_format, bit_depth, num_channels))\n    if spec is None:\n        raise ValueError(\n            f\"No {file_format}/{bit_depth} encoder for {num_channels}-channel images: \"\n            \"supported channel counts are 1 (grayscale), 3 (RGB) and 4 (RGBA).\"\n        )\n\n    if spec[\"dtype\"] == np.float32:\n        # EXR path: preserve full range, no clamp.\n        if colorspace == \"sRGB\":\n            img_tensor = srgb_to_linear(img_tensor)\n        elif colorspace == \"HDR\":\n            img_tensor = hlg_to_linear(img_tensor)\n        img_np = img_tensor.cpu().numpy().astype(np.float32)\n    else:\n        # PNG path: quantize to integer range.\n        scaled = (img_tensor * spec[\"scale\"]).clamp(0, spec[\"scale\"])\n        img_np = scaled.to(torch.int32).cpu().numpy().astype(spec[\"dtype\"])\n\n    # Encode directly via CodecContext. PyAV's `image2` muxer does NOT write to\n    # BytesIO (it expects a real file path), so we bypass the container entirely.","sourceCodeStart":1097,"sourceCodeEnd":1133,"githubUrl":"https://github.com/Comfy-Org/ComfyUI/blob/1c6d8d45b3693bfbb32385b410d813a7fd6be216/comfy_extras/nodes_images.py#L1097-L1133","documentation":"Raised by the image-format save path when _FORMAT_SPECS has no entry for the (file_format, bit_depth, num_channels) tuple. The node supports PNG 8/16-bit and EXR 32-bit float, each only for 1, 3, or 4 channels (a 2-D HxW tensor is first unsqueezed to 1 channel). A tensor with 2, 5+ channels, or a format/depth/channel combination not in the spec table, is rejected.","triggerScenarios":"Saving a 2-channel tensor (e.g. flow fields, xy maps, or a mask concatenated to a 1-channel image making 2 channels); saving an image whose channel count changed after custom compositing; selecting EXR with a bit_depth other than '32-bit float'.","commonSituations":"Workflows that repurpose image tensors to carry non-image data (normals as 3ch is fine, flow as 2ch is not); nodes that append extra channels (depth+alpha) producing 5 channels.","solutions":["Reduce channels to 1, 3, or 4 before saving: slice or pad, e.g. t[..., :3] for RGB, torch.cat([t, t[..., :1]], -1) to promote grayscale to RGBA.","If you need the extra channels, save them as a separate 1/3/4-channel image or a .pt/.npz file instead.","Double-check the file_format/bit_depth pair — EXR only pairs with '32-bit float'."],"exampleFix":"# before\nimg = flow_field  # (B, H, W, 2) -> ValueError\nsave(img)\n\n# after\nimg = torch.cat([flow_field, torch.zeros_like(flow_field[..., :1])], dim=-1)  # (B,H,W,3)\nsave(img)","handlingStrategy":"validation","validationCode":"SUPPORTED = {(1), (3), (4)}\nassert img_tensor.shape[-1] in SUPPORTED, (\n    f\"{img_tensor.shape[-1]} channels not saveable; convert to 1, 3 or 4 channels first\")","typeGuard":"def is_saveableable_image(t) -> bool:\n    # after the node's own (H, W) -> (H, W, 1) unsqueeze convention\n    return t.ndim >= 3 and t.shape[-1] in (1, 3, 4)","tryCatchPattern":"try:\n    save_image(img, \"png\", \"8-bit\")\nexcept ValueError as e:\n    if \"supported channel counts\" in str(e):\n        img = img[..., :3] if img.shape[-1] > 3 else img\n        save_image(img, \"png\", \"8-bit\")\n    else:\n        raise","preventionTips":["Normalize tensors to 1/3/4 channels right after any custom channel math.","Remember EXR only pairs with '32-bit float'.","Store non-image channel layouts (e.g. flow as 2ch) as .pt/.npz, not images."],"tags":["image-io","channels","png","exr","validation"],"backgroundTag":null,"analyzedSha":"1c6d8d45b3693bfbb32385b410d813a7fd6be216","analyzedAt":"2026-08-14T19:37:18.893Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}