imazen / imazen/zenpixels

HDR→SDR fails open on default builds: PQ/HLG→sRGB silently clips highlights (ConvertError not #[non_exhaustive])

Open
#64 0 comments 0 reactions 1 assignee Claimed by @lilith View on GitHub
Dominant language
Rust
Stars
1
Forks
0
PR merge metrics
No merged PRs in 30d

Description

Verified in source, not a stale claim. Violates "the users pixels are sacred".

## The hole

`zenpixels-convert/src/convert.rs:560` gates the refusal behind the feature:

```rust
#[cfg(feature = "hdr-experimental")]
if is_hdr_to_sdr(from.transfer(), to.transfer()) {
return Err(whereat::at!(ConvertError::HdrSourceRequiresPeak { from, to }));
}
```

On a **default** (non-`hdr-experimental`) build the guard does not exist, so a PQ/HLG→sRGB `RowConverter` takes the no-tone-map arms and **hard-clips every highlight to 1.0, silently**. The comment at `:553-559` states this is deliberate — *"the historic pass-through behavior is preserved as a deliberate semi-compatibility shim for legacy non-HDR builds"* — but a silent clip is exactly what the zero-tolerance rule prohibits.

## Root cause is semver, not design

`ConvertError` is **not** `#[non_exhaustive]`, so the `HdrSourceRequiresPeak` variant cannot exist on the default build without a breaking change. That is why the guard is feature-gated rather than unconditional. The same blocker is already acknowledged in two TODOs:
- `zenpixels-convert/src/output.rs:105-107` — *"TODO(0.3.0): Add HdrPolicy enum and ConvertOutputOptions here once ConvertError is #[non_exhaustive] and can carry HdrTransferRequiresToneMapping"*
- `output.rs:230-231` — same, for the HDR→SDR policy gate.

## Runner-up (same file)

`convert.rs:664-666`: the gamut step is skipped when **either** sides primaries are `Unknown`:
```rust
let need_primaries = from.primaries != to.primaries
&& from.primaries != ColorPrimaries::Unknown
&& to.primaries != ColorPrimaries::Unknown;
```
so wide-gamut pixels tagged `Unknown` are silently relabeled as sRGB. More defensible (you cannot convert *from* an unknown gamut) — but refusing may beat relabeling.

## Ask

Make `ConvertError` `#[non_exhaustive]` in the 0.3.0 batch, then make the HDR→SDR guard unconditional: tone-map or refuse, never silent-clip. Until then the default build has a silent precision-loss path.

Found while auditing the encode surface; the in-flight `zencodec/docs/pixel-descriptor-negotiation.md` §12 independently flags the same hazard.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.