Repository navigation
OpenEXR color data is incorrect when using HalfVector4 #3194
Description
Activity
Thanks for the detailed report. I confirm the behavior that you describe. Here is what happens and what I plan to do.
Why the
HalfVector4values look wrong.HalfVector4is the XNA/MonoGame packed vector, not a color type. It has no white point, so its scaled range is the full finite binary16 range,[-65504, 65504]. The EXR decoder writes pixels through the scaled conversions, because it must work for every pixel format.ToVector4()then returns the native stored values, not the EXR values.ToScaledVector4()returns the normalized[0, 1]values.Why values outside
[0, 1]clamp. Scaled values in ImageSharp are normalized to[0, 1], because processors need a fixed range. OpenEXR defines no range for color values, so the decoder cannot normalize HDR data without loss. It uses1.0as white and clamps the remaining values. This is a limitation of the current design, and it applies to every pixel format, includingRgbaHalfandRgbaVector.What I can improve in 4.1.x. Your report led me to an inconsistency. The EXR decoder writes scaled values into the pixel format, but the EXR encoder reads native values. For a pixel format where those two are different, such as
HalfVector4, a load and a save writes wrong values to the file. I will change the encoder so that it agrees with the decoder.What needs v5. Full HDR support means that the float color formats (
RgbaVector,RgbaHalf,RgbaHalfP) keep values outside[0, 1]in their native storage, while their scaled view stays in[0, 1]. That changes the clamp behavior of those formats, and it adds API toPixelTypeInfo. Both are breaking changes, so I am moving this issue to the v5 milestone.Until then. Decode EXR data to
RgbaHalfPorRgbaHalf, not toHalfVector4.ToVector4()then returns the color values directly.RgbaHalfPstores associated alpha, the same as EXR, soHALFdata in[0, 1]decodes exactly. Values more than1.0clip at white in 4.x.Thanks for the detailed report. I confirm the behavior that you describe. Here is what happens and what I plan to do.
Why the HalfVector4 values look wrong. HalfVector4 is the XNA/MonoGame packed vector, not a color type. It has no white point, so its scaled range is the full finite binary16 range, [-65504, 65504]. The EXR decoder writes pixels through the scaled conversions, because it must work for every pixel format. ToVector4() then returns the native stored values, not the EXR values. ToScaledVector4() returns the normalized [0, 1] values.Thanks for the incredibly detailed explanation! I completely understand the historical context of the [0, 1] normalized pipeline and why a proper, unified fix for unbounded HDR values requires the breaking changes planned for v5.
Since v5 might still be a while away, I'd like to propose a pragmatic, low-impact workaround for the 4.1.x branch. Would it be possible to implement a "fast path" specifically for HalfVector4 (for both the Decoder and Encoder) to allow it to act as a pure, lossless data container for OpenEXR?
The Idea:
In ExrDecoderCore, could we add a type check like if (typeof(TPixel) == typeof(HalfVector4))?If true, we bypass the generic FromAssociatedScaledVector4 pipeline entirely, and directly pack the raw unbounded EXR float values into the struct (effectively just doing a float-to-half precision truncation).
If false, it falls back to the current clamp/scale logic, keeping all other pixel types and existing tests perfectly intact.
My use case is reading OpenEXR files to build textures for a rendering pipeline — e.g. HDR environment maps, lighting data — where values well above 1.0 are the whole point.
Would a targeted exception like this be acceptable for the 4.1.x milestone?I understand the use case, but I don’t want to introduce a
HalfVector4exception in 4.1.x. Preserving the raw values during decoding would address loading, but it would leave those values inconsistent with the scaled conversions used elsewhere in ImageSharp. That inconsistency would surface again when converting or processing the image.The encoder correction described above fixes a mismatch within the existing contract. Your proposal changes that contract specifically for one pixel format and one codec. I’d prefer to address this consistently through the HDR work planned for v5, so preservation of values outside
[0, 1]is a supported behavior rather than a special case.- linked a pull request that will close this issuePreserve HDR samples and expand high-precision TIFF/EXR support #3201
on Sep 28, 2026
Prerequisites
DEBUGandRELEASEmodeImageSharp version
4.1.2
Other ImageSharp packages and versions
4.1.2
Environment (Operating system, version and so on)
windows11
.NET Framework version
10
Description
I encountered an issue where OpenEXR image data is being incorrectly altered during loading when using the HalfVector4 floating-point pixel formats
OpenEXR frequently stores High Dynamic Range (HDR) color data in floating-point formats, which means values often exceed the [0, 1] range. However, the current loading logic clamps and scales these values, destroying the original raw data.
I traced the issue to two specific areas in the codebase:
In ExrDecoderCore.cs (around line 225):
pixelRow[x] = TPixel.FromAssociatedScaledVector4(pixelValue);
If TPixel is HalfVector4 and the EXR pixel type is half or float, the RGB channels are clamped to [0, 1] and then scaled/biased to [FiniteMinimum, FiniteMaximum]. This breaks the original floating-point data. I believe the clamp and scale operations should be avoided here.
As a side note for other floating point pixel format like RgbaHalf and RgbaVector.The FromAssociatedScaledVector4 method calls FromVector4(Vector4 source). Inside FromVector4, the original values are explicitly clamped to [0, 1]. This also breaks the raw EXR data.
Steps to Reproduce
load a openEXR file in HalfVector4 or HalfVector4P format,then check the pixel value of ToVector4();
Images
No response