Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,11 @@ private ulong[] EncodeFloatingPointPixelData<TPixel>(
Span<TPixel> pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex);
for (int x = 0; x < width; x++)
{
// OpenEXR stores RGB associated with alpha. Use the native vector domain so floating-point and HDR component
// ranges are preserved instead of being clamped through the scaled [0, 1] representation.
Vector4 vector4 = pixelRowSpan[x].ToAssociatedVector4();
// OpenEXR stores RGB associated with alpha, and the decoder maps an EXR value of 1 to a scaled value of 1.
// Read the scaled vector so that encoding and decoding agree for every pixel format. The native vector
// is wrong here because its range belongs to the pixel format, not to OpenEXR. For example, HalfVector4
// stores opaque alpha as the native value 65504, which would be written to the file unchanged.
Vector4 vector4 = pixelRowSpan[x].ToAssociatedScaledVector4();
redBuffer[x] = vector4.X;
greenBuffer[x] = vector4.Y;
blueBuffer[x] = vector4.Z;
Expand Down Expand Up @@ -305,8 +307,10 @@ private ulong[] EncodeUnsignedIntPixelData<TPixel>(
Span<TPixel> pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex);
for (int x = 0; x < width; x++)
{
// OpenEXR channels use associated alpha; the native vector conversion also preserves the integer channel range.
Vector4 vector4 = pixelRowSpan[x].ToAssociatedVector4();
// OpenEXR channels use associated alpha. Rgba128.FromVector4 expects components in [0, 1], which is the
// scaled range. The native range of the source pixel format can differ, so read the scaled vector,
// the same domain that the decoder writes.
Vector4 vector4 = pixelRowSpan[x].ToAssociatedScaledVector4();
rgb = Rgba128.FromVector4(vector4);

redBuffer[x] = rgb.R;
Expand Down
25 changes: 25 additions & 0 deletions tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Numerics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Exr.Constants;
Expand Down Expand Up @@ -38,6 +39,30 @@ public void EncoderOptions_SetPixelType_Works(ExrPixelType? pixelType, ExrPixelT
Assert.Equal(expectedPixelType, exrMetaData.PixelType);
}

[Theory]
[InlineData(ExrPixelType.Half)]
[InlineData(ExrPixelType.Float)]
[InlineData(ExrPixelType.UnsignedInt)]
public void Encode_PixelFormatWithNonUnitNativeRange_WritesScaledValues(ExrPixelType pixelType)
{
// arrange
// HalfVector4 stores the scaled range [0, 1] as the native range [-65504, 65504], so its native and scaled
// vectors differ. The alpha of 0.5 makes the test sensitive to the alpha channel too: a wrong alpha value
// larger than 1 would otherwise be hidden by the clamp in the decoder.
Vector4 expected = new(0.25F, 0.5F, 0.75F, 0.5F);
ExrEncoder exrEncoder = new() { PixelType = pixelType };
using Image<HalfVector4> input = new(2, 2, HalfVector4.FromScaledVector4(expected));
using MemoryStream memStream = new();

// act
input.Save(memStream, exrEncoder);

// assert
memStream.Position = 0;
using Image<RgbaVector> output = Image.Load<RgbaVector>(memStream);
Assert.Equal(expected, output[0, 0].ToScaledVector4(), new ApproximateFloatComparer(1e-4F));
}

[Theory]
[WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)]
public void ExrEncoder_WithNoCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
Expand Down
Loading