Add support for OpenEXR image format#3096
Conversation
# Conflicts: # src/ImageSharp/Configuration.cs
# Conflicts: # src/ImageSharp/Formats/Bmp/BmpConstants.cs # src/ImageSharp/Formats/Bmp/BmpDecoder.cs # src/ImageSharp/Formats/Bmp/BmpFormat.cs
# Conflicts: # src/ImageSharp/Configuration.cs # src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs # src/ImageSharp/Formats/Bmp/BmpConstants.cs # src/ImageSharp/Formats/Bmp/BmpFormat.cs # src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs # src/ImageSharp/Formats/ImageExtensions.Save.cs # src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs # tests/ImageSharp.Tests/ConfigurationTests.cs # tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
# Conflicts: # src/ImageSharp/Configuration.cs # src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs # src/ImageSharp/Formats/Bmp/BmpConstants.cs # src/ImageSharp/Formats/Bmp/BmpDecoder.cs # src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs # src/ImageSharp/Formats/Bmp/BmpFileHeader.cs # src/ImageSharp/Formats/Bmp/BmpFormat.cs # src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs # src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs # src/ImageSharp/Formats/ImageDecoderUtilities.cs # src/ImageSharp/Formats/ImageExtensions.Save.cs # src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs # src/ImageSharp/Image.Decode.cs # tests/ImageSharp.Tests/ConfigurationTests.cs # tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
# Conflicts: # src/ImageSharp/Formats/Bmp/BmpFormat.cs # src/ImageSharp/Formats/ImageDecoderUtilities.cs # src/ImageSharp/Formats/ImageExtensions.Save.cs # src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs
# Conflicts: # src/ImageSharp/Configuration.cs # src/ImageSharp/Formats/ImageDecoderUtilities.cs # tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
There was a problem hiding this comment.
This output looks bugged to me. Has the Magick comparison been hiding an issue?
There was a problem hiding this comment.
The image is from the bmp suite where alot of the edge case image in the tests come from.
From the description of the image:
An RLE-compressed image that uses “delta” codes, and early EOL & EOBMP markers, to skip over some pixels. It’s okay if the viewer’s image doesn’t exactly match any of the reference images.
It looks like the third image in the "correct display" list: the undefined pixels are black.
| /// <param name="component">The 32 bit component value.</param> | ||
| /// <returns>The <see cref="byte"/> value.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static byte From32BitTo8Bit(uint component) => (byte)(component >> 24); |
There was a problem hiding this comment.
This is actually truncation, not scaling, and introduces bias across the range. Here's a test demonstrating that vs a safer scaling operation like the one we have for From16BitTo8Bit.
[Fact]
public void From32BitTo8Bit_CurrentImplementation_IsTruncation_NotScaling()
{
// Use 8421505 as the input because it is the first integer that should map to
// 1 under correct 32->8 bit scaling.
//
// The scaled value is:
//
// round(component * 255 / uint.MaxValue)
//
// To find the smallest input that rounds to 1, solve for the point where the
// scaled value reaches 0.5:
//
// component * 255 / uint.MaxValue >= 0.5
//
// Rearranging:
//
// component >= uint.MaxValue / (2 * 255)
// component >= 4294967295 / 510
// component >= 8421504.5
//
// Since component is an integer, the smallest valid value is:
//
// 8421505
//
// At this point correct scaling yields 1, while (component >> 24) still yields
// 0, making this the smallest counterexample.
uint component = 8421505u;
byte truncated = CurrentFrom32BitTo8Bit(component);
byte scaled = CorrectFrom32BitTo8Bit(component);
// Current implementation: floor(component / 2^24)
// Correct scaling: round(component * 255 / uint.MaxValue)
Assert.Equal(0, truncated);
Assert.Equal(1, scaled);
Assert.NotEqual(truncated, scaled);
static byte CurrentFrom32BitTo8Bit(uint value) => (byte)(value >> 24);
static byte CorrectFrom32BitTo8Bit(uint value) =>
(byte)((((ulong)component) + 8421504UL) / 16843009UL)
}The correct method would be.
/// <summary>
/// Scales a value from a 32 bit <see cref="uint"/> to an
/// 8 bit <see cref="byte"/> equivalent.
/// </summary>
/// <param name="component">The 32 bit component value.</param>
/// <returns>The <see cref="byte"/> value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte From32BitTo8Bit(uint component) =>
// To scale to 8 bits from a 32-bit value V the required value is:
//
// (V * 255) / 4294967295
//
// Since:
//
// 4294967295 = 255 * 16843009
//
// this reduces exactly to:
//
// V / 16843009
//
// To round to nearest using integer arithmetic we add half the divisor
// before dividing:
//
// (V + 16843009 / 2) / 16843009
//
// where:
//
// 16843009 / 2 = 8421504.5
//
// Using 8421504 ensures correct round-to-nearest behaviour:
//
// 8421504 -> 0
// 8421505 -> 1
//
// The addition must be performed in 64-bit to avoid overflow for large
// input values (for example uint.MaxValue).
//
// Final exact integer implementation:
//
// ((ulong)V + 8421504) / 16843009
(byte)((((ulong)component) + 8421504UL) / 16843009UL);There was a problem hiding this comment.
Thanks for fixing this, I already had a feeling that there might be an issue with this
JimBobSquarePants
left a comment
There was a problem hiding this comment.
@brianpopow I fixed the remaining issues I could find. I can't see anything else unless you have more to add.
Otherwise, happy to merge.
@JimBobSquarePants : No, I dont have more to add. I think it is now good to go in now. Thanks for helping with the last issues with it 👍 |

Prerequisites
Description
This PR adds support for decoding and encoding of images in the OpenEXR format. OpenEXR is a HDR image format, therefore I have added two new pixel formats
Rgb96andRgba128which store each color channel asUInt32.The specification can be found here: https://openexr.com/en/latest/index.html#openexr
A reference implementation can be found here: https://github.com/AcademySoftwareFoundation/openexr.git