diff --git a/ImageSharp.sln b/ImageSharp.sln
index 7ccd92c07d..13dd2fba7e 100644
--- a/ImageSharp.sln
+++ b/ImageSharp.sln
@@ -37,8 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{815C0625-CD3
ProjectSection(SolutionItems) = preProject
src\Directory.Build.props = src\Directory.Build.props
src\Directory.Build.targets = src\Directory.Build.targets
- src\README.md = src\README.md
src\ImageSharp.ruleset = src\ImageSharp.ruleset
+ src\README.md = src\README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp", "src\ImageSharp\ImageSharp.csproj", "{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}"
@@ -215,6 +215,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
ProjectSection(SolutionItems) = preProject
tests\Images\Input\Jpg\issues\issue-1076-invalid-subsampling.jpg = tests\Images\Input\Jpg\issues\issue-1076-invalid-subsampling.jpg
tests\Images\Input\Jpg\issues\issue-1221-identify-multi-frame.jpg = tests\Images\Input\Jpg\issues\issue-1221-identify-multi-frame.jpg
+ tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
tests\Images\Input\Jpg\issues\issue1006-incorrect-resize.jpg = tests\Images\Input\Jpg\issues\issue1006-incorrect-resize.jpg
tests\Images\Input\Jpg\issues\issue1049-exif-resize.jpg = tests\Images\Input\Jpg\issues\issue1049-exif-resize.jpg
tests\Images\Input\Jpg\issues\Issue159-MissingFF00-Progressive-Bedroom.jpg = tests\Images\Input\Jpg\issues\Issue159-MissingFF00-Progressive-Bedroom.jpg
@@ -238,7 +239,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg = tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg
tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg = tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg
tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg = tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg
- tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}"
diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs
index 1c30d857f6..735f6bc597 100644
--- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs
+++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs
@@ -132,6 +132,15 @@ public static byte From16BitTo8Bit(ushort component) =>
// (V * 255 + 32895) >> 16
(byte)(((component * 255) + 32895) >> 16);
+ ///
+ /// Scales a value from an 32 bit to
+ /// an 8 bit equivalent.
+ ///
+ /// The 32 bit component value.
+ /// The value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte From32BitTo8Bit(uint component) => (byte)(component >> 24);
+
///
/// Scales a value from an 8 bit to
/// an 16 bit equivalent.
@@ -142,6 +151,26 @@ public static byte From16BitTo8Bit(ushort component) =>
public static ushort From8BitTo16Bit(byte component)
=> (ushort)(component * 257);
+ ///
+ /// Scales a value from an 16 bit to
+ /// an 16 bit equivalent.
+ ///
+ /// The 16 bit component value.
+ /// The 32 bit
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint From16BitTo32Bit(ushort component)
+ => (uint)(component * 65537);
+
+ ///
+ /// Scales a value from an 8 bit to
+ /// an 32 bit equivalent.
+ ///
+ /// The 8 bit component value.
+ /// The 32 bit
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint From8BitTo32Bit(byte component)
+ => (uint)(component * 16843009);
+
///
/// Returns how many bits are required to store the specified number of colors.
/// Performs a Log2() on the value.
diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs
index 2673927231..85cd3363ec 100644
--- a/src/ImageSharp/Configuration.cs
+++ b/src/ImageSharp/Configuration.cs
@@ -5,6 +5,7 @@
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
+using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
@@ -213,6 +214,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
/// .
/// .
/// .
+ /// .
/// .
///
/// The default configuration of .
@@ -225,6 +227,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule(),
+ new ExrConfigurationModule(),
new QoiConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule());
diff --git a/src/ImageSharp/Formats/Bmp/BmpConstants.cs b/src/ImageSharp/Formats/Bmp/BmpConstants.cs
index 1ac79a9e26..913cbdcd1e 100644
--- a/src/ImageSharp/Formats/Bmp/BmpConstants.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpConstants.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Bmp;
diff --git a/src/ImageSharp/Formats/Bmp/BmpFormat.cs b/src/ImageSharp/Formats/Bmp/BmpFormat.cs
index 5dec4a6748..50e4090e1b 100644
--- a/src/ImageSharp/Formats/Bmp/BmpFormat.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpFormat.cs
@@ -30,5 +30,5 @@ private BmpFormat()
public IEnumerable FileExtensions => BmpConstants.FileExtensions;
///
- public BmpMetadata CreateDefaultFormatMetadata() => new();
+ public BmpMetadata CreateDefaultFormatMetadata() => new BmpMetadata();
}
diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs
new file mode 100644
index 0000000000..cd9bec6131
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;
+
+internal class NoneExrCompressor : ExrBaseCompressor
+{
+ public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
+ : base(output, allocator, bytesPerBlock, bytesPerRow)
+ {
+ }
+
+ ///
+ public override ExrCompression Method => ExrCompression.Zip;
+
+ ///
+ public override uint CompressRowBlock(Span rows, int rowCount)
+ {
+ this.Output.Write(rows);
+ return (uint)rows.Length;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs
new file mode 100644
index 0000000000..dcfbe3ae05
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Compression.Zlib;
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;
+
+internal class ZipExrCompressor : ExrBaseCompressor
+{
+ private readonly DeflateCompressionLevel compressionLevel;
+
+ private readonly MemoryStream memoryStream;
+
+ private readonly System.Buffers.IMemoryOwner buffer;
+
+ public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, DeflateCompressionLevel compressionLevel)
+ : base(output, allocator, bytesPerBlock, bytesPerRow)
+ {
+ this.compressionLevel = compressionLevel;
+ this.buffer = allocator.Allocate((int)bytesPerBlock);
+ this.memoryStream = new();
+ }
+
+ ///
+ public override ExrCompression Method => ExrCompression.Zip;
+
+ ///
+ public override uint CompressRowBlock(Span rows, int rowCount)
+ {
+ // Re-oder pixel values.
+ Span reordered = this.buffer.GetSpan()[..(int)(rowCount * this.BytesPerRow)];
+ int n = reordered.Length;
+ int t1 = 0;
+ int t2 = (n + 1) >> 1;
+ for (int i = 0; i < n; i++)
+ {
+ bool isOdd = (i & 1) == 1;
+ reordered[isOdd ? t2++ : t1++] = rows[i];
+ }
+
+ // Predictor.
+ Span predicted = reordered;
+ byte p = predicted[0];
+ for (int i = 1; i < predicted.Length; i++)
+ {
+ int d = (predicted[i] - p + 128 + 256) & 255;
+ p = predicted[i];
+ predicted[i] = (byte)d;
+ }
+
+ this.memoryStream.Seek(0, SeekOrigin.Begin);
+ using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel))
+ {
+ stream.Write(predicted);
+ stream.Flush();
+ }
+
+ int size = (int)this.memoryStream.Position;
+ byte[] buffer = this.memoryStream.GetBuffer();
+ this.Output.Write(buffer, 0, size);
+
+ // Reset memory stream for next pixel row.
+ this.memoryStream.Seek(0, SeekOrigin.Begin);
+ this.memoryStream.SetLength(0);
+
+ return (uint)size;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ this.buffer.Dispose();
+ this.memoryStream?.Dispose();
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
new file mode 100644
index 0000000000..e5b735a395
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
@@ -0,0 +1,190 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
+
+internal class B44ExrCompression : ExrBaseDecompressor
+{
+ private readonly int width;
+
+ private readonly uint rowsPerBlock;
+
+ private readonly int channelCount;
+
+ private readonly byte[] scratch = new byte[14];
+
+ private readonly ushort[] s = new ushort[16];
+
+ private readonly IMemoryOwner tmpBuffer;
+
+ public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount)
+ : base(allocator, bytesPerBlock, bytesPerRow)
+ {
+ this.width = width;
+ this.rowsPerBlock = rowsPerBlock;
+ this.channelCount = channelCount;
+ this.tmpBuffer = allocator.Allocate((int)(width * rowsPerBlock * channelCount));
+ }
+
+ ///
+ public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer)
+ {
+ Span outputBuffer = MemoryMarshal.Cast(buffer);
+ Span decompressed = this.tmpBuffer.GetSpan();
+ int outputOffset = 0;
+ int bytesLeft = (int)compressedBytes;
+ for (int i = 0; i < this.channelCount && bytesLeft > 0; i++)
+ {
+ for (int y = 0; y < this.rowsPerBlock; y += 4)
+ {
+ Span row0 = decompressed.Slice(outputOffset, this.width);
+ outputOffset += this.width;
+ Span row1 = decompressed.Slice(outputOffset, this.width);
+ outputOffset += this.width;
+ Span row2 = decompressed.Slice(outputOffset, this.width);
+ outputOffset += this.width;
+ Span row3 = decompressed.Slice(outputOffset, this.width);
+ outputOffset += this.width;
+
+ int rowOffset = 0;
+ for (int x = 0; x < this.width && bytesLeft > 0; x += 4)
+ {
+ int bytesRead = stream.Read(this.scratch, 0, 3);
+ if (bytesRead == 0)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream");
+ }
+
+ if (this.scratch[2] >= 13 << 2)
+ {
+ Unpack3(this.scratch, this.s);
+ bytesLeft -= 3;
+ }
+ else
+ {
+ bytesRead = stream.Read(this.scratch, 3, 11);
+ if (bytesRead == 0)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream");
+ }
+
+ Unpack14(this.scratch, this.s);
+ bytesLeft -= 14;
+ }
+
+ int n = x + 3 < this.width ? 4 : this.width - x;
+ if (y + 3 < this.rowsPerBlock)
+ {
+ this.s.AsSpan(0, n).CopyTo(row0.Slice(rowOffset));
+ this.s.AsSpan(4, n).CopyTo(row1.Slice(rowOffset));
+ this.s.AsSpan(8, n).CopyTo(row2.Slice(rowOffset));
+ this.s.AsSpan(12, n).CopyTo(row3.Slice(rowOffset));
+ }
+ else
+ {
+ this.s.AsSpan(0, n).CopyTo(row0.Slice(rowOffset));
+ if (y + 1 < this.rowsPerBlock)
+ {
+ this.s.AsSpan(4, n).CopyTo(row1.Slice(rowOffset));
+ }
+
+ if (y + 2 < this.rowsPerBlock)
+ {
+ this.s.AsSpan(8, n).CopyTo(row2.Slice(rowOffset));
+ }
+ }
+
+ rowOffset += 4;
+ }
+
+ if (bytesLeft <= 0)
+ {
+ break;
+ }
+ }
+ }
+
+ // Rearrange the decompressed data such that the data for each scan line form a contiguous block.
+ int offsetDecompressed = 0;
+ int offsetOutput = 0;
+ int blockSize = (int)(this.width * this.rowsPerBlock);
+ for (int y = 0; y < this.rowsPerBlock; y++)
+ {
+ for (int i = 0; i < this.channelCount; i++)
+ {
+ decompressed.Slice(offsetDecompressed + (i * blockSize), this.width).CopyTo(outputBuffer.Slice(offsetOutput));
+ offsetOutput += this.width;
+ }
+
+ offsetDecompressed += this.width;
+ }
+ }
+
+ // Unpack a 14-byte block into 4 by 4 16-bit pixels.
+ private static void Unpack14(Span b, Span s)
+ {
+ s[0] = (ushort)((b[0] << 8) | b[1]);
+
+ ushort shift = (ushort)(b[2] >> 2);
+ ushort bias = (ushort)(0x20u << shift);
+
+ s[4] = (ushort)(s[0] + ((((b[2] << 4) | (b[3] >> 4)) & 0x3fu) << shift) - bias);
+ s[8] = (ushort)(s[4] + ((((b[3] << 2) | (b[4] >> 6)) & 0x3fu) << shift) - bias);
+ s[12] = (ushort)(s[8] + ((b[4] & 0x3fu) << shift) - bias);
+
+ s[1] = (ushort)(s[0] + ((uint)(b[5] >> 2) << shift) - bias);
+ s[5] = (ushort)(s[4] + ((((b[5] << 4) | (b[6] >> 4)) & 0x3fu) << shift) - bias);
+ s[9] = (ushort)(s[8] + ((((b[6] << 2) | (b[7] >> 6)) & 0x3fu) << shift) - bias);
+ s[13] = (ushort)(s[12] + ((b[7] & 0x3fu) << shift) - bias);
+
+ s[2] = (ushort)(s[1] + ((uint)(b[8] >> 2) << shift) - bias);
+ s[6] = (ushort)(s[5] + ((((b[8] << 4) | (b[9] >> 4)) & 0x3fu) << shift) - bias);
+ s[10] = (ushort)(s[9] + ((((b[9] << 2) | (b[10] >> 6)) & 0x3fu) << shift) - bias);
+ s[14] = (ushort)(s[13] + ((b[10] & 0x3fu) << shift) - bias);
+
+ s[3] = (ushort)(s[2] + ((uint)(b[11] >> 2) << shift) - bias);
+ s[7] = (ushort)(s[6] + ((((b[11] << 4) | (b[12] >> 4)) & 0x3fu) << shift) - bias);
+ s[11] = (ushort)(s[10] + ((((b[12] << 2) | (b[13] >> 6)) & 0x3fu) << shift) - bias);
+ s[15] = (ushort)(s[14] + ((b[13] & 0x3fu) << shift) - bias);
+
+ for (int i = 0; i < 16; ++i)
+ {
+ if ((s[i] & 0x8000) != 0)
+ {
+ s[i] &= 0x7fff;
+ }
+ else
+ {
+ s[i] = (ushort)~s[i];
+ }
+ }
+ }
+
+ // Unpack a 3-byte block into 4 by 4 identical 16-bit pixels.
+ private static void Unpack3(Span b, Span s)
+ {
+ s[0] = (ushort)((b[0] << 8) | b[1]);
+
+ if ((s[0] & 0x8000) != 0)
+ {
+ s[0] &= 0x7fff;
+ }
+ else
+ {
+ s[0] = (ushort)~s[0];
+ }
+
+ for (int i = 1; i < 16; ++i)
+ {
+ s[i] = s[0];
+ }
+ }
+
+ ///
+ protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
+}
diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs
new file mode 100644
index 0000000000..fb4fa5d832
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
+
+internal class NoneExrCompression : ExrBaseDecompressor
+{
+ public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
+ : base(allocator, bytesPerBlock, bytesPerRow)
+ {
+ }
+
+ ///
+ public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer)
+ {
+ int bytesRead = stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock));
+ if (bytesRead != (int)this.BytesPerBlock)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough pixel data!");
+ }
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs
new file mode 100644
index 0000000000..12f5fc8ab6
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
+
+internal class RunLengthExrCompression : ExrBaseDecompressor
+{
+ private readonly IMemoryOwner tmpBuffer;
+
+ private readonly ushort[] s = new ushort[16];
+
+ public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
+ : base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock);
+
+ ///
+ public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer)
+ {
+ Span uncompressed = this.tmpBuffer.GetSpan();
+ int maxLength = (int)this.BytesPerBlock;
+ int offset = 0;
+ while (compressedBytes > 0)
+ {
+ byte nextByte = ReadNextByte(stream);
+
+ sbyte input = (sbyte)nextByte;
+ if (input < 0)
+ {
+ int count = -input;
+ compressedBytes -= (uint)(count + 1);
+
+ if ((maxLength -= count) < 0)
+ {
+ return;
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ uncompressed[offset + i] = ReadNextByte(stream);
+ }
+
+ offset += count;
+ }
+ else
+ {
+ int count = input;
+ byte value = ReadNextByte(stream);
+ compressedBytes -= 2;
+
+ if ((maxLength -= count + 1) < 0)
+ {
+ return;
+ }
+
+ for (int i = 0; i < count + 1; i++)
+ {
+ uncompressed[offset + i] = value;
+ }
+
+ offset += count + 1;
+ }
+ }
+
+ Reconstruct(uncompressed, this.BytesPerBlock);
+ Interleave(uncompressed, this.BytesPerBlock, buffer);
+ }
+
+ private static byte ReadNextByte(BufferedReadStream stream)
+ {
+ int nextByte = stream.ReadByte();
+ if (nextByte == -1)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to decompress RLE image!");
+ }
+
+ return (byte)nextByte;
+ }
+
+ ///
+ protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
+}
diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs
new file mode 100644
index 0000000000..b8dc5efa8e
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.IO.Compression;
+using SixLabors.ImageSharp.Compression.Zlib;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
+
+internal class ZipExrCompression : ExrBaseDecompressor
+{
+ private readonly IMemoryOwner tmpBuffer;
+
+ public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
+ : base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock);
+
+ ///
+ public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer)
+ {
+ Span uncompressed = this.tmpBuffer.GetSpan();
+
+ long pos = stream.Position;
+ using ZlibInflateStream inflateStream = new(
+ stream,
+ () =>
+ {
+ int left = (int)(compressedBytes - (stream.Position - pos));
+ return left > 0 ? left : 0;
+ });
+ inflateStream.AllocateNewBytes((int)this.BytesPerBlock, true);
+ using DeflateStream dataStream = inflateStream.CompressedStream!;
+
+ int totalRead = 0;
+ while (totalRead < buffer.Length)
+ {
+ int bytesRead = dataStream.Read(uncompressed, totalRead, buffer.Length - totalRead);
+ if (bytesRead <= 0)
+ {
+ break;
+ }
+
+ totalRead += bytesRead;
+ }
+
+ if (totalRead == 0)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Could not read zip compressed image data!");
+ }
+
+ Reconstruct(uncompressed, (uint)totalRead);
+ Interleave(uncompressed, (uint)totalRead, buffer);
+ }
+
+ ///
+ protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
+}
diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs
new file mode 100644
index 0000000000..57e6b2a26c
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression;
+
+internal abstract class ExrBaseCompression : IDisposable
+{
+ private bool isDisposed;
+
+ protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
+ {
+ this.Allocator = allocator;
+ this.BytesPerBlock = bytesPerBlock;
+ this.BytesPerRow = bytesPerRow;
+ }
+
+ ///
+ /// Gets the memory allocator.
+ ///
+ protected MemoryAllocator Allocator { get; }
+
+ ///
+ /// Gets the bits per pixel.
+ ///
+ public int BitsPerPixel { get; }
+
+ ///
+ /// Gets the bytes per row.
+ ///
+ public uint BytesPerRow { get; }
+
+ ///
+ /// Gets the uncompressed bytes per block.
+ ///
+ public uint BytesPerBlock { get; }
+
+ ///
+ /// Gets the image width.
+ ///
+ public int Width { get; }
+
+ ///
+ public void Dispose()
+ {
+ if (this.isDisposed)
+ {
+ return;
+ }
+
+ this.isDisposed = true;
+ this.Dispose(true);
+ }
+
+ protected abstract void Dispose(bool disposing);
+}
diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
new file mode 100644
index 0000000000..1bbf36d768
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression;
+
+internal abstract class ExrBaseDecompressor : ExrBaseCompression
+{
+ protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
+ : base(allocator, bytesPerBlock, bytesPerRow)
+ {
+ }
+
+ public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer);
+
+ protected static void Reconstruct(Span buffer, uint unCompressedBytes)
+ {
+ int offset = 0;
+ for (int i = 0; i < unCompressedBytes - 1; i++)
+ {
+ byte d = (byte)(buffer[offset] + (buffer[offset + 1] - 128));
+ buffer[offset + 1] = d;
+ offset++;
+ }
+ }
+
+ protected static void Interleave(Span source, uint unCompressedBytes, Span output)
+ {
+ int sourceOffset = 0;
+ int offset0 = 0;
+ int offset1 = (int)((unCompressedBytes + 1) / 2);
+ while (sourceOffset < unCompressedBytes)
+ {
+ output[sourceOffset++] = source[offset0++];
+ output[sourceOffset++] = source[offset1++];
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs
new file mode 100644
index 0000000000..24f396e16f
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Compression.Zlib;
+using SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression;
+
+internal static class ExrCompressorFactory
+{
+ public static ExrBaseCompressor Create(
+ ExrCompression method,
+ MemoryAllocator allocator,
+ Stream output,
+ uint bytesPerBlock,
+ uint bytesPerRow,
+ DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression)
+ {
+ switch (method)
+ {
+ case ExrCompression.None:
+ return new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow);
+ case ExrCompression.Zips:
+ return new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel);
+ case ExrCompression.Zip:
+ return new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel);
+
+ default:
+ throw ExrThrowHelper.NotSupportedCompressor(method.ToString());
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs
new file mode 100644
index 0000000000..2696a289cd
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression;
+
+internal static class ExrDecompressorFactory
+{
+ public static ExrBaseDecompressor Create(
+ ExrCompression method,
+ MemoryAllocator memoryAllocator,
+ int width,
+ uint bytesPerBlock,
+ uint bytesPerRow,
+ uint rowsPerBlock,
+ int channelCount)
+ {
+ switch (method)
+ {
+ case ExrCompression.None:
+ return new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow);
+ case ExrCompression.Zips:
+ return new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow);
+ case ExrCompression.Zip:
+ return new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow);
+ case ExrCompression.RunLengthEncoded:
+ return new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow);
+ case ExrCompression.B44:
+ return new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount);
+ default:
+ throw ExrThrowHelper.NotSupportedDecompressor(nameof(method));
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs b/src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs
new file mode 100644
index 0000000000..d0964bf33b
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Exr.Constants;
+
+///
+/// Enumeration representing the compression formats defined by the EXR file-format.
+///
+public enum ExrCompression
+{
+ ///
+ /// Pixel data is not compressed.
+ ///
+ None = 0,
+
+ ///
+ /// Differences between horizontally adjacent pixels are run-length encoded.
+ /// This method is fast, and works well for images with large flat areas, but for photographic images,
+ /// the compressed file size is usually between 60 and 75 percent of the uncompressed size.
+ /// Compression is lossless.
+ ///
+ RunLengthEncoded = 1,
+
+ ///
+ /// Uses the open source zlib library for compression. Unlike ZIP compression, this operates one scan line at a time.
+ /// Compression is lossless.
+ ///
+ Zips = 2,
+
+ ///
+ /// Differences between horizontally adjacent pixels are compressed using the open source zlib library.
+ /// Unlike ZIPS compression, this operates in in blocks of 16 scan lines.
+ /// Compression is lossless.
+ ///
+ Zip = 3,
+
+ ///
+ /// A wavelet transform is applied to the pixel data, and the result is Huffman-encoded.
+ /// Compression is lossless.
+ ///
+ Piz = 4,
+
+ ///
+ /// After reducing 32-bit floating-point data to 24 bits by rounding, differences between horizontally adjacent pixels are compressed with zlib,
+ /// similar to ZIP. PXR24 compression preserves image channels of type HALF and UINT exactly, but the relative error of FLOAT data increases to about 3×10-5.
+ /// Compression is lossy.
+ ///
+ Pxr24 = 5,
+
+ ///
+ /// Channels of type HALF are split into blocks of four by four pixels or 32 bytes. Each block is then packed into 14 bytes,
+ /// reducing the data to 44 percent of their uncompressed size.
+ /// Compression is lossy.
+ ///
+ B44 = 6,
+
+ ///
+ /// Like B44, except for blocks of four by four pixels where all pixels have the same value, which are packed into 3 instead of 14 bytes.
+ /// For images with large uniform areas, B44A produces smaller files than B44 compression.
+ /// Compression is lossy.
+ ///
+ B44A = 7
+}
diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs
new file mode 100644
index 0000000000..9453a7d9cd
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Exr.Constants;
+
+///
+/// This enum represents the type of pixel data in the EXR image.
+///
+public enum ExrImageDataType
+{
+ ///
+ /// The pixel data is unknown.
+ ///
+ Unknown = 0,
+
+ ///
+ /// The pixel data has 3 channels: red, green and blue.
+ ///
+ Rgb = 1,
+
+ ///
+ /// The pixel data has four channels: red, green, blue and a alpha channel.
+ ///
+ Rgba = 2,
+
+ ///
+ /// There is only one channel with the luminance.
+ ///
+ Gray = 3,
+}
diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs
new file mode 100644
index 0000000000..beeabe35e1
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Exr.Constants;
+
+internal enum ExrImageType
+{
+ ScanLine = 0,
+
+ Tiled = 1
+}
diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs b/src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs
new file mode 100644
index 0000000000..56573472ca
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Exr.Constants;
+
+internal enum ExrLineOrder : byte
+{
+ IncreasingY = 0,
+
+ DecreasingY = 1,
+
+ RandomY = 2
+}
diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs
new file mode 100644
index 0000000000..cddc43d594
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Exr.Constants;
+
+///
+/// The different pixel formats for a OpenEXR image.
+///
+public enum ExrPixelType
+{
+ ///
+ /// unsigned int (32 bit).
+ ///
+ UnsignedInt = 0,
+
+ ///
+ /// half (16 bit floating point).
+ ///
+ Half = 1,
+
+ ///
+ /// float (32 bit floating point).
+ ///
+ Float = 2
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrAttribute.cs b/src/ImageSharp/Formats/Exr/ExrAttribute.cs
new file mode 100644
index 0000000000..b4e95f1d47
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrAttribute.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+[DebuggerDisplay("Name: {Name}, Type: {Type}, Length: {Length}")]
+internal class ExrAttribute
+{
+ public static readonly ExrAttribute EmptyAttribute = new(string.Empty, string.Empty, 0);
+
+ public ExrAttribute(string name, string type, int length)
+ {
+ this.Name = name;
+ this.Type = type;
+ this.Length = length;
+ }
+
+ public string Name { get; }
+
+ public string Type { get; }
+
+ public int Length { get; }
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs
new file mode 100644
index 0000000000..1f464ec675
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Exr.Compression;
+
+internal abstract class ExrBaseCompressor : ExrBaseCompression
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The output stream to write the compressed image to.
+ /// The memory allocator.
+ /// Bytes per row block.
+ /// Bytes per pixel row.
+ protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
+ : base(allocator, bytesPerBlock, bytesPerRow)
+ => this.Output = output;
+
+ ///
+ /// Gets the compression method to use.
+ ///
+ public abstract ExrCompression Method { get; }
+
+ ///
+ /// Gets the output stream to write the compressed image to.
+ ///
+ public Stream Output { get; }
+
+ ///
+ /// Compresses a block of rows of the image.
+ ///
+ /// Image rows to compress.
+ /// The number of rows to compress.
+ /// Number of bytes of of the compressed data.
+ public abstract uint CompressRowBlock(Span rows, int rowCount);
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrBox2i.cs b/src/ImageSharp/Formats/Exr/ExrBox2i.cs
new file mode 100644
index 0000000000..032e60d929
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrBox2i.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+[DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")]
+internal readonly struct ExrBox2i
+{
+ public ExrBox2i(int xMin, int yMin, int xMax, int yMax)
+ {
+ this.XMin = xMin;
+ this.YMin = yMin;
+ this.XMax = xMax;
+ this.YMax = yMax;
+ }
+
+ public int XMin { get; }
+
+ public int YMin { get; }
+
+ public int XMax { get; }
+
+ public int YMax { get; }
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs b/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs
new file mode 100644
index 0000000000..d4f5825b99
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+[DebuggerDisplay("Name: {ChannelName}, PixelType: {PixelType}")]
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+internal readonly struct ExrChannelInfo
+{
+ public ExrChannelInfo(string channelName, ExrPixelType pixelType, byte pLinear, int xSampling, int ySampling)
+ {
+ this.ChannelName = channelName;
+ this.PixelType = pixelType;
+ this.PLinear = pLinear;
+ this.XSampling = xSampling;
+ this.YSampling = ySampling;
+ }
+
+ public string ChannelName { get; }
+
+ public ExrPixelType PixelType { get; }
+
+ public byte PLinear { get; }
+
+ public int XSampling { get; }
+
+ public int YSampling { get; }
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs b/src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs
new file mode 100644
index 0000000000..a2f1b8c7ab
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Registers the image encoders, decoders and mime type detectors for the OpenExr format.
+///
+public sealed class ExrConfigurationModule : IImageFormatConfigurationModule
+{
+ ///
+ public void Configure(Configuration configuration)
+ {
+ configuration.ImageFormatsManager.SetEncoder(ExrFormat.Instance, new ExrEncoder());
+ configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, ExrDecoder.Instance);
+ configuration.ImageFormatsManager.AddImageFormatDetector(new ExrImageFormatDetector());
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrConstants.cs b/src/ImageSharp/Formats/Exr/ExrConstants.cs
new file mode 100644
index 0000000000..214dbd8321
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrConstants.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Defines constants relating to OpenExr images.
+///
+internal static class ExrConstants
+{
+ ///
+ /// The list of mimetypes that equate to a OpenExr image.
+ ///
+ public static readonly IEnumerable MimeTypes = new[] { "image/x-exr" };
+
+ ///
+ /// The list of file extensions that equate to a OpenExr image.
+ ///
+ public static readonly IEnumerable FileExtensions = new[] { "exr" };
+
+ ///
+ /// The magick bytes identifying an OpenExr image.
+ ///
+ public static readonly int MagickBytes = 20000630;
+
+ ///
+ /// EXR attribute names.
+ ///
+ internal static class AttributeNames
+ {
+ public const string Channels = "channels";
+
+ public const string Compression = "compression";
+
+ public const string DataWindow = "dataWindow";
+
+ public const string DisplayWindow = "displayWindow";
+
+ public const string LineOrder = "lineOrder";
+
+ public const string PixelAspectRatio = "pixelAspectRatio";
+
+ public const string ScreenWindowCenter = "screenWindowCenter";
+
+ public const string ScreenWindowWidth = "screenWindowWidth";
+
+ public const string Tiles = "tiles";
+
+ public const string ChunkCount = "chunkCount";
+ }
+
+ ///
+ /// EXR attribute types.
+ ///
+ internal static class AttibuteTypes
+ {
+ public const string ChannelList = "chlist";
+
+ public const string Compression = "compression";
+
+ public const string Float = "float";
+
+ public const string LineOrder = "lineOrder";
+
+ public const string TwoFloat = "v2f";
+
+ public const string BoxInt = "box2i";
+ }
+
+ internal static class ChannelNames
+ {
+ public const string Red = "R";
+
+ public const string Green = "G";
+
+ public const string Blue = "B";
+
+ public const string Alpha = "A";
+
+ public const string Luminance = "Y";
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrDecoder.cs b/src/ImageSharp/Formats/Exr/ExrDecoder.cs
new file mode 100644
index 0000000000..2e27717282
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrDecoder.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Image decoder for generating an image out of a OpenExr stream.
+///
+public class ExrDecoder : ImageDecoder
+{
+ private ExrDecoder()
+ {
+ }
+
+ ///
+ /// Gets the shared instance.
+ ///
+ public static ExrDecoder Instance { get; } = new();
+
+ ///
+ protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+
+ return new ExrDecoderCore(new ExrDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken);
+ }
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+
+ ExrDecoderCore decoder = new(new ExrDecoderOptions { GeneralOptions = options });
+ Image image = decoder.Decode(options.Configuration, stream, cancellationToken);
+
+ ScaleToTargetSize(options, image);
+
+ return image;
+ }
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ => this.Decode(options, stream, cancellationToken);
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
new file mode 100644
index 0000000000..7598c62f3a
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
@@ -0,0 +1,821 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+#nullable disable
+
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Text;
+using SixLabors.ImageSharp.Formats.Exr.Compression;
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Performs the OpenExr decoding operation.
+///
+internal sealed class ExrDecoderCore : ImageDecoderCore
+{
+ private const float Scale32Bit = 1f / 0xFFFFFFFF;
+
+ ///
+ /// Reusable buffer.
+ ///
+ private readonly byte[] buffer = new byte[8];
+
+ ///
+ /// Used for allocating memory during processing operations.
+ ///
+ private readonly MemoryAllocator memoryAllocator;
+
+ ///
+ /// The global configuration.
+ ///
+ private readonly Configuration configuration;
+
+ ///
+ /// The metadata.
+ ///
+ private ImageMetadata metadata;
+
+ ///
+ /// The exr specific metadata.
+ ///
+ private ExrMetadata exrMetadata;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The options.
+ public ExrDecoderCore(ExrDecoderOptions options)
+ : base(options.GeneralOptions)
+ {
+ this.configuration = options.GeneralOptions.Configuration;
+ this.memoryAllocator = this.configuration.MemoryAllocator;
+ }
+
+ ///
+ /// Gets or sets the image width.
+ ///
+ private int Width { get; set; }
+
+ ///
+ /// Gets or sets the image height.
+ ///
+ private int Height { get; set; }
+
+ ///
+ /// Gets or sets the image channel info's.
+ ///
+ private IList Channels { get; set; }
+
+ ///
+ /// Gets or sets the compression method.
+ ///
+ private ExrCompression Compression { get; set; }
+
+ ///
+ /// Gets or sets the image data type, either RGB, RGBA or gray.
+ ///
+ private ExrImageDataType ImageDataType { get; set; }
+
+ ///
+ /// Gets or sets the pixel type.
+ ///
+ private ExrPixelType PixelType { get; set; }
+
+ ///
+ /// Gets or sets the header attributes.
+ ///
+ private ExrHeaderAttributes HeaderAttributes { get; set; }
+
+ ///
+ protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
+ {
+ this.ReadExrHeader(stream);
+ if (!this.IsSupportedCompression())
+ {
+ ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported");
+ }
+
+ Image image = new(this.configuration, this.Width, this.Height, this.metadata);
+ Buffer2D pixels = image.GetRootFramePixelBuffer();
+
+ switch (this.PixelType)
+ {
+ case ExrPixelType.Half:
+ case ExrPixelType.Float:
+ this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken);
+ break;
+ case ExrPixelType.UnsignedInt:
+ this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken);
+ break;
+ default:
+ ExrThrowHelper.ThrowNotSupported("Pixel type is not supported");
+ break;
+ }
+
+ return image;
+ }
+
+ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels, CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ bool hasAlpha = this.HasAlpha();
+ uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
+ uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
+ uint bytesPerBlock = bytesPerRow * rowsPerBlock;
+ int width = this.Width;
+ int height = this.Height;
+ int channelCount = this.Channels.Count;
+
+ using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4);
+ using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock);
+ Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan();
+ Span redPixelData = rowBuffer.GetSpan()[..width];
+ Span greenPixelData = rowBuffer.GetSpan().Slice(width, width);
+ Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width);
+ Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width);
+
+ using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount);
+
+ int decodedRows = 0;
+ while (decodedRows < height)
+ {
+ ulong rowOffset = this.ReadUnsignedLong(stream);
+ long nextRowOffsetPosition = stream.Position;
+
+ stream.Position = (long)rowOffset;
+ uint rowStartIndex = this.ReadUnsignedInteger(stream);
+
+ uint compressedBytesCount = this.ReadUnsignedInteger(stream);
+ decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData);
+
+ int offset = 0;
+ for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++)
+ {
+ Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex);
+ for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++)
+ {
+ ExrChannelInfo channel = this.Channels[channelIdx];
+ offset += ReadFloatChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width);
+ }
+
+ for (int x = 0; x < width; x++)
+ {
+ HalfVector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f);
+ pixelRow[x] = TPixel.FromVector4(pixelValue.ToVector4());
+ }
+
+ decodedRows++;
+ }
+
+ stream.Position = nextRowOffsetPosition;
+
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+ }
+
+ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels, CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ bool hasAlpha = this.HasAlpha();
+ uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
+ uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
+ uint bytesPerBlock = bytesPerRow * rowsPerBlock;
+ int width = this.Width;
+ int height = this.Height;
+ int channelCount = this.Channels.Count;
+
+ using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4);
+ using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock);
+ Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan();
+ Span redPixelData = rowBuffer.GetSpan()[..width];
+ Span greenPixelData = rowBuffer.GetSpan().Slice(width, width);
+ Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width);
+ Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width);
+
+ using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount);
+
+ int decodedRows = 0;
+ while (decodedRows < height)
+ {
+ ulong rowOffset = this.ReadUnsignedLong(stream);
+ long nextRowOffsetPosition = stream.Position;
+
+ stream.Position = (long)rowOffset;
+ uint rowStartIndex = this.ReadUnsignedInteger(stream);
+
+ uint compressedBytesCount = this.ReadUnsignedInteger(stream);
+ decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData);
+
+ int offset = 0;
+ for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++)
+ {
+ Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex);
+ for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++)
+ {
+ ExrChannelInfo channel = this.Channels[channelIdx];
+ offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width);
+ }
+
+ for (int x = 0; x < width; x++)
+ {
+ pixelRow[x] = ColorScaleTo32Bit(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue);
+ }
+
+ decodedRows++;
+ }
+
+ stream.Position = nextRowOffsetPosition;
+
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+ }
+
+ private static int ReadFloatChannelData(
+ BufferedReadStream stream,
+ ExrChannelInfo channel,
+ Span decompressedPixelData,
+ Span redPixelData,
+ Span greenPixelData,
+ Span bluePixelData,
+ Span alphaPixelData,
+ int width)
+ {
+ switch (channel.ChannelName)
+ {
+ case ExrConstants.ChannelNames.Red:
+ return ReadChannelData(channel, decompressedPixelData, redPixelData, width);
+
+ case ExrConstants.ChannelNames.Blue:
+ return ReadChannelData(channel, decompressedPixelData, bluePixelData, width);
+
+ case ExrConstants.ChannelNames.Green:
+ return ReadChannelData(channel, decompressedPixelData, greenPixelData, width);
+
+ case ExrConstants.ChannelNames.Alpha:
+ return ReadChannelData(channel, decompressedPixelData, alphaPixelData, width);
+
+ case ExrConstants.ChannelNames.Luminance:
+ int bytesRead = ReadChannelData(channel, decompressedPixelData, redPixelData, width);
+ redPixelData.CopyTo(bluePixelData);
+ redPixelData.CopyTo(greenPixelData);
+
+ return bytesRead;
+
+ default:
+ // Skip unknown channel.
+ int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2;
+ stream.Position += width * channelDataSizeInBytes;
+ return channelDataSizeInBytes;
+ }
+ }
+
+ private int ReadUnsignedIntChannelData(
+ BufferedReadStream stream,
+ ExrChannelInfo channel,
+ Span decompressedPixelData,
+ Span redPixelData,
+ Span greenPixelData,
+ Span bluePixelData,
+ Span alphaPixelData,
+ int width)
+ {
+ switch (channel.ChannelName)
+ {
+ case ExrConstants.ChannelNames.Red:
+ return ReadChannelData(channel, decompressedPixelData, redPixelData, width);
+
+ case ExrConstants.ChannelNames.Blue:
+ return ReadChannelData(channel, decompressedPixelData, bluePixelData, width);
+
+ case ExrConstants.ChannelNames.Green:
+ return ReadChannelData(channel, decompressedPixelData, greenPixelData, width);
+
+ case ExrConstants.ChannelNames.Alpha:
+ return ReadChannelData(channel, decompressedPixelData, alphaPixelData, width);
+
+ case ExrConstants.ChannelNames.Luminance:
+ int bytesRead = ReadChannelData(channel, decompressedPixelData, redPixelData, width);
+ redPixelData.CopyTo(bluePixelData);
+ redPixelData.CopyTo(greenPixelData);
+ return bytesRead;
+
+ default:
+ // Skip unknown channel.
+ int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2;
+ stream.Position += this.Width * channelDataSizeInBytes;
+ return channelDataSizeInBytes;
+ }
+ }
+
+ private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) => channel.PixelType switch
+ {
+ ExrPixelType.Half => ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width),
+ ExrPixelType.Float => ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width),
+ _ => 0,
+ };
+
+ private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) => channel.PixelType switch
+ {
+ ExrPixelType.UnsignedInt => ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width),
+ _ => 0,
+ };
+
+ private static int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData, int width)
+ {
+ int offset = 0;
+ for (int x = 0; x < width; x++)
+ {
+ ushort shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2));
+ channelData[x] = HalfTypeHelper.Unpack(shortValue);
+ offset += 2;
+ }
+
+ return offset;
+ }
+
+ private static int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData, int width)
+ {
+ int offset = 0;
+ for (int x = 0; x < width; x++)
+ {
+ int intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4));
+ channelData[x] = Unsafe.As(ref intValue);
+ offset += 4;
+ }
+
+ return offset;
+ }
+
+ private static int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Span channelData, int width)
+ {
+ int offset = 0;
+ for (int x = 0; x < width; x++)
+ {
+ channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(decompressedPixelData.Slice(offset, 4));
+ offset += 4;
+ }
+
+ return offset;
+ }
+
+ ///
+ protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
+ {
+ ExrHeaderAttributes header = this.ReadExrHeader(stream);
+
+ return new ImageInfo(new Size(header.DataWindow.XMax, header.DataWindow.YMax), this.metadata);
+ }
+
+ private ExrPixelType ValidateChannels()
+ {
+ if (this.Channels.Count == 0)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("At least one channel of pixel data is expected!");
+ }
+
+ // Find pixel the type of any channel which is R, G, B or A.
+ ExrPixelType pixelType = this.FindPixelType();
+
+ return pixelType;
+ }
+
+ private ExrImageDataType ReadImageDataType()
+ {
+ bool hasRedChannel = false;
+ bool hasGreenChannel = false;
+ bool hasBlueChannel = false;
+ bool hasAlphaChannel = false;
+ bool hasLuminance = false;
+ foreach (ExrChannelInfo channelInfo in this.Channels)
+ {
+ if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal))
+ {
+ hasAlphaChannel = true;
+ }
+
+ if (channelInfo.ChannelName.Equals("R", StringComparison.Ordinal))
+ {
+ hasRedChannel = true;
+ }
+
+ if (channelInfo.ChannelName.Equals("G", StringComparison.Ordinal))
+ {
+ hasGreenChannel = true;
+ }
+
+ if (channelInfo.ChannelName.Equals("B", StringComparison.Ordinal))
+ {
+ hasBlueChannel = true;
+ }
+
+ if (channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal))
+ {
+ hasLuminance = true;
+ }
+ }
+
+ if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel)
+ {
+ return ExrImageDataType.Rgba;
+ }
+
+ if (hasRedChannel && hasGreenChannel && hasBlueChannel)
+ {
+ return ExrImageDataType.Rgb;
+ }
+
+ if (hasLuminance && this.Channels.Count == 1)
+ {
+ return ExrImageDataType.Gray;
+ }
+
+ return ExrImageDataType.Unknown;
+ }
+
+ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream)
+ {
+ // Skip over the magick bytes, we already know its an EXR image.
+ stream.Skip(4);
+
+ // Read version number.
+ byte version = (byte)stream.ReadByte();
+ if (version != 2)
+ {
+ ExrThrowHelper.ThrowNotSupportedVersion();
+ }
+
+ // Next three bytes contain info's about the image.
+ byte flagsByte0 = (byte)stream.ReadByte();
+ if ((flagsByte0 & (1 << 1)) != 0)
+ {
+ ExrThrowHelper.ThrowNotSupported("Decoding tiled exr images is not supported yet!");
+ }
+
+ // Discard the next two bytes.
+ int bytesRead = stream.Read(this.buffer, 0, 2);
+ if (bytesRead != 2)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for exr file!");
+ }
+
+ this.HeaderAttributes = this.ParseHeaderAttributes(stream);
+
+ this.Width = this.HeaderAttributes.DataWindow.XMax - this.HeaderAttributes.DataWindow.XMin + 1;
+ this.Height = this.HeaderAttributes.DataWindow.YMax - this.HeaderAttributes.DataWindow.YMin + 1;
+ this.Channels = this.HeaderAttributes.Channels;
+ this.Compression = this.HeaderAttributes.Compression;
+ this.PixelType = this.ValidateChannels();
+ this.ImageDataType = this.ReadImageDataType();
+
+ this.metadata = new ImageMetadata();
+
+ this.exrMetadata = this.metadata.GetExrMetadata();
+ this.exrMetadata.PixelType = this.PixelType;
+ this.exrMetadata.ImageDataType = this.ImageDataType;
+ this.exrMetadata.Compression = this.Compression;
+
+ return this.HeaderAttributes;
+ }
+
+ private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream)
+ {
+ ExrAttribute attribute = this.ReadAttribute(stream);
+
+ IList channels = null;
+ ExrBox2i? dataWindow = null;
+ ExrCompression? compression = null;
+ ExrBox2i? displayWindow = null;
+ ExrLineOrder? lineOrder = null;
+ float? aspectRatio = null;
+ float? screenWindowCenterX = null;
+ float? screenWindowCenterY = null;
+ float? screenWindowWidth = null;
+ uint? tileXSize = null;
+ uint? tileYSize = null;
+ int? chunkCount = null;
+ while (!attribute.Equals(ExrAttribute.EmptyAttribute))
+ {
+ switch (attribute.Name)
+ {
+ case ExrConstants.AttributeNames.Channels:
+ channels = this.ReadChannelList(stream, attribute.Length);
+ break;
+ case ExrConstants.AttributeNames.Compression:
+ compression = (ExrCompression)stream.ReadByte();
+ break;
+ case ExrConstants.AttributeNames.DataWindow:
+ dataWindow = this.ReadBoxInteger(stream);
+ break;
+ case ExrConstants.AttributeNames.DisplayWindow:
+ displayWindow = this.ReadBoxInteger(stream);
+ break;
+ case ExrConstants.AttributeNames.LineOrder:
+ lineOrder = (ExrLineOrder)stream.ReadByte();
+ break;
+ case ExrConstants.AttributeNames.PixelAspectRatio:
+ aspectRatio = this.ReadSingle(stream);
+ break;
+ case ExrConstants.AttributeNames.ScreenWindowCenter:
+ screenWindowCenterX = this.ReadSingle(stream);
+ screenWindowCenterY = this.ReadSingle(stream);
+ break;
+ case ExrConstants.AttributeNames.ScreenWindowWidth:
+ screenWindowWidth = this.ReadSingle(stream);
+ break;
+ case ExrConstants.AttributeNames.Tiles:
+ tileXSize = this.ReadUnsignedInteger(stream);
+ tileYSize = this.ReadUnsignedInteger(stream);
+ break;
+ case ExrConstants.AttributeNames.ChunkCount:
+ chunkCount = this.ReadSignedInteger(stream);
+ break;
+ default:
+ // Skip unknown attribute bytes.
+ stream.Skip(attribute.Length);
+ break;
+ }
+
+ attribute = this.ReadAttribute(stream);
+ }
+
+ if (!displayWindow.HasValue)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the displayWindow attribute is missing!");
+ }
+
+ if (!dataWindow.HasValue)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the dataWindow attribute is missing!");
+ }
+
+ if (channels is null)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the channels attribute is missing!");
+ }
+
+ if (!compression.HasValue)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the compression attribute is missing!");
+ }
+
+ if (!lineOrder.HasValue)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the lineOrder attribute is missing!");
+ }
+
+ if (!aspectRatio.HasValue)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the aspectRatio attribute is missing!");
+ }
+
+ if (!screenWindowWidth.HasValue)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the screenWindowWidth attribute is missing!");
+ }
+
+ if (!screenWindowCenterX.HasValue || !screenWindowCenterY.HasValue)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the screenWindowCenter attribute is missing!");
+ }
+
+ ExrHeaderAttributes header = new(
+ channels,
+ compression.Value,
+ dataWindow.Value,
+ displayWindow.Value,
+ lineOrder.Value,
+ aspectRatio.Value,
+ screenWindowWidth.Value,
+ new PointF(screenWindowCenterX.Value, screenWindowCenterY.Value),
+ tileXSize,
+ tileYSize,
+ chunkCount);
+ return header;
+ }
+
+ private ExrAttribute ReadAttribute(BufferedReadStream stream)
+ {
+ string attributeName = ReadString(stream);
+ if (attributeName.Equals(string.Empty, StringComparison.Ordinal))
+ {
+ return ExrAttribute.EmptyAttribute;
+ }
+
+ string attributeType = ReadString(stream);
+
+ int attributeSize = this.ReadSignedInteger(stream);
+
+ return new ExrAttribute(attributeName, attributeType, attributeSize);
+ }
+
+ private ExrBox2i ReadBoxInteger(BufferedReadStream stream)
+ {
+ int xMin = this.ReadSignedInteger(stream);
+ int yMin = this.ReadSignedInteger(stream);
+ int xMax = this.ReadSignedInteger(stream);
+ int yMax = this.ReadSignedInteger(stream);
+
+ return new ExrBox2i(xMin, yMin, xMax, yMax);
+ }
+
+ private List ReadChannelList(BufferedReadStream stream, int attributeSize)
+ {
+ List channels = [];
+ while (attributeSize > 1)
+ {
+ ExrChannelInfo channelInfo = this.ReadChannelInfo(stream, out int bytesRead);
+ channels.Add(channelInfo);
+ attributeSize -= bytesRead;
+ }
+
+ // Last byte should be a null byte.
+ if (stream.ReadByte() == -1)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data to read exr channel list!");
+ }
+
+ return channels;
+ }
+
+ private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesRead)
+ {
+ string channelName = ReadString(stream);
+ bytesRead = channelName.Length + 1;
+
+ ExrPixelType pixelType = (ExrPixelType)this.ReadSignedInteger(stream);
+ bytesRead += 4;
+
+ byte pLinear = (byte)stream.ReadByte();
+
+ // Next 3 bytes are reserved bytes and not use.
+ if (stream.Read(this.buffer, 0, 3) != 3)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data to read exr channel info!");
+ }
+
+ bytesRead += 4;
+
+ int xSampling = this.ReadSignedInteger(stream);
+ bytesRead += 4;
+
+ int ySampling = this.ReadSignedInteger(stream);
+ bytesRead += 4;
+
+ return new ExrChannelInfo(channelName, pixelType, pLinear, xSampling, ySampling);
+ }
+
+ private static string ReadString(BufferedReadStream stream)
+ {
+ StringBuilder str = new();
+ int character = stream.ReadByte();
+ if (character == 0)
+ {
+ // End of file header reached.
+ return string.Empty;
+ }
+
+ while (character != 0)
+ {
+ if (character == -1)
+ {
+ ExrThrowHelper.ThrowInvalidImageHeader();
+ }
+
+ str.Append((char)character);
+ character = stream.ReadByte();
+ }
+
+ return str.ToString();
+ }
+
+ private ExrPixelType FindPixelType()
+ {
+ ExrPixelType? pixelType = null;
+ for (int i = 0; i < this.Channels.Count; i++)
+ {
+ if (this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Blue, StringComparison.Ordinal) ||
+ this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Green, StringComparison.Ordinal) ||
+ this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Red, StringComparison.Ordinal) ||
+ this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Alpha, StringComparison.Ordinal) ||
+ this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Luminance, StringComparison.Ordinal))
+ {
+ if (!pixelType.HasValue)
+ {
+ pixelType = this.Channels[i].PixelType;
+ }
+ else
+ {
+ if (pixelType != this.Channels[i].PixelType)
+ {
+ ExrThrowHelper.ThrowNotSupported("Pixel channel data is expected to be the same for all channels.");
+ }
+ }
+ }
+ }
+
+ if (!pixelType.HasValue)
+ {
+ ExrThrowHelper.ThrowNotSupported("Pixel channel data is unknown! Only R, G, B, A and Y are supported.");
+ }
+
+ return pixelType.Value;
+ }
+
+ private bool IsSupportedCompression() => this.Compression switch
+ {
+ ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 => true,
+ _ => false,
+ };
+
+ private bool HasAlpha()
+ {
+ foreach (ExrChannelInfo channelInfo in this.Channels)
+ {
+ if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Reads a unsigned long value from the stream.
+ ///
+ /// The stream to read the data from.
+ /// The unsigned long value.
+ private ulong ReadUnsignedLong(BufferedReadStream stream)
+ {
+ int bytesRead = stream.Read(this.buffer, 0, 8);
+ if (bytesRead != 8)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a unsigned long from the stream!");
+ }
+
+ return BinaryPrimitives.ReadUInt64LittleEndian(this.buffer);
+ }
+
+ ///
+ /// Reads a unsigned integer value from the stream.
+ ///
+ /// The stream to read the data from.
+ /// The integer value.
+ private uint ReadUnsignedInteger(BufferedReadStream stream)
+ {
+ int bytesRead = stream.Read(this.buffer, 0, 4);
+ if (bytesRead != 4)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a unsigned int from the stream!");
+ }
+
+ return BinaryPrimitives.ReadUInt32LittleEndian(this.buffer);
+ }
+
+ ///
+ /// Reads a signed integer value from the stream.
+ ///
+ /// The stream to read the data from.
+ /// The integer value.
+ private int ReadSignedInteger(BufferedReadStream stream)
+ {
+ int bytesRead = stream.Read(this.buffer, 0, 4);
+ if (bytesRead != 4)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a signed int from the stream!");
+ }
+
+ return BinaryPrimitives.ReadInt32LittleEndian(this.buffer);
+ }
+
+ ///
+ /// Reads a float value from the stream.
+ ///
+ /// The stream to read the data from.
+ /// The float value.
+ private float ReadSingle(BufferedReadStream stream)
+ {
+ int bytesRead = stream.Read(this.buffer, 0, 4);
+ if (bytesRead != 4)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a float value from the stream!");
+ }
+
+ int intValue = BinaryPrimitives.ReadInt32BigEndian(this.buffer);
+
+ return Unsafe.As(ref intValue);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static TPixel ColorScaleTo32Bit(uint r, uint g, uint b, uint a)
+ where TPixel : unmanaged, IPixel
+ => TPixel.FromScaledVector4(new Vector4(r, g, b, a) * Scale32Bit);
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs b/src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs
new file mode 100644
index 0000000000..a8cda0b357
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Image decoder options for decoding OpenExr streams.
+///
+public sealed class ExrDecoderOptions : ISpecializedDecoderOptions
+{
+ ///
+ public DecoderOptions GeneralOptions { get; init; } = new();
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrEncoder.cs b/src/ImageSharp/Formats/Exr/ExrEncoder.cs
new file mode 100644
index 0000000000..2ea1b91161
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrEncoder.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Image encoder for writing an image to a stream in the OpenExr Format.
+///
+public sealed class ExrEncoder : ImageEncoder
+{
+ ///
+ /// Gets or sets the pixel type of the image.
+ ///
+ public ExrPixelType? PixelType { get; set; }
+
+ ///
+ /// Gets the compression type to use.
+ ///
+ public ExrCompression? Compression { get; init; }
+
+ ///
+ protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
+ {
+ ExrEncoderCore encoder = new(this, image.Configuration, image.Configuration.MemoryAllocator);
+ encoder.Encode(image, stream, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
new file mode 100644
index 0000000000..6f11bd9f4c
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
@@ -0,0 +1,531 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Exr.Compression;
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Image encoder for writing an image to a stream in the OpenExr format.
+///
+internal sealed class ExrEncoderCore
+{
+ ///
+ /// Reusable buffer.
+ ///
+ private readonly byte[] buffer = new byte[8];
+
+ ///
+ /// Used for allocating memory during processing operations.
+ ///
+ private readonly MemoryAllocator memoryAllocator;
+
+ ///
+ /// The global configuration.
+ ///
+ private readonly Configuration configuration;
+
+ ///
+ /// The encoder with options.
+ ///
+ private readonly ExrEncoder encoder;
+
+ ///
+ /// The pixel type of the image.
+ ///
+ private ExrPixelType? pixelType;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The encoder with options.
+ /// The configuration.
+ /// The memory manager.
+ public ExrEncoderCore(ExrEncoder encoder, Configuration configuration, MemoryAllocator memoryAllocator)
+ {
+ this.configuration = configuration;
+ this.encoder = encoder;
+ this.memoryAllocator = memoryAllocator;
+ this.Compression = encoder.Compression ?? ExrCompression.None;
+ }
+
+ ///
+ /// Gets or sets the compression implementation to use when encoding the image.
+ ///
+ internal ExrCompression Compression { get; set; }
+
+ ///
+ /// Encodes the image to the specified stream from the .
+ ///
+ /// The pixel format.
+ /// The to encode from.
+ /// The to encode the image data to.
+ /// The token to request cancellation.
+ public void Encode(Image image, Stream stream, CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ Guard.NotNull(image, nameof(image));
+ Guard.NotNull(stream, nameof(stream));
+
+ Buffer2D pixels = image.Frames.RootFrame.PixelBuffer;
+
+ ImageMetadata metadata = image.Metadata;
+ ExrMetadata exrMetadata = metadata.GetExrMetadata();
+ this.pixelType ??= exrMetadata.PixelType;
+ int width = image.Width;
+ int height = image.Height;
+ float aspectRatio = 1.0f;
+ ExrBox2i dataWindow = new(0, 0, width - 1, height - 1);
+ ExrBox2i displayWindow = new(0, 0, width - 1, height - 1);
+ ExrLineOrder lineOrder = ExrLineOrder.IncreasingY;
+ PointF screenWindowCenter = new(0.0f, 0.0f);
+ int screenWindowWidth = 1;
+ List channels =
+ [
+ new(ExrConstants.ChannelNames.Blue, this.pixelType.Value, 0, 1, 1),
+ new(ExrConstants.ChannelNames.Green, this.pixelType.Value, 0, 1, 1),
+ new(ExrConstants.ChannelNames.Red, this.pixelType.Value, 0, 1, 1),
+ ];
+ ExrHeaderAttributes header = new(
+ channels,
+ this.Compression,
+ dataWindow,
+ displayWindow,
+ lineOrder,
+ aspectRatio,
+ screenWindowWidth,
+ screenWindowCenter);
+
+ // Write magick bytes.
+ BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes);
+ stream.Write(this.buffer.AsSpan(0, 4));
+
+ // Version number.
+ this.buffer[0] = 2;
+
+ // Second, third and fourth bytes store info about the image, set all to default: zero.
+ this.buffer[1] = 0;
+ this.buffer[2] = 0;
+ this.buffer[3] = 0;
+ stream.Write(this.buffer.AsSpan(0, 4));
+
+ // Write EXR header.
+ this.WriteHeader(stream, header);
+
+ // Next is offsets table to each pixel row, which will be written after the pixel data was written.
+ ulong startOfRowOffsetData = (ulong)stream.Position;
+ stream.Position += 8 * height;
+
+ // Write pixel data.
+ switch (this.pixelType)
+ {
+ case ExrPixelType.Half:
+ case ExrPixelType.Float:
+ {
+ ulong[] rowOffsets = this.EncodeFloatingPointPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken);
+ stream.Position = (long)startOfRowOffsetData;
+ this.WriteRowOffsets(stream, height, rowOffsets);
+ break;
+ }
+
+ case ExrPixelType.UnsignedInt:
+ {
+ ulong[] rowOffsets = this.EncodeUnsignedIntPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken);
+ stream.Position = (long)startOfRowOffsetData;
+ this.WriteRowOffsets(stream, height, rowOffsets);
+ break;
+ }
+ }
+ }
+
+ private ulong[] EncodeFloatingPointPixelData(
+ Stream stream,
+ Buffer2D pixels,
+ int width,
+ int height,
+ List channels,
+ ExrCompression compression,
+ CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
+ uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
+ uint bytesPerBlock = bytesPerRow * rowsPerBlock;
+
+ using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean);
+ using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean);
+ Span redBuffer = rgbBuffer.GetSpan()[..width];
+ Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width);
+ Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
+
+ using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow);
+
+ ulong[] rowOffsets = new ulong[height];
+ for (uint y = 0; y < height; y += rowsPerBlock)
+ {
+ rowOffsets[y] = (ulong)stream.Position;
+
+ // Write row index.
+ BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y);
+ stream.Write(this.buffer.AsSpan(0, 4));
+
+ // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position.
+ long pixelDataSizePos = stream.Position;
+ stream.Position = pixelDataSizePos + 4;
+
+ uint rowsInBlockCount = 0;
+ for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++)
+ {
+ Span pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex);
+ for (int x = 0; x < width; x++)
+ {
+ Vector4 vector4 = pixelRowSpan[x].ToVector4();
+ redBuffer[x] = vector4.X;
+ greenBuffer[x] = vector4.Y;
+ blueBuffer[x] = vector4.Z;
+ }
+
+ // Write pixel data to row block buffer.
+ Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow);
+ switch (this.pixelType)
+ {
+ case ExrPixelType.Float:
+ WriteSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer);
+ break;
+ case ExrPixelType.Half:
+ WriteHalfSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer);
+ break;
+ }
+
+ rowsInBlockCount++;
+ }
+
+ // Write compressed pixel row data to the stream.
+ uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount);
+ long positionAfterPixelData = stream.Position;
+
+ // Write pixel row data size.
+ BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes);
+ stream.Position = pixelDataSizePos;
+ stream.Write(this.buffer.AsSpan(0, 4));
+ stream.Position = positionAfterPixelData;
+
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ return rowOffsets;
+ }
+
+ private ulong[] EncodeUnsignedIntPixelData(
+ Stream stream,
+ Buffer2D pixels,
+ int width,
+ int height,
+ List channels,
+ ExrCompression compression,
+ CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
+ uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
+ uint bytesPerBlock = bytesPerRow * rowsPerBlock;
+
+ using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean);
+ using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean);
+ Span redBuffer = rgbBuffer.GetSpan()[..width];
+ Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width);
+ Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
+
+ using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow);
+
+ Rgb96 rgb = default;
+ ulong[] rowOffsets = new ulong[height];
+ for (uint y = 0; y < height; y += rowsPerBlock)
+ {
+ rowOffsets[y] = (ulong)stream.Position;
+
+ // Write row index.
+ BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y);
+ stream.Write(this.buffer.AsSpan(0, 4));
+
+ // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position.
+ long pixelDataSizePos = stream.Position;
+ stream.Position = pixelDataSizePos + 4;
+
+ uint rowsInBlockCount = 0;
+ for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++)
+ {
+ Span pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex);
+ for (int x = 0; x < width; x++)
+ {
+ Vector4 vector4 = pixelRowSpan[x].ToVector4();
+ Rgb96.FromVector4(vector4);
+
+ redBuffer[x] = rgb.R;
+ greenBuffer[x] = rgb.G;
+ blueBuffer[x] = rgb.B;
+ }
+
+ // Write row data to row block buffer.
+ Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow);
+ WriteUnsignedIntRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer);
+ rowsInBlockCount++;
+ }
+
+ // Write pixel row data compressed to the stream.
+ uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount);
+ long positionAfterPixelData = stream.Position;
+
+ // Write pixel row data size.
+ BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes);
+ stream.Position = pixelDataSizePos;
+ stream.Write(this.buffer.AsSpan(0, 4));
+ stream.Position = positionAfterPixelData;
+
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ return rowOffsets;
+ }
+
+ private void WriteHeader(Stream stream, ExrHeaderAttributes header)
+ {
+ this.WriteChannels(stream, header.Channels);
+ this.WriteCompression(stream, header.Compression);
+ this.WriteDataWindow(stream, header.DataWindow);
+ this.WriteDisplayWindow(stream, header.DisplayWindow);
+ this.WritePixelAspectRatio(stream, header.AspectRatio);
+ this.WriteLineOrder(stream, header.LineOrder);
+ this.WriteScreenWindowCenter(stream, header.ScreenWindowCenter);
+ this.WriteScreenWindowWidth(stream, header.ScreenWindowWidth);
+ stream.WriteByte(0);
+ }
+
+ private static void WriteSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer)
+ {
+ int offset = 0;
+ for (int x = 0; x < width; x++)
+ {
+ WriteSingleToBuffer(buffer.Slice(offset, 4), blueBuffer[x]);
+ offset += 4;
+ }
+
+ for (int x = 0; x < width; x++)
+ {
+ WriteSingleToBuffer(buffer.Slice(offset, 4), greenBuffer[x]);
+ offset += 4;
+ }
+
+ for (int x = 0; x < width; x++)
+ {
+ WriteSingleToBuffer(buffer.Slice(offset, 4), redBuffer[x]);
+ offset += 4;
+ }
+ }
+
+ private static void WriteHalfSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer)
+ {
+ int offset = 0;
+ for (int x = 0; x < width; x++)
+ {
+ WriteHalfSingleToBuffer(buffer.Slice(offset, 2), blueBuffer[x]);
+ offset += 2;
+ }
+
+ for (int x = 0; x < width; x++)
+ {
+ WriteHalfSingleToBuffer(buffer.Slice(offset, 2), greenBuffer[x]);
+ offset += 2;
+ }
+
+ for (int x = 0; x < width; x++)
+ {
+ WriteHalfSingleToBuffer(buffer.Slice(offset, 2), redBuffer[x]);
+ offset += 2;
+ }
+ }
+
+ private static void WriteUnsignedIntRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer)
+ {
+ int offset = 0;
+ for (int x = 0; x < width; x++)
+ {
+ WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), blueBuffer[x]);
+ offset += 4;
+ }
+
+ for (int x = 0; x < width; x++)
+ {
+ WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), greenBuffer[x]);
+ offset += 4;
+ }
+
+ for (int x = 0; x < width; x++)
+ {
+ WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), redBuffer[x]);
+ offset += 4;
+ }
+ }
+
+ private void WriteRowOffsets(Stream stream, int height, ulong[] rowOffsets)
+ {
+ for (int i = 0; i < height; i++)
+ {
+ BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, rowOffsets[i]);
+ stream.Write(this.buffer);
+ }
+ }
+
+ private void WriteChannels(Stream stream, IList channels)
+ {
+ int attributeSize = 0;
+ foreach (ExrChannelInfo channelInfo in channels)
+ {
+ attributeSize += channelInfo.ChannelName.Length + 1;
+ attributeSize += 16;
+ }
+
+ // Last zero byte.
+ attributeSize++;
+ this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Channels, ExrConstants.AttibuteTypes.ChannelList, attributeSize);
+
+ foreach (ExrChannelInfo channelInfo in channels)
+ {
+ this.WriteChannelInfo(stream, channelInfo);
+ }
+
+ // Last byte should be zero.
+ stream.WriteByte(0);
+ }
+
+ private void WriteCompression(Stream stream, ExrCompression compression)
+ {
+ this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1);
+ stream.WriteByte((byte)compression);
+ }
+
+ private void WritePixelAspectRatio(Stream stream, float aspectRatio)
+ {
+ this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.PixelAspectRatio, ExrConstants.AttibuteTypes.Float, 4);
+ this.WriteSingle(stream, aspectRatio);
+ }
+
+ private void WriteLineOrder(Stream stream, ExrLineOrder lineOrder)
+ {
+ this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.LineOrder, ExrConstants.AttibuteTypes.LineOrder, 1);
+ stream.WriteByte((byte)lineOrder);
+ }
+
+ private void WriteScreenWindowCenter(Stream stream, PointF screenWindowCenter)
+ {
+ this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowCenter, ExrConstants.AttibuteTypes.TwoFloat, 8);
+ this.WriteSingle(stream, screenWindowCenter.X);
+ this.WriteSingle(stream, screenWindowCenter.Y);
+ }
+
+ private void WriteScreenWindowWidth(Stream stream, float screenWindowWidth)
+ {
+ this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowWidth, ExrConstants.AttibuteTypes.Float, 4);
+ this.WriteSingle(stream, screenWindowWidth);
+ }
+
+ private void WriteDataWindow(Stream stream, ExrBox2i dataWindow)
+ {
+ this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DataWindow, ExrConstants.AttibuteTypes.BoxInt, 16);
+ this.WriteBoxInteger(stream, dataWindow);
+ }
+
+ private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow)
+ {
+ this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DisplayWindow, ExrConstants.AttibuteTypes.BoxInt, 16);
+ this.WriteBoxInteger(stream, displayWindow);
+ }
+
+ private void WriteAttributeInformation(Stream stream, string name, string type, int size)
+ {
+ // Write attribute name.
+ WriteString(stream, name);
+
+ // Write attribute type.
+ WriteString(stream, type);
+
+ // Write attribute size.
+ BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size);
+ stream.Write(this.buffer.AsSpan(0, 4));
+ }
+
+ private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo)
+ {
+ WriteString(stream, channelInfo.ChannelName);
+
+ BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType);
+ stream.Write(this.buffer.AsSpan(0, 4));
+
+ stream.WriteByte(channelInfo.PLinear);
+
+ // Next 3 bytes are reserved and will set to zero.
+ stream.WriteByte(0);
+ stream.WriteByte(0);
+ stream.WriteByte(0);
+
+ BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.XSampling);
+ stream.Write(this.buffer.AsSpan(0, 4));
+
+ BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.YSampling);
+ stream.Write(this.buffer.AsSpan(0, 4));
+ }
+
+ private static void WriteString(Stream stream, string str)
+ {
+ foreach (char c in str)
+ {
+ stream.WriteByte((byte)c);
+ }
+
+ // Write termination byte.
+ stream.WriteByte(0);
+ }
+
+ private void WriteBoxInteger(Stream stream, ExrBox2i box)
+ {
+ BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin);
+ stream.Write(this.buffer.AsSpan(0, 4));
+
+ BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMin);
+ stream.Write(this.buffer.AsSpan(0, 4));
+
+ BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMax);
+ stream.Write(this.buffer.AsSpan(0, 4));
+
+ BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMax);
+ stream.Write(this.buffer.AsSpan(0, 4));
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private unsafe void WriteSingle(Stream stream, float value)
+ {
+ BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value);
+ stream.Write(this.buffer.AsSpan(0, 4));
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static unsafe void WriteSingleToBuffer(Span buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value);
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static void WriteHalfSingleToBuffer(Span buffer, float value)
+ {
+ ushort valueAsShort = HalfTypeHelper.Pack(value);
+ BinaryPrimitives.WriteUInt16LittleEndian(buffer, valueAsShort);
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static void WriteUnsignedIntToBuffer(Span buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrFormat.cs b/src/ImageSharp/Formats/Exr/ExrFormat.cs
new file mode 100644
index 0000000000..4415c15b06
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrFormat.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Registers the image encoders, decoders and mime type detectors for the OpenExr format.
+///
+public sealed class ExrFormat : IImageFormat
+{
+ private ExrFormat()
+ {
+ }
+
+ ///
+ /// Gets the current instance.
+ ///
+ public static ExrFormat Instance { get; } = new();
+
+ ///
+ public string Name => "EXR";
+
+ ///
+ public string DefaultMimeType => "image/x-exr";
+
+ ///
+ public IEnumerable MimeTypes => ExrConstants.MimeTypes;
+
+ ///
+ public IEnumerable FileExtensions => ExrConstants.FileExtensions;
+
+ ///
+ public ExrMetadata CreateDefaultFormatMetadata() => new();
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs
new file mode 100644
index 0000000000..cdcddd1175
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// The header of an EXR image.
+///
+///
+internal class ExrHeaderAttributes
+{
+ public ExrHeaderAttributes(
+ IList channels,
+ ExrCompression compression,
+ ExrBox2i dataWindow,
+ ExrBox2i displayWindow,
+ ExrLineOrder lineOrder,
+ float aspectRatio,
+ float screenWindowWidth,
+ PointF screenWindowCenter,
+ uint? tileXSize = null,
+ uint? tileYSize = null,
+ int? chunkCount = null)
+ {
+ this.Channels = channels;
+ this.Compression = compression;
+ this.DataWindow = dataWindow;
+ this.DisplayWindow = displayWindow;
+ this.LineOrder = lineOrder;
+ this.AspectRatio = aspectRatio;
+ this.ScreenWindowWidth = screenWindowWidth;
+ this.ScreenWindowCenter = screenWindowCenter;
+ this.TileXSize = tileXSize;
+ this.TileYSize = tileYSize;
+ this.ChunkCount = chunkCount;
+ }
+
+ ///
+ /// Gets or sets a description of the image channels stored in the file.
+ ///
+ public IList Channels { get; set; }
+
+ ///
+ /// Gets or sets the compression method applied to the pixel data of all channels in the file.
+ ///
+ public ExrCompression Compression { get; set; }
+
+ ///
+ /// Gets or sets the image’s data window.
+ ///
+ public ExrBox2i DataWindow { get; set; }
+
+ ///
+ /// Gets or sets the image’s display window.
+ ///
+ public ExrBox2i DisplayWindow { get; set; }
+
+ ///
+ /// Gets or sets in what order the scan lines in the file are stored in the file (increasing Y, decreasing Y, or, for tiled images, also random Y).
+ ///
+ public ExrLineOrder LineOrder { get; set; }
+
+ ///
+ /// Gets or sets the aspect ratio of the image.
+ ///
+ public float AspectRatio { get; set; }
+
+ ///
+ /// Gets or sets the screen width.
+ ///
+ public float ScreenWindowWidth { get; set; }
+
+ ///
+ /// Gets or sets the screen window center.
+ ///
+ public PointF ScreenWindowCenter { get; set; }
+
+ ///
+ /// Gets or sets the number of horizontal tiles.
+ ///
+ public uint? TileXSize { get; set; }
+
+ ///
+ /// Gets or sets the number of vertical tiles.
+ ///
+ public uint? TileYSize { get; set; }
+
+ ///
+ /// Gets or sets the chunk count. Indicates the number of chunks in this part. Required if the multipart bit (12) is set.
+ ///
+ public int? ChunkCount { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs b/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs
new file mode 100644
index 0000000000..62663a4a78
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+using System.Diagnostics.CodeAnalysis;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Detects OpenExr file headers.
+///
+public sealed class ExrImageFormatDetector : IImageFormatDetector
+{
+ ///
+ public int HeaderSize => 4;
+
+ private bool IsSupportedFileFormat(ReadOnlySpan header)
+ {
+ if (header.Length >= this.HeaderSize)
+ {
+ int fileTypeMarker = BinaryPrimitives.ReadInt32LittleEndian(header);
+ return fileTypeMarker == ExrConstants.MagickBytes;
+ }
+
+ return false;
+ }
+
+ ///
+ public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format)
+ {
+ format = this.IsSupportedFileFormat(header) ? ExrFormat.Instance : null;
+ return format != null;
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrMetadata.cs b/src/ImageSharp/Formats/Exr/ExrMetadata.cs
new file mode 100644
index 0000000000..1fa724657f
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrMetadata.cs
@@ -0,0 +1,118 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Provides OpenExr specific metadata information for the image.
+///
+public class ExrMetadata : IFormatMetadata
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ExrMetadata()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metadata to create an instance from.
+ private ExrMetadata(ExrMetadata other) => this.PixelType = other.PixelType;
+
+ ///
+ /// Gets or sets the pixel format.
+ ///
+ public ExrPixelType PixelType { get; set; } = ExrPixelType.Half;
+
+ ///
+ /// Gets or sets the image data type, either RGB, RGBA or gray.
+ ///
+ public ExrImageDataType ImageDataType { get; set; } = ExrImageDataType.Unknown;
+
+ ///
+ /// Gets or sets the compression method.
+ ///
+ public ExrCompression Compression { get; set; } = ExrCompression.None;
+
+ ///
+ public PixelTypeInfo GetPixelTypeInfo()
+ {
+ bool hasAlpha = this.ImageDataType is ExrImageDataType.Rgba;
+
+ int bitsPerComponent = 32;
+ int bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3;
+ if (this.PixelType == ExrPixelType.Half)
+ {
+ bitsPerComponent = 16;
+ bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3;
+ }
+
+ PixelAlphaRepresentation alpha = hasAlpha ? PixelAlphaRepresentation.Unassociated : PixelAlphaRepresentation.None;
+ PixelColorType color = PixelColorType.RGB;
+
+ int componentsCount = 0;
+ int[] precision = [];
+ switch (this.ImageDataType)
+ {
+ case ExrImageDataType.Rgb:
+ color = PixelColorType.RGB;
+ componentsCount = 3;
+ precision = new int[componentsCount];
+ precision[0] = bitsPerComponent;
+ precision[1] = bitsPerComponent;
+ precision[2] = bitsPerComponent;
+ break;
+ case ExrImageDataType.Rgba:
+ color = PixelColorType.RGB | PixelColorType.Alpha;
+ componentsCount = 4;
+ precision = new int[componentsCount];
+ precision[0] = bitsPerComponent;
+ precision[1] = bitsPerComponent;
+ precision[2] = bitsPerComponent;
+ precision[3] = bitsPerComponent;
+ break;
+ case ExrImageDataType.Gray:
+ color = PixelColorType.Luminance;
+ componentsCount = 1;
+ precision = new int[componentsCount];
+ precision[0] = bitsPerComponent;
+ break;
+ }
+
+ PixelComponentInfo info = PixelComponentInfo.Create(componentsCount, bitsPerPixel, precision);
+ return new PixelTypeInfo(bitsPerPixel)
+ {
+ AlphaRepresentation = alpha,
+ ComponentInfo = info,
+ ColorType = color
+ };
+ }
+
+ ///
+ public FormatConnectingMetadata ToFormatConnectingMetadata() => new()
+ {
+ EncodingType = this.Compression is ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Pxr24 ? EncodingType.Lossy : EncodingType.Lossless,
+ PixelTypeInfo = this.GetPixelTypeInfo()
+ };
+
+ ///
+ public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => new() { PixelType = ExrPixelType.Half };
+
+ ///
+ ExrMetadata IDeepCloneable.DeepClone() => new(this);
+
+ ///
+ public IDeepCloneable DeepClone() => new ExrMetadata(this);
+
+ ///
+ public void AfterImageApply(Image destination, Matrix4x4 matrix)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs b/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs
new file mode 100644
index 0000000000..51419ec95c
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+///
+/// Cold path optimizations for throwing exr format based exceptions.
+///
+internal static class ExrThrowHelper
+{
+ [DoesNotReturn]
+ public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}");
+
+ [DoesNotReturn]
+ public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage);
+
+ [DoesNotReturn]
+ public static void ThrowNotSupportedVersion() => throw new NotSupportedException("Unsupported EXR version");
+
+ [DoesNotReturn]
+ public static void ThrowNotSupported(string msg) => throw new NotSupportedException(msg);
+
+ [DoesNotReturn]
+ public static void ThrowInvalidImageHeader() => throw new InvalidImageContentException("Invalid EXR image header");
+
+ [DoesNotReturn]
+ public static void ThrowInvalidImageHeader(string msg) => throw new InvalidImageContentException(msg);
+
+ [DoesNotReturn]
+ public static Exception NotSupportedCompressor(string compressionType) => throw new NotSupportedException($"Not supported encoder compression method: {compressionType}");
+}
diff --git a/src/ImageSharp/Formats/Exr/ExrUtils.cs b/src/ImageSharp/Formats/Exr/ExrUtils.cs
new file mode 100644
index 0000000000..386210b81d
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/ExrUtils.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+
+namespace SixLabors.ImageSharp.Formats.Exr;
+
+internal static class ExrUtils
+{
+ ///
+ /// Calcualtes the required bytes for a pixel row.
+ ///
+ /// The image channels array.
+ /// The width in pixels of a row.
+ /// The number of bytes per row.
+ public static uint CalculateBytesPerRow(IList channels, uint width)
+ {
+ uint bytesPerRow = 0;
+ foreach (ExrChannelInfo channelInfo in channels)
+ {
+ if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)
+ || channelInfo.ChannelName.Equals("R", StringComparison.Ordinal)
+ || channelInfo.ChannelName.Equals("G", StringComparison.Ordinal)
+ || channelInfo.ChannelName.Equals("B", StringComparison.Ordinal)
+ || channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal))
+ {
+ if (channelInfo.PixelType == ExrPixelType.Half)
+ {
+ bytesPerRow += 2 * width;
+ }
+ else
+ {
+ bytesPerRow += 4 * width;
+ }
+ }
+ }
+
+ return bytesPerRow;
+ }
+
+ ///
+ /// Determines how many pixel rows there are in a block. This varies depending on the compression used.
+ ///
+ /// The compression used.
+ /// Pixel rows in a block.
+ public static uint RowsPerBlock(ExrCompression compression) => compression switch
+ {
+ ExrCompression.Zip or ExrCompression.Pxr24 => 16,
+ ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Piz => 32,
+ _ => 1,
+ };
+}
diff --git a/src/ImageSharp/Formats/Exr/README.md b/src/ImageSharp/Formats/Exr/README.md
new file mode 100644
index 0000000000..c71ab113d1
--- /dev/null
+++ b/src/ImageSharp/Formats/Exr/README.md
@@ -0,0 +1,4 @@
+### Some useful links for documentation about the OpenEXR format:
+
+- [Technical Introduction](https://openexr.readthedocs.io/en/latest/TechnicalIntroduction.html)
+- [OpenExr file layout](https://openexr.readthedocs.io/en/latest/OpenEXRFileLayout.html)
\ No newline at end of file
diff --git a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs
index 73d1145883..fa6eaa722d 100644
--- a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs
+++ b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs
@@ -13,6 +13,7 @@
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
+using SixLabors.ImageSharp.Formats.Exr;
namespace SixLabors.ImageSharp;
@@ -1143,4 +1144,106 @@ public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance),
cancellationToken);
+ ///
+ /// Saves the image to the given stream with the Exr format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// Thrown if the path is null.
+ public static void SaveAsExr(this Image source, string path) => SaveAsExr(source, path, default);
+
+ ///
+ /// Saves the image to the given stream with the Exr format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// Thrown if the path is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsExrAsync(this Image source, string path) => SaveAsExrAsync(source, path, default);
+
+ ///
+ /// Saves the image to the given stream with the Exr format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// The token to monitor for cancellation requests.
+ /// Thrown if the path is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsExrAsync(this Image source, string path, CancellationToken cancellationToken)
+ => SaveAsExrAsync(source, path, default, cancellationToken);
+
+ ///
+ /// Saves the image to the given stream with the Exr format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// The encoder to save the image with.
+ /// Thrown if the path is null.
+ public static void SaveAsExr(this Image source, string path, ExrEncoder encoder) =>
+ source.Save(
+ path,
+ encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance));
+
+ ///
+ /// Saves the image to the given stream with the Exr format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// The encoder to save the image with.
+ /// The token to monitor for cancellation requests.
+ /// Thrown if the path is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsExrAsync(this Image source, string path, ExrEncoder encoder, CancellationToken cancellationToken = default)
+ => source.SaveAsync(
+ path,
+ encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance),
+ cancellationToken);
+
+ ///
+ /// Saves the image to the given stream with the Exr format.
+ ///
+ /// The image this method extends.
+ /// The stream to save the image to.
+ /// Thrown if the stream is null.
+ public static void SaveAsExr(this Image source, Stream stream)
+ => SaveAsExr(source, stream, default);
+
+ ///
+ /// Saves the image to the given stream with the Exr format.
+ ///
+ /// The image this method extends.
+ /// The stream to save the image to.
+ /// The token to monitor for cancellation requests.
+ /// Thrown if the stream is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsExrAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
+ => SaveAsExrAsync(source, stream, default, cancellationToken);
+
+ ///
+ /// Saves the image to the given stream with the Exr format.
+ ///
+ /// The image this method extends.
+ /// The stream to save the image to.
+ /// The encoder to save the image with.
+ /// Thrown if the stream is null.
+ public static void SaveAsExr(this Image source, Stream stream, ExrEncoder encoder)
+ => source.Save(
+ stream,
+ encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance));
+
+ ///
+ /// Saves the image to the given stream with the Exr format.
+ ///
+ /// The image this method extends.
+ /// The stream to save the image to.
+ /// The encoder to save the image with.
+ /// The token to monitor for cancellation requests.
+ /// Thrown if the stream is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsExrAsync(this Image source, Stream stream, ExrEncoder encoder, CancellationToken cancellationToken = default)
+ => source.SaveAsync(
+ stream,
+ encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance),
+ cancellationToken);
+
}
diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs
index e35d00ed39..0dcbaed808 100644
--- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs
+++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs
@@ -14,6 +14,7 @@
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
+using SixLabors.ImageSharp.Formats.Exr;
namespace SixLabors.ImageSharp;
@@ -242,6 +243,26 @@ public static class ImageMetadataExtensions
/// The new
public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance);
+ ///
+ /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata
+ /// or the requested format default constructor.
+ /// This instance will be added to the metadata for future requests.
+ ///
+ /// The image metadata.
+ ///
+ /// The
+ ///
+ public static ExrMetadata GetExrMetadata(this ImageMetadata source) => source.GetFormatMetadata(ExrFormat.Instance);
+
+ ///
+ /// Creates a new cloned instance of from the .
+ /// The instance is created via
+ ///
+ /// The image metadata.
+ /// The new
+ public static ExrMetadata CloneExrMetadata(this ImageMetadata source) => source.CloneFormatMetadata(ExrFormat.Instance);
+
///
/// Gets the from .
diff --git a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude
index 2d6129c4c0..c1c69c5b5b 100644
--- a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude
+++ b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude
@@ -14,7 +14,8 @@
"Qoi",
"Tga",
"Tiff",
- "Webp"
+ "Webp",
+ "Exr"
];
private static readonly string[] frameFormats = [
diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj
index 2c7172387f..32a5f0073e 100644
--- a/src/ImageSharp/ImageSharp.csproj
+++ b/src/ImageSharp/ImageSharp.csproj
@@ -56,6 +56,11 @@
True
InlineArray.tt
+
+ True
+ True
+ ImageExtensions.Save.tt
+
True
True
@@ -141,11 +146,6 @@
True
PorterDuffFunctions.Generated.tt
-
- True
- True
- ImageExtensions.Save.tt
-
diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs
new file mode 100644
index 0000000000..5f61d5778b
--- /dev/null
+++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs
@@ -0,0 +1,195 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.PixelFormats;
+
+///
+/// Pixel type containing three 32-bit unsigned normalized values ranging from 0 to 4294967295.
+/// The color components are stored in red, green, blue.
+///
+/// Ranges from [0, 0, 0] to [1, 1, 1] in vector form.
+///
+///
+[StructLayout(LayoutKind.Sequential)]
+public partial struct Rgb96 : IPixel, IEquatable
+{
+ private const float InvMax = 1.0f / uint.MaxValue;
+
+ private const float Max = uint.MaxValue;
+
+ ///
+ /// Gets the red component.
+ ///
+ public uint R;
+
+ ///
+ /// Gets the green component.
+ ///
+ public uint G;
+
+ ///
+ /// Gets the blue component.
+ ///
+ public uint B;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The red component.
+ /// The green component.
+ /// The blue component.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Rgb96(uint r, uint g, uint b)
+ {
+ this.R = r;
+ this.G = g;
+ this.B = b;
+ }
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ ///
+ /// True if the parameter is equal to the parameter; otherwise, false.
+ ///
+ /// The on the right side of the operand.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(Rgb96 left, Rgb96 right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the parameter is not equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(Rgb96 left, Rgb96 right) => !left.Equals(right);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly Vector4 ToScaledVector4() => this.ToVector4();
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly Vector4 ToVector4() => new(
+ this.R * InvMax,
+ this.G * InvMax,
+ this.B * InvMax,
+ 1.0f);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PixelOperations CreatePixelOperations() => new();
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromScaledVector4(Vector4 source) => FromVector4(source);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromVector4(Vector4 source)
+ {
+ source = Numerics.Clamp(source, Vector4.Zero, Vector4.One) * Max;
+ return new Rgb96((uint)MathF.Round(source.X), (uint)MathF.Round(source.Y), (uint)MathF.Round(source.Z));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromAbgr32(Abgr32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromArgb32(Argb32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4());
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromBgr24(Bgr24 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromBgra32(Bgra32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromL8(L8 source)
+ {
+ uint rgb = ColorNumerics.From8BitTo32Bit(source.PackedValue);
+ return new Rgb96(rgb, rgb, rgb);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromL16(L16 source)
+ {
+ uint rgb = ColorNumerics.From16BitTo32Bit(source.PackedValue);
+ return new(rgb, rgb, rgb);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromLa16(La16 source)
+ {
+ uint rgb = ColorNumerics.From8BitTo32Bit((byte)source.PackedValue);
+ return new(rgb, rgb, rgb);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromLa32(La32 source)
+ {
+ uint rgb = ColorNumerics.From16BitTo32Bit(source.L);
+ return new(rgb, rgb, rgb);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromRgb24(Rgb24 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromRgba32(Rgba32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromRgb48(Rgb48 source) => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgb96 FromRgba64(Rgba64 source) => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B));
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create(
+ PixelComponentInfo.Create(3, 32, 32, 32),
+ PixelColorType.RGB,
+ PixelAlphaRepresentation.None);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly Rgba32 ToRgba32() => Rgba32.FromRgb96(this);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B);
+
+ ///
+ public override readonly string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})");
+
+ ///
+ public override readonly bool Equals(object? obj) => obj is Rgb96 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B);
+}
diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs
new file mode 100644
index 0000000000..77934eac81
--- /dev/null
+++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs
@@ -0,0 +1,191 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.PixelFormats;
+
+///
+/// Pixel type containing four 32-bit unsigned normalized values ranging from 0 to 4294967295.
+/// The color components are stored in red, green, blue and alpha.
+///
+/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form.
+///
+///
+[StructLayout(LayoutKind.Sequential)]
+public partial struct Rgba128 : IPixel, IEquatable
+{
+ private const float InvMax = 1.0f / uint.MaxValue;
+
+ private const float Max = uint.MaxValue;
+
+ ///
+ /// Gets the red component.
+ ///
+ public uint R;
+
+ ///
+ /// Gets the green component.
+ ///
+ public uint G;
+
+ ///
+ /// Gets the blue component.
+ ///
+ public uint B;
+
+ ///
+ /// Gets the alpha channel.
+ ///
+ public uint A;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The red component.
+ /// The green component.
+ /// The blue component.
+ /// The alpha component.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Rgba128(uint r, uint g, uint b, uint a)
+ {
+ this.R = r;
+ this.G = g;
+ this.B = b;
+ this.A = a;
+ }
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ ///
+ /// True if the parameter is equal to the parameter; otherwise, false.
+ ///
+ /// The on the right side of the operand.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(Rgba128 left, Rgba128 right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the parameter is not equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(Rgba128 left, Rgba128 right) => !left.Equals(right);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly Vector4 ToVector4() => new(
+ this.R * InvMax,
+ this.G * InvMax,
+ this.B * InvMax,
+ this.A * InvMax);
+
+ ///
+ public static PixelOperations CreatePixelOperations() => new();
+
+ ///
+ public static Rgba128 FromScaledVector4(Vector4 source) => FromVector4(source);
+
+ ///
+ public static Rgba128 FromVector4(Vector4 source)
+ {
+ source = Numerics.Clamp(source, Vector4.Zero, Vector4.One) * Max;
+ return new Rgba128((uint)MathF.Round(source.X), (uint)MathF.Round(source.Y), (uint)MathF.Round(source.Z), (uint)MathF.Round(source.W));
+ }
+
+ ///
+ public static Rgba128 FromAbgr32(Abgr32 source)
+ => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A));
+
+ ///
+ public static Rgba128 FromArgb32(Argb32 source)
+ => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A));
+
+ ///
+ public static Rgba128 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4());
+
+ ///
+ public static Rgba128 FromBgr24(Bgr24 source)
+ => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), uint.MaxValue);
+
+ ///
+ public static Rgba128 FromBgra32(Bgra32 source)
+ => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A));
+
+ ///
+ public static Rgba128 FromL8(L8 source)
+ {
+ uint rgb = ColorNumerics.From8BitTo32Bit(source.PackedValue);
+ return new Rgba128(rgb, rgb, rgb, rgb);
+ }
+
+ ///
+ public static Rgba128 FromL16(L16 source)
+ {
+ uint rgb = ColorNumerics.From16BitTo32Bit(source.PackedValue);
+ return new(rgb, rgb, rgb, rgb);
+ }
+
+ ///
+ public static Rgba128 FromLa16(La16 source)
+ {
+ uint rgb = ColorNumerics.From8BitTo32Bit((byte)source.PackedValue);
+ return new(rgb, rgb, rgb, rgb);
+ }
+
+ ///
+ public static Rgba128 FromLa32(La32 source)
+ {
+ uint rgb = ColorNumerics.From16BitTo32Bit(source.L);
+ return new(rgb, rgb, rgb, rgb);
+ }
+
+ ///
+ public static Rgba128 FromRgb24(Rgb24 source)
+ => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), uint.MaxValue);
+
+ ///
+ public static Rgba128 FromRgba32(Rgba32 source)
+ => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A));
+
+ ///
+ public static Rgba128 FromRgb48(Rgb48 source)
+ => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B), uint.MaxValue);
+
+ ///
+ public static Rgba128 FromRgba64(Rgba64 source)
+ => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B), ColorNumerics.From16BitTo32Bit(source.A));
+
+ ///
+ public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create(
+ PixelComponentInfo.Create(4, 32, 32, 32, 32),
+ PixelColorType.RGB | PixelColorType.Alpha,
+ PixelAlphaRepresentation.Unassociated);
+
+ ///
+ public readonly Rgba32 ToRgba32() => Rgba32.FromRgba128(this);
+
+ ///
+ public readonly Vector4 ToScaledVector4() => this.ToVector4();
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A);
+
+ ///
+ public override readonly string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})");
+
+ ///
+ public override readonly bool Equals(object? obj) => obj is Rgba128 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B && rgb.A == this.A;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly bool Equals(Rgba128 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B) && this.A.Equals(other.A);
+}
diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs
index 199754c690..1eafab854f 100644
--- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs
+++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs
@@ -314,6 +314,36 @@ public static Rgba32 FromRgba64(Rgba64 source)
A = ColorNumerics.From16BitTo8Bit(source.A)
};
+ ///
+ /// Initializes the pixel instance from an value.
+ ///
+ /// The value.
+ /// The pixel value as Rgba32.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgba32 FromRgb96(Rgb96 source)
+ => new()
+ {
+ R = ColorNumerics.From32BitTo8Bit(source.R),
+ G = ColorNumerics.From32BitTo8Bit(source.G),
+ B = ColorNumerics.From32BitTo8Bit(source.B),
+ A = byte.MaxValue
+ };
+
+ ///
+ /// Initializes the pixel instance from an value.
+ ///
+ /// The value.
+ /// The pixel value as Rgba32.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgba32 FromRgba128(Rgba128 source)
+ => new()
+ {
+ R = ColorNumerics.From32BitTo8Bit(source.R),
+ G = ColorNumerics.From32BitTo8Bit(source.G),
+ B = ColorNumerics.From32BitTo8Bit(source.B),
+ A = ColorNumerics.From32BitTo8Bit(source.A),
+ };
+
///
/// Converts the value of this instance to a hexadecimal string.
///
diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets
index 8c88ff647d..6b25509ed8 100644
--- a/tests/Directory.Build.targets
+++ b/tests/Directory.Build.targets
@@ -24,7 +24,7 @@
Do not update to 14+ yet. There's differnce in how the BMP decoder handles rounding in 16 bit images.
See https://github.com/ImageMagick/ImageMagick/commit/27a0a9c37f18af9c8d823a3ea076f600843b553c
-->
-
+
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs
new file mode 100644
index 0000000000..45021cadc0
--- /dev/null
+++ b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs
@@ -0,0 +1,68 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using BenchmarkDotNet.Attributes;
+
+using ImageMagick;
+using SixLabors.ImageSharp.Formats.Exr;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests;
+
+namespace SixLabors.ImageSharp.Benchmarks.Codecs;
+
+[MarkdownExporter]
+[HtmlExporter]
+[Config(typeof(Config.Short))]
+public class DecodeExr
+{
+ private Configuration configuration;
+
+ private byte[] imageBytes;
+
+ private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage);
+
+ [Params(TestImages.Exr.Benchmark)]
+ public string TestImage { get; set; }
+
+ [GlobalSetup]
+ public void ReadImages()
+ {
+ this.configuration = Configuration.CreateDefaultInstance();
+ new ExrConfigurationModule().Configure(this.configuration);
+
+ this.imageBytes ??= File.ReadAllBytes(this.TestImageFullPath);
+ }
+
+ [Benchmark(Description = "Magick Exr")]
+ public uint ExrImageMagick()
+ {
+ MagickReadSettings settings = new() { Format = MagickFormat.Exr };
+ using MemoryStream memoryStream = new(this.imageBytes);
+ using MagickImage image = new(memoryStream, settings);
+ return image.Width;
+ }
+
+ [Benchmark(Description = "ImageSharp Exr")]
+ public int ExrImageSharp()
+ {
+ using MemoryStream memoryStream = new(this.imageBytes);
+ using Image image = Image.Load(memoryStream);
+ return image.Height;
+ }
+
+ /* Results 27.03.2026
+ BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8037/25H2/2025Update/HudsonValley2)
+ Intel Core i7-14700T 1.30GHz, 1 CPU, 28 logical and 20 physical cores
+ .NET SDK 10.0.201
+ [Host] : .NET 8.0.25 (8.0.25, 8.0.2526.11203), X64 RyuJIT x86-64-v3
+ Job-VDWIGO : .NET 8.0.25 (8.0.25, 8.0.2526.11203), X64 RyuJIT x86-64-v3
+
+ Runtime=.NET 8.0 Arguments=/p:DebugType=portable IterationCount=3
+ LaunchCount=1 WarmupCount=3
+
+ | Method | TestImage | Mean | Error | StdDev | Allocated |
+ |----------------- |----------------------------- |---------:|---------:|---------:|----------:|
+ | 'Magick Exr' | Exr/Calliphora_benchmark.exr | 20.37 ms | 0.790 ms | 0.043 ms | 12.98 KB |
+ | 'ImageSharp Exr' | Exr/Calliphora_benchmark.exr | 45.68 ms | 4.999 ms | 0.274 ms | 34.09 KB |
+ */
+}
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs
index d6a6cf1fb4..4c81aee6d8 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs
@@ -27,7 +27,7 @@ public void SetupData()
=> this.data = File.ReadAllBytes(this.TestImageFullPath);
[Benchmark(Baseline = true, Description = "ImageMagick Tga")]
- public int TgaImageMagick()
+ public uint TgaImageMagick()
{
MagickReadSettings settings = new() { Format = MagickFormat.Tga };
using MagickImage image = new(new MemoryStream(this.data), settings);
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs
index bba1bc1871..a10f1527f1 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs
@@ -42,7 +42,7 @@ public void ReadImages()
}
[Benchmark(Description = "Magick Lossy Webp")]
- public int WebpLossyMagick()
+ public uint WebpLossyMagick()
{
MagickReadSettings settings = new() { Format = MagickFormat.WebP };
using MemoryStream memoryStream = new(this.webpLossyBytes);
@@ -59,7 +59,7 @@ public int WebpLossy()
}
[Benchmark(Description = "Magick Lossless Webp")]
- public int WebpLosslessMagick()
+ public uint WebpLosslessMagick()
{
MagickReadSettings settings = new()
{ Format = MagickFormat.WebP };
diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
index f8bf19d576..8835fdbcca 100644
--- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
+++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
@@ -248,10 +248,10 @@ public async Task ImageSharpResizeAsync(string input)
public void MagickResize(string input)
{
using MagickImage image = new(input);
- this.LogImageProcessed(image.Width, image.Height);
+ this.LogImageProcessed((int)image.Width, (int)image.Height);
// Resize it to fit a 150x150 square
- image.Resize(this.ThumbnailSize, this.ThumbnailSize);
+ image.Resize((uint)this.ThumbnailSize, (uint)this.ThumbnailSize);
// Reduce the size of the file
image.Strip();
diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs
index 3c6c759f82..bd65200844 100644
--- a/tests/ImageSharp.Tests/ConfigurationTests.cs
+++ b/tests/ImageSharp.Tests/ConfigurationTests.cs
@@ -20,7 +20,7 @@ public class ConfigurationTests
public Configuration DefaultConfiguration { get; }
- private readonly int expectedDefaultConfigurationCount = 11;
+ private readonly int expectedDefaultConfigurationCount = 12;
public ConfigurationTests()
{
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index caa6c507dc..e85c6bcdf7 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
@@ -91,13 +91,22 @@ public void BmpDecoder_CanDecodeBitfields(TestImageProvider prov
{
using Image image = provider.GetImage(BmpDecoder.Instance);
image.DebugSave(provider);
- image.CompareToOriginal(provider);
+ image.CompareToReferenceOutput(provider);
}
[Theory]
[WithFile(Bit16Inverted, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_16Bit_Inverted(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(BmpDecoder.Instance);
+ image.DebugSave(provider);
+ image.CompareToReferenceOutput(provider);
+ }
+
+ [Theory]
[WithFile(Bit8Inverted, PixelTypes.Rgba32)]
- public void BmpDecoder_CanDecode_Inverted(TestImageProvider provider)
+ public void BmpDecoder_CanDecode_8Bit_Inverted(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
using Image image = provider.GetImage(BmpDecoder.Instance);
@@ -156,7 +165,7 @@ public void BmpDecoder_CanDecode_16Bit(TestImageProvider provide
{
using Image image = provider.GetImage(BmpDecoder.Instance);
image.DebugSave(provider);
- image.CompareToOriginal(provider);
+ image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder(BmpFormat.Instance));
}
[Theory]
@@ -186,12 +195,12 @@ public void BmpDecoder_CanDecode_32BitV4Header_Fast(TestImageProvider(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
- RleSkippedPixelHandling skippedPixelHandling = TestEnvironment.IsWindows ? RleSkippedPixelHandling.Black : RleSkippedPixelHandling.FirstColorOfPalette;
+ RleSkippedPixelHandling skippedPixelHandling = RleSkippedPixelHandling.Black;
BmpDecoderOptions options = new() { RleSkippedPixelHandling = skippedPixelHandling };
using Image image = provider.GetImage(BmpDecoder.Instance, options);
image.DebugSave(provider);
- image.CompareToOriginal(provider);
+ image.CompareToReferenceOutput(provider);
}
[Theory]
@@ -224,8 +233,8 @@ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_SystemDrawingRe
}
}
+ // An RLE-compressed image that uses “delta” codes, to skip over some pixels.
[Theory]
- [WithFile(RLE8Cut, PixelTypes.Rgba32)]
[WithFile(RLE8Delta, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_MagickRefDecoder(TestImageProvider provider)
where TPixel : unmanaged, IPixel
@@ -236,11 +245,21 @@ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_MagickRefDecode
image.CompareToOriginal(provider, MagickReferenceDecoder.Png);
}
+ // An RLE-compressed image that uses “delta” codes, and early EOL & EOBMP markers, to skip over some pixels.
+ [Theory]
+ [WithFile(RLE8Cut, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette };
+ using Image image = provider.GetImage(BmpDecoder.Instance, options);
+ image.DebugSave(provider);
+ image.CompareToReferenceOutput(provider);
+ }
+
[Theory]
[WithFile(RLE8, PixelTypes.Rgba32, false)]
- [WithFile(RLE8Inverted, PixelTypes.Rgba32, false)]
[WithFile(RLE8, PixelTypes.Rgba32, true)]
- [WithFile(RLE8Inverted, PixelTypes.Rgba32, true)]
public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit(TestImageProvider provider, bool enforceDiscontiguousBuffers)
where TPixel : unmanaged, IPixel
{
@@ -255,6 +274,25 @@ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit(TestImageProvider
image.CompareToOriginal(provider, MagickReferenceDecoder.Png);
}
+ [Theory]
+ [WithFile(RLE8Inverted, PixelTypes.Rgba32, false)]
+ [WithFile(RLE8Inverted, PixelTypes.Rgba32, true)]
+ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted(TestImageProvider provider, bool enforceDiscontiguousBuffers)
+ where TPixel : unmanaged, IPixel
+ {
+ if (enforceDiscontiguousBuffers)
+ {
+ provider.LimitAllocatorBufferCapacity().InBytesSqrt(400);
+ }
+
+ BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette };
+ using Image image = provider.GetImage(BmpDecoder.Instance, options);
+ image.DebugSave(provider);
+
+ // The Reference decoder does not support decoding compressed bmp which are inverted (with negative height).
+ image.CompareToReferenceOutput(provider);
+ }
+
[Theory]
[WithFile(RLE24, PixelTypes.Rgba32, false)]
[WithFile(RLE24Cut, PixelTypes.Rgba32, false)]
diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs
new file mode 100644
index 0000000000..e6a00c84d5
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Exr;
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Exr;
+
+[Trait("Format", "Exr")]
+[ValidateDisposedMemoryAllocations]
+public class ExrDecoderTests
+{
+ private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.Exr;
+
+ [Theory]
+ [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)]
+ public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Half(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(ExrDecoder.Instance);
+ ExrMetadata exrMetaData = image.Metadata.GetExrMetadata();
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
+ Assert.Equal(ExrPixelType.Half, exrMetaData.PixelType);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Exr.UncompressedFloatRgb, PixelTypes.Rgba32)]
+ public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Float(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(ExrDecoder.Instance);
+ ExrMetadata exrMetaData = image.Metadata.GetExrMetadata();
+ image.DebugSave(provider);
+
+ // There is a 0,0059% difference to the Reference decoder.
+ image.CompareToOriginal(provider, ImageComparer.Tolerant(0.0005f), ReferenceDecoder);
+ Assert.Equal(ExrPixelType.Float, exrMetaData.PixelType);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Exr.UncompressedUintRgb, PixelTypes.Rgba32)]
+ public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(ExrDecoder.Instance);
+ ExrMetadata exrMetaData = image.Metadata.GetExrMetadata();
+ image.DebugSave(provider);
+
+ // Compare to referene output, since the reference decoder does not support this pixel type.
+ image.CompareToReferenceOutput(provider);
+ Assert.Equal(ExrPixelType.UnsignedInt, exrMetaData.PixelType);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Exr.Rgb, PixelTypes.Rgba32)]
+ public void ExrDecoder_CanDecode_Rgb(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(ExrDecoder.Instance);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Exr.Gray, PixelTypes.Rgba32)]
+ public void ExrDecoder_CanDecode_Gray(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(ExrDecoder.Instance);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Exr.Zip, PixelTypes.Rgba32)]
+ public void ExrDecoder_CanDecode_ZipCompressed(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(ExrDecoder.Instance);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Exr.Zips, PixelTypes.Rgba32)]
+ public void ExrDecoder_CanDecode_ZipsCompressed(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(ExrDecoder.Instance);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Exr.Rle, PixelTypes.Rgba32)]
+ public void ExrDecoder_CanDecode_RunLengthCompressed(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(ExrDecoder.Instance);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Exr.B44, PixelTypes.Rgba32)]
+ public void ExrDecoder_CanDecode_B44Compressed(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(ExrDecoder.Instance);
+ image.DebugSave(provider);
+
+ // Note: There is a 0,1190% difference to the reference decoder.
+ image.CompareToOriginal(provider, ImageComparer.Tolerant(0.011f), ReferenceDecoder);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs
new file mode 100644
index 0000000000..6aa3ebc70b
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.Formats.Exr;
+using SixLabors.ImageSharp.Formats.Exr.Constants;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Exr;
+
+[Trait("Format", "Exr")]
+[ValidateDisposedMemoryAllocations]
+public class ExrEncoderTests
+{
+ protected static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(ExrFormat.Instance);
+
+ [Theory]
+ [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)]
+ public void ExrEncoder_WithNoCompression_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "NoCompression", compression: ExrCompression.None);
+
+ [Theory]
+ [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)]
+ public void ExrEncoder_WithZipCompression_Works(TestImageProvider