Browse Source

Merge branch 'main' into js/accumulative-memory-limit

pull/3056/head
James Jackson-South 3 weeks ago
committed by GitHub
parent
commit
ceb656dab0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      ImageSharp.sln
  2. 65
      src/ImageSharp/Common/Helpers/ColorNumerics.cs
  3. 3
      src/ImageSharp/Configuration.cs
  4. 2
      src/ImageSharp/Formats/Bmp/BmpConstants.cs
  5. 2
      src/ImageSharp/Formats/Bmp/BmpFormat.cs
  6. 36
      src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs
  7. 84
      src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs
  8. 210
      src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
  9. 39
      src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs
  10. 98
      src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs
  11. 67
      src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs
  12. 66
      src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs
  13. 67
      src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
  14. 39
      src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs
  15. 42
      src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs
  16. 63
      src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs
  17. 30
      src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs
  18. 21
      src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs
  19. 25
      src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs
  20. 25
      src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs
  21. 43
      src/ImageSharp/Formats/Exr/ExrAttribute.cs
  22. 33
      src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs
  23. 48
      src/ImageSharp/Formats/Exr/ExrBox2i.cs
  24. 60
      src/ImageSharp/Formats/Exr/ExrChannelInfo.cs
  25. 18
      src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs
  26. 82
      src/ImageSharp/Formats/Exr/ExrConstants.cs
  27. 48
      src/ImageSharp/Formats/Exr/ExrDecoder.cs
  28. 935
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  29. 13
      src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs
  30. 29
      src/ImageSharp/Formats/Exr/ExrEncoder.cs
  31. 699
      src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
  32. 34
      src/ImageSharp/Formats/Exr/ExrFormat.cs
  33. 108
      src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs
  34. 34
      src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs
  35. 156
      src/ImageSharp/Formats/Exr/ExrMetadata.cs
  36. 33
      src/ImageSharp/Formats/Exr/ExrThrowHelper.cs
  37. 52
      src/ImageSharp/Formats/Exr/ExrUtils.cs
  38. 4
      src/ImageSharp/Formats/Exr/README.md
  39. 103
      src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs
  40. 21
      src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs
  41. 3
      src/ImageSharp/Formats/_Generated/_Formats.ttinclude
  42. 10
      src/ImageSharp/ImageSharp.csproj
  43. 203
      src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs
  44. 200
      src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs
  45. 30
      src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs
  46. 2
      tests/Directory.Build.targets
  47. 68
      tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs
  48. 2
      tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs
  49. 4
      tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs
  50. 16
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  51. 65
      tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs
  52. 1
      tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
  53. 13
      tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
  54. 262
      tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputBenchmark.cs
  55. 80
      tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
  56. 2
      tests/ImageSharp.Tests/ConfigurationTests.cs
  57. 54
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  58. 133
      tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs
  59. 80
      tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs
  60. 118
      tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs
  61. 136
      tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs
  62. 56
      tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs
  63. 311
      tests/ImageSharp.Tests/PixelFormats/Rgb96Tests.cs
  64. 313
      tests/ImageSharp.Tests/PixelFormats/Rgba128Tests.cs
  65. 16
      tests/ImageSharp.Tests/TestImages.cs
  66. 18
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
  67. 4
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
  68. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_issue735.png
  69. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565.png
  70. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565pal.png
  71. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16bfdef.png
  72. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bf.png
  73. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bfdef.png
  74. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_16Bit_Inverted_Rgba32_test16-inverted.png
  75. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rlecut.png
  76. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rletrns.png
  77. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_rle4-delta-320x240.png
  78. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted_Rgba32_RunLengthEncoded-inverted.png
  79. 3
      tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder_Rgba32_pal8rlecut.png
  80. 3
      tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint_Rgba32_Calliphora_uint32_uncompressed.png
  81. 3
      tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Uint_Rgba32_rgba_uint_uncompressed.png
  82. 3
      tests/Images/Input/Exr/Calliphora_b44.exr
  83. 3
      tests/Images/Input/Exr/Calliphora_benchmark.exr
  84. 3
      tests/Images/Input/Exr/Calliphora_gray.exr
  85. 3
      tests/Images/Input/Exr/Calliphora_rgb.exr
  86. 3
      tests/Images/Input/Exr/Calliphora_rle.exr
  87. 3
      tests/Images/Input/Exr/Calliphora_uint32_uncompressed.exr
  88. 3
      tests/Images/Input/Exr/Calliphora_uncompressed.exr
  89. 3
      tests/Images/Input/Exr/Calliphora_uncompressed_rgba.exr
  90. 3
      tests/Images/Input/Exr/Calliphora_zip.exr
  91. 3
      tests/Images/Input/Exr/Calliphora_zips.exr
  92. 3
      tests/Images/Input/Exr/rgb_float32_uncompressed.exr
  93. 3
      tests/Images/Input/Exr/rgba_uint_uncompressed.exr

4
ImageSharp.sln

@ -37,8 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{815C0625-CD3
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
src\Directory.Build.props = src\Directory.Build.props src\Directory.Build.props = src\Directory.Build.props
src\Directory.Build.targets = src\Directory.Build.targets src\Directory.Build.targets = src\Directory.Build.targets
src\README.md = src\README.md
src\ImageSharp.ruleset = src\ImageSharp.ruleset src\ImageSharp.ruleset = src\ImageSharp.ruleset
src\README.md = src\README.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp", "src\ImageSharp\ImageSharp.csproj", "{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}" 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 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-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-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\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\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 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\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\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\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 EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}"

65
src/ImageSharp/Common/Helpers/ColorNumerics.cs

@ -97,7 +97,7 @@ internal static class ColorNumerics
/// Scales a value from a 16 bit <see cref="ushort"/> to an /// Scales a value from a 16 bit <see cref="ushort"/> to an
/// 8 bit <see cref="byte"/> equivalent. /// 8 bit <see cref="byte"/> equivalent.
/// </summary> /// </summary>
/// <param name="component">The 8 bit component value.</param> /// <param name="component">The 16 bit component value.</param>
/// <returns>The <see cref="byte"/></returns> /// <returns>The <see cref="byte"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte From16BitTo8Bit(ushort component) => public static byte From16BitTo8Bit(ushort component) =>
@ -132,6 +132,49 @@ internal static class ColorNumerics
// (V * 255 + 32895) >> 16 // (V * 255 + 32895) >> 16
(byte)(((component * 255) + 32895) >> 16); (byte)(((component * 255) + 32895) >> 16);
/// <summary>
/// Scales a value from a 32 bit <see cref="uint"/> to an
/// 8 bit <see cref="byte"/> equivalent.
/// </summary>
/// <param name="component">The 32 bit component value.</param>
/// <returns>The <see cref="byte"/> value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte From32BitTo8Bit(uint component) =>
// To scale to 8 bits from a 32-bit value V the required value is:
//
// (V * 255) / 4294967295
//
// Since:
//
// 4294967295 = 255 * 16843009
//
// this reduces exactly to:
//
// V / 16843009
//
// To round to nearest using integer arithmetic we add half the divisor
// before dividing:
//
// (V + 16843009 / 2) / 16843009
//
// where:
//
// 16843009 / 2 = 8421504.5
//
// Using 8421504 ensures correct round-to-nearest behaviour:
//
// 8421504 -> 0
// 8421505 -> 1
//
// The addition must be performed in 64-bit to avoid overflow for large
// input values (for example uint.MaxValue).
//
// Final exact integer implementation:
//
// ((ulong)V + 8421504) / 16843009
(byte)((component + 8421504UL) / 16843009UL);
/// <summary> /// <summary>
/// Scales a value from an 8 bit <see cref="byte"/> to /// Scales a value from an 8 bit <see cref="byte"/> to
/// an 16 bit <see cref="ushort"/> equivalent. /// an 16 bit <see cref="ushort"/> equivalent.
@ -142,6 +185,26 @@ internal static class ColorNumerics
public static ushort From8BitTo16Bit(byte component) public static ushort From8BitTo16Bit(byte component)
=> (ushort)(component * 257); => (ushort)(component * 257);
/// <summary>
/// Scales a value from an 16 bit <see cref="byte"/> to
/// an 16 bit <see cref="uint"/> equivalent.
/// </summary>
/// <param name="component">The 16 bit component value.</param>
/// <returns>The 32 bit <see cref="uint"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint From16BitTo32Bit(ushort component)
=> (uint)(component * 65537);
/// <summary>
/// Scales a value from an 8 bit <see cref="byte"/> to
/// an 32 bit <see cref="ushort"/> equivalent.
/// </summary>
/// <param name="component">The 8 bit component value.</param>
/// <returns>The 32 bit <see cref="uint"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint From8BitTo32Bit(byte component)
=> (uint)(component * 16843009);
/// <summary> /// <summary>
/// Returns how many bits are required to store the specified number of colors. /// Returns how many bits are required to store the specified number of colors.
/// Performs a Log2() on the value. /// Performs a Log2() on the value.

3
src/ImageSharp/Configuration.cs

@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
@ -213,6 +214,7 @@ public sealed class Configuration
/// <see cref="TgaConfigurationModule"/>. /// <see cref="TgaConfigurationModule"/>.
/// <see cref="TiffConfigurationModule"/>. /// <see cref="TiffConfigurationModule"/>.
/// <see cref="WebpConfigurationModule"/>. /// <see cref="WebpConfigurationModule"/>.
/// <see cref="ExrConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>. /// <see cref="QoiConfigurationModule"/>.
/// </summary> /// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns> /// <returns>The default configuration of <see cref="Configuration"/>.</returns>
@ -225,6 +227,7 @@ public sealed class Configuration
new TgaConfigurationModule(), new TgaConfigurationModule(),
new TiffConfigurationModule(), new TiffConfigurationModule(),
new WebpConfigurationModule(), new WebpConfigurationModule(),
new ExrConfigurationModule(),
new QoiConfigurationModule(), new QoiConfigurationModule(),
new IcoConfigurationModule(), new IcoConfigurationModule(),
new CurConfigurationModule()); new CurConfigurationModule());

2
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. // Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Bmp; namespace SixLabors.ImageSharp.Formats.Bmp;

2
src/ImageSharp/Formats/Bmp/BmpFormat.cs

@ -30,5 +30,5 @@ public sealed class BmpFormat : IImageFormat<BmpMetadata>
public IEnumerable<string> FileExtensions => BmpConstants.FileExtensions; public IEnumerable<string> FileExtensions => BmpConstants.FileExtensions;
/// <inheritdoc/> /// <inheritdoc/>
public BmpMetadata CreateDefaultFormatMetadata() => new(); public BmpMetadata CreateDefaultFormatMetadata() => new BmpMetadata();
} }

36
src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs

@ -0,0 +1,36 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;
/// <summary>
/// Compressor for EXR image data which does not use any compression method.
/// </summary>
internal class NoneExrCompressor : ExrBaseCompressor
{
/// <summary>
/// Initializes a new instance of the <see cref="NoneExrCompressor"/> class.
/// </summary>
/// <param name="output">The output stream to write the compressed image data to.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">Bytes per row block.</param>
/// <param name="bytesPerRow">Bytes per pixel row.</param>
public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(output, allocator, bytesPerBlock, bytesPerRow)
{
}
/// <inheritdoc/>
public override uint CompressRowBlock(Span<byte> rows, int rowCount)
{
this.Output.Write(rows);
return (uint)rows.Length;
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}

84
src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs

@ -0,0 +1,84 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;
/// <summary>
/// Compressor for EXR image data using the ZIP compression.
/// </summary>
internal class ZipExrCompressor : ExrBaseCompressor
{
private readonly DeflateCompressionLevel compressionLevel;
private readonly MemoryStream memoryStream;
private readonly System.Buffers.IMemoryOwner<byte> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="ZipExrCompressor"/> class.
/// </summary>
/// <param name="output">The stream to write the compressed data to.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="compressionLevel">The compression level for deflate compression.</param>
public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, DeflateCompressionLevel compressionLevel)
: base(output, allocator, bytesPerBlock, bytesPerRow)
{
this.compressionLevel = compressionLevel;
this.buffer = allocator.Allocate<byte>((int)bytesPerBlock);
this.memoryStream = new();
}
/// <inheritdoc/>
public override uint CompressRowBlock(Span<byte> rows, int rowCount)
{
// Re-oder pixel values.
Span<byte> 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<byte> 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;
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
this.buffer.Dispose();
this.memoryStream?.Dispose();
}
}

210
src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs

@ -0,0 +1,210 @@
// 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;
/// <summary>
/// Implementation of B44 decompressor for EXR image data.
/// </summary>
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<ushort> tmpBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="B44ExrCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The rows per block.</param>
/// <param name="width">The width of a pixel row in pixels.</param>
/// <param name="channelCount">The number of channels of the image.</param>
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<ushort>((int)(width * rowsPerBlock * channelCount));
}
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<ushort> outputBuffer = MemoryMarshal.Cast<byte, ushort>(buffer);
Span<ushort> 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<ushort> row0 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row1 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row2 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> 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[rowOffset..]);
this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]);
this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]);
this.s.AsSpan(12, n).CopyTo(row3[rowOffset..]);
}
else
{
this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]);
if (y + 1 < this.rowsPerBlock)
{
this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]);
}
if (y + 2 < this.rowsPerBlock)
{
this.s.AsSpan(8, n).CopyTo(row2[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[offsetOutput..]);
offsetOutput += this.width;
}
offsetDecompressed += this.width;
}
}
/// <summary>
/// Unpack a 14-byte block into 4 by 4 16-bit pixels.
/// </summary>
/// <param name="b">The source byte data to unpack.</param>
/// <param name="s">Destintation buffer.</param>
private static void Unpack14(Span<byte> b, Span<ushort> 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];
}
}
}
/// <summary>
/// // Unpack a 3-byte block into 4 by 4 identical 16-bit pixels.
/// </summary>
/// <param name="b">The source byte data to unpack.</param>
/// <param name="s">The destination buffer.</param>
private static void Unpack3(Span<byte> b, Span<ushort> 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];
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}

39
src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs

@ -0,0 +1,39 @@
// 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;
/// <summary>
/// Decompressor for EXR image data which do not use any compression.
/// </summary>
internal class NoneExrCompression : ExrBaseDecompressor
{
/// <summary>
/// Initializes a new instance of the <see cref="NoneExrCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per pixel row.</param>
public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow)
{
}
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> 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 from the stream!");
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}

98
src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs

@ -0,0 +1,98 @@
// 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;
/// <summary>
/// Implementation of RLE decompressor for EXR images.
/// </summary>
internal class RunLengthExrCompression : ExrBaseDecompressor
{
private readonly IMemoryOwner<byte> tmpBuffer;
private readonly ushort[] s = new ushort[16];
/// <summary>
/// Initializes a new instance of the <see cref="RunLengthExrCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<byte> 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);
}
/// <summary>
/// Reads the next byte from the stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns>The next byte.</returns>
private static byte ReadNextByte(BufferedReadStream stream)
{
int nextByte = stream.ReadByte();
if (nextByte == -1)
{
ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to decompress RLE encoded EXR image!");
}
return (byte)nextByte;
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}

67
src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs

@ -0,0 +1,67 @@
// 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;
/// <summary>
/// Implementation of zhe Zip decompressor for EXR image data.
/// </summary>
internal class ZipExrCompression : ExrBaseDecompressor
{
private readonly IMemoryOwner<byte> tmpBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="ZipExrCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per pixel row.</param>
public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<byte> 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 enough data for zip compressed image data!");
}
Reconstruct(uncompressed, (uint)totalRead);
Interleave(uncompressed, (uint)totalRead, buffer);
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}

66
src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs

@ -0,0 +1,66 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression;
/// <summary>
/// Base class for EXR compression.
/// </summary>
internal abstract class ExrBaseCompression : IDisposable
{
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="ExrBaseCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
{
this.Allocator = allocator;
this.BytesPerBlock = bytesPerBlock;
this.BytesPerRow = bytesPerRow;
}
/// <summary>
/// Gets the memory allocator.
/// </summary>
protected MemoryAllocator Allocator { get; }
/// <summary>
/// Gets the bits per pixel.
/// </summary>
public int BitsPerPixel { get; }
/// <summary>
/// Gets the bytes per row.
/// </summary>
public uint BytesPerRow { get; }
/// <summary>
/// Gets the uncompressed bytes per block.
/// </summary>
public uint BytesPerBlock { get; }
/// <summary>
/// Gets the image width.
/// </summary>
public int Width { get; }
/// <inheritdoc />
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.isDisposed = true;
this.Dispose(true);
}
protected abstract void Dispose(bool disposing);
}

67
src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs

@ -0,0 +1,67 @@
// 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;
/// <summary>
/// The base EXR decompressor class.
/// </summary>
internal abstract class ExrBaseDecompressor : ExrBaseCompression
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrBaseDecompressor" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per row block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow)
{
}
/// <summary>
/// Decompresses the specified stream.
/// </summary>
/// <param name="stream">The buffered stream to decompress.</param>
/// <param name="compressedBytes">The compressed bytes.</param>
/// <param name="buffer">The buffer to write the decompressed data to.</param>
public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer);
/// <summary>
/// Integrate over all differences to the previous value in order to
/// reconstruct sample values.
/// </summary>
/// <param name="buffer">The buffer with the data.</param>
/// <param name="unCompressedBytes">The un compressed bytes.</param>
protected static void Reconstruct(Span<byte> 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++;
}
}
/// <summary>
/// Interleaves the input data.
/// </summary>
/// <param name="source">The source data.</param>
/// <param name="unCompressedBytes">The uncompressed bytes.</param>
/// <param name="output">The output to write to.</param>
protected static void Interleave(Span<byte> source, uint unCompressedBytes, Span<byte> output)
{
int sourceOffset = 0;
int offset0 = 0;
int offset1 = (int)((unCompressedBytes + 1) / 2);
while (sourceOffset < unCompressedBytes)
{
output[sourceOffset++] = source[offset0++];
output[sourceOffset++] = source[offset1++];
}
}
}

39
src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs

@ -0,0 +1,39 @@
// 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;
/// <summary>
/// Factory class for creating a compressor for EXR image data.
/// </summary>
internal static class ExrCompressorFactory
{
/// <summary>
/// Creates the specified exr data compressor.
/// </summary>
/// <param name="method">The compression method.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="output">The output stream.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="compressionLevel">The deflate compression level.</param>
/// <returns>A compressor for EXR image data.</returns>
public static ExrBaseCompressor Create(
ExrCompression method,
MemoryAllocator allocator,
Stream output,
uint bytesPerBlock,
uint bytesPerRow,
DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression) => method switch
{
ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow),
ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel),
ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel),
_ => throw ExrThrowHelper.NotSupportedCompressor(method.ToString()),
};
}

42
src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs

@ -0,0 +1,42 @@
// 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;
/// <summary>
/// The Factory class for creating a EXR data decompressor.
/// </summary>
internal static class ExrDecompressorFactory
{
/// <summary>
/// Creates a decomprssor for a specific EXR compression type.
/// </summary>
/// <param name="method">The compression method.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="width">The width in pixels of the image.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The rows per block.</param>
/// <param name="channelCount">The number of image channels.</param>
/// <returns>Decompressor for EXR image data.</returns>
public static ExrBaseDecompressor Create(
ExrCompression method,
MemoryAllocator memoryAllocator,
int width,
uint bytesPerBlock,
uint bytesPerRow,
uint rowsPerBlock,
int channelCount) => method switch
{
ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow),
ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow),
ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow),
ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow),
ExrCompression.B44 => new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount),
_ => throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)),
};
}

63
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;
/// <summary>
/// Enumeration representing the compression formats defined by the EXR file-format.
/// </summary>
public enum ExrCompression
{
/// <summary>
/// Pixel data is not compressed.
/// </summary>
None = 0,
/// <summary>
/// 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.
/// </summary>
RunLengthEncoded = 1,
/// <summary>
/// Uses the open source zlib library for compression. Unlike ZIP compression, this operates one scan line at a time.
/// Compression is lossless.
/// </summary>
Zips = 2,
/// <summary>
/// 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.
/// </summary>
Zip = 3,
/// <summary>
/// A wavelet transform is applied to the pixel data, and the result is Huffman-encoded.
/// Compression is lossless.
/// </summary>
Piz = 4,
/// <summary>
/// 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.
/// </summary>
Pxr24 = 5,
/// <summary>
/// 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.
/// </summary>
B44 = 6,
/// <summary>
/// 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.
/// </summary>
B44A = 7
}

30
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;
/// <summary>
/// This enum represents the type of pixel data in the EXR image.
/// </summary>
public enum ExrImageDataType
{
/// <summary>
/// The pixel data is unknown.
/// </summary>
Unknown = 0,
/// <summary>
/// The pixel data has 3 channels: red, green and blue.
/// </summary>
Rgb = 1,
/// <summary>
/// The pixel data has four channels: red, green, blue and a alpha channel.
/// </summary>
Rgba = 2,
/// <summary>
/// There is only one channel with the luminance.
/// </summary>
Gray = 3,
}

21
src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr.Constants;
/// <summary>
/// Enum for the differnt exr image type.
/// </summary>
internal enum ExrImageType
{
/// <summary>
/// The image data is stored in scan lines.
/// </summary>
ScanLine = 0,
/// <summary>
/// The image data is stored in tile.
/// This is not yet supported.
/// </summary>
Tiled = 1
}

25
src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr.Constants;
/// <summary>
/// Enum for the different scan line ordering.
/// </summary>
internal enum ExrLineOrder : byte
{
/// <summary>
/// The scan lines are written from top-to-bottom.
/// </summary>
IncreasingY = 0,
/// <summary>
/// The scan lines are written from bottom-to-top.
/// </summary>
DecreasingY = 1,
/// <summary>
/// The Scan lines are written in no particular oder.
/// </summary>
RandomY = 2
}

25
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;
/// <summary>
/// The different pixel formats for a OpenEXR image.
/// </summary>
public enum ExrPixelType
{
/// <summary>
/// unsigned int (32 bit).
/// </summary>
UnsignedInt = 0,
/// <summary>
/// half (16 bit floating point).
/// </summary>
Half = 1,
/// <summary>
/// float (32 bit floating point).
/// </summary>
Float = 2
}

43
src/ImageSharp/Formats/Exr/ExrAttribute.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Repressents an exr image attribute.
/// </summary>
[DebuggerDisplay("Name: {Name}, Type: {Type}, Length: {Length}")]
internal class ExrAttribute
{
public static readonly ExrAttribute EmptyAttribute = new(string.Empty, string.Empty, 0);
/// <summary>
/// Initializes a new instance of the <see cref="ExrAttribute"/> class.
/// </summary>
/// <param name="name">The name of the attribute.</param>
/// <param name="type">The type of the attribute.</param>
/// <param name="length">The length in bytes.</param>
public ExrAttribute(string name, string type, int length)
{
this.Name = name;
this.Type = type;
this.Length = length;
}
/// <summary>
/// Gets the name of the attribute.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the type of the attribute.
/// </summary>
public string Type { get; }
/// <summary>
/// Gets the length in bytes of the attribute.
/// </summary>
public int Length { get; }
}

33
src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression;
internal abstract class ExrBaseCompressor : ExrBaseCompression
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrBaseCompressor"/> class.
/// </summary>
/// <param name="output">The output stream to write the compressed image to.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">Bytes per row block.</param>
/// <param name="bytesPerRow">Bytes per pixel row.</param>
protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow)
=> this.Output = output;
/// <summary>
/// Gets the output stream to write the compressed image to.
/// </summary>
public Stream Output { get; }
/// <summary>
/// Compresses a block of rows of the image.
/// </summary>
/// <param name="rows">Image rows to compress.</param>
/// <param name="rowCount">The number of rows to compress.</param>
/// <returns>Number of bytes of of the compressed data.</returns>
public abstract uint CompressRowBlock(Span<byte> rows, int rowCount);
}

48
src/ImageSharp/Formats/Exr/ExrBox2i.cs

@ -0,0 +1,48 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Integer region definition.
/// </summary>
[DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")]
internal readonly struct ExrBox2i
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrBox2i"/> struct.
/// </summary>
/// <param name="xMin">The minimum x value.</param>
/// <param name="yMin">The minimum y value.</param>
/// <param name="xMax">The maximum x value.</param>
/// <param name="yMax">The maximum y value.</param>
public ExrBox2i(int xMin, int yMin, int xMax, int yMax)
{
this.XMin = xMin;
this.YMin = yMin;
this.XMax = xMax;
this.YMax = yMax;
}
/// <summary>
/// Gets the minimum x value.
/// </summary>
public int XMin { get; }
/// <summary>
/// Gets the minimum y value.
/// </summary>
public int YMin { get; }
/// <summary>
/// Gets the maximum x value.
/// </summary>
public int XMax { get; }
/// <summary>
/// Gets the maximum y value.
/// </summary>
public int YMax { get; }
}

60
src/ImageSharp/Formats/Exr/ExrChannelInfo.cs

@ -0,0 +1,60 @@
// 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;
/// <summary>
/// Information about a pixel channel.
/// </summary>
[DebuggerDisplay("Name: {ChannelName}, PixelType: {PixelType}")]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal readonly struct ExrChannelInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrChannelInfo" /> struct.
/// </summary>
/// <param name="channelName">Name of the channel.</param>
/// <param name="pixelType">The type of the pixel data.</param>
/// <param name="linear">Linear flag, possible values are 0 and 1.</param>
/// <param name="xSampling">X sampling.</param>
/// <param name="ySampling">Y sampling.</param>
public ExrChannelInfo(string channelName, ExrPixelType pixelType, byte linear, int xSampling, int ySampling)
{
this.ChannelName = channelName;
this.PixelType = pixelType;
this.Linear = linear;
this.XSampling = xSampling;
this.YSampling = ySampling;
}
/// <summary>
/// Gets the channel name.
/// </summary>
public string ChannelName { get; }
/// <summary>
/// Gets the type of the pixel data.
/// </summary>
public ExrPixelType PixelType { get; }
/// <summary>
/// Gets the linear flag. Hint to lossy compression methods that indicates whether
/// human perception of the quantity represented by this channel
/// is closer to linear or closer to logarithmic.
/// </summary>
public byte Linear { get; }
/// <summary>
/// Gets the x sampling value.
/// </summary>
public int XSampling { get; }
/// <summary>
/// Gets the y sampling value.
/// </summary>
public int YSampling { get; }
}

18
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the OpenExr format.
/// </summary>
public sealed class ExrConfigurationModule : IImageFormatConfigurationModule
{
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetEncoder(ExrFormat.Instance, new ExrEncoder());
configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, ExrDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new ExrImageFormatDetector());
}
}

82
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;
/// <summary>
/// Defines constants relating to OpenExr images.
/// </summary>
internal static class ExrConstants
{
/// <summary>
/// The list of mimetypes that equate to a OpenExr image.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/x-exr" };
/// <summary>
/// The list of file extensions that equate to a OpenExr image.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "exr" };
/// <summary>
/// The magick bytes identifying an OpenExr image.
/// </summary>
public static readonly int MagickBytes = 20000630;
/// <summary>
/// EXR attribute names.
/// </summary>
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";
}
/// <summary>
/// EXR attribute types.
/// </summary>
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";
}
}

48
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;
/// <summary>
/// Image decoder for generating an image out of a OpenExr stream.
/// </summary>
public class ExrDecoder : ImageDecoder
{
private ExrDecoder()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static ExrDecoder Instance { get; } = new();
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
ExrDecoderCore decoder = new(new ExrDecoderOptions { GeneralOptions = options });
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}
/// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken);
}

935
src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

@ -0,0 +1,935 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers;
using System.Buffers.Binary;
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;
/// <summary>
/// Performs the OpenExr decoding operation.
/// </summary>
internal sealed class ExrDecoderCore : ImageDecoderCore
{
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[8];
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The global configuration.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The metadata.
/// </summary>
private ImageMetadata metadata;
/// <summary>
/// The exr specific metadata.
/// </summary>
private ExrMetadata exrMetadata;
/// <summary>
/// Initializes a new instance of the <see cref="ExrDecoderCore"/> class.
/// </summary>
/// <param name="options">The options.</param>
public ExrDecoderCore(ExrDecoderOptions options)
: base(options.GeneralOptions)
{
this.configuration = options.GeneralOptions.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator;
}
/// <summary>
/// Gets or sets the image width.
/// </summary>
private int Width { get; set; }
/// <summary>
/// Gets or sets the image height.
/// </summary>
private int Height { get; set; }
/// <summary>
/// Gets or sets the image channel info's.
/// </summary>
private IList<ExrChannelInfo> Channels { get; set; }
/// <summary>
/// Gets or sets the compression method.
/// </summary>
private ExrCompression Compression { get; set; }
/// <summary>
/// Gets or sets the image data type, either RGB, RGBA or gray.
/// </summary>
private ExrImageDataType ImageDataType { get; set; }
/// <summary>
/// Gets or sets the pixel type.
/// </summary>
private ExrPixelType PixelType { get; set; }
/// <summary>
/// Gets or sets the header attributes.
/// </summary>
private ExrHeaderAttributes HeaderAttributes { get; set; }
/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ReadExrHeader(stream);
if (!this.IsSupportedCompression())
{
ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported");
}
Image<TPixel> image = new(this.configuration, this.Width, this.Height, this.metadata);
Buffer2D<TPixel> 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;
}
/// <inheritdoc />
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);
}
/// <summary>
/// Decodes image data with floating point pixel data.
/// </summary>
/// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="stream">The stream to read from.</param>
/// <param name="pixels">The pixel buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void DecodeFloatingPointPixelData<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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<float> rowBuffer = this.memoryAllocator.Allocate<float>(width * 4);
using IMemoryOwner<byte> decompressedPixelDataBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock);
Span<byte> decompressedPixelData = decompressedPixelDataBuffer.GetSpan();
Span<float> redPixelData = rowBuffer.GetSpan()[..width];
Span<float> greenPixelData = rowBuffer.GetSpan().Slice(width, width);
Span<float> bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width);
Span<float> 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<TPixel> 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();
}
}
/// <summary>
/// Decodes image data with unsigned int pixel data.
/// </summary>
/// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="stream">The stream to read from.</param>
/// <param name="pixels">The pixel buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void DecodeUnsignedIntPixelData<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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<uint> rowBuffer = this.memoryAllocator.Allocate<uint>(width * 4);
using IMemoryOwner<byte> decompressedPixelDataBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock);
Span<byte> decompressedPixelData = decompressedPixelDataBuffer.GetSpan();
Span<uint> redPixelData = rowBuffer.GetSpan()[..width];
Span<uint> greenPixelData = rowBuffer.GetSpan().Slice(width, width);
Span<uint> bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width);
Span<uint> 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<TPixel> 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++)
{
Rgba128 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue);
pixelRow[x] = TPixel.FromVector4(pixelValue.ToVector4());
}
decodedRows++;
}
stream.Position = nextRowOffsetPosition;
cancellationToken.ThrowIfCancellationRequested();
}
}
/// <summary>
/// Reads float image channel data.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="channel">The channel info.</param>
/// <param name="decompressedPixelData">The decompressed pixel data.</param>
/// <param name="redPixelData">The red channel pixel data.</param>
/// <param name="greenPixelData">The green channel pixel data.</param>
/// <param name="bluePixelData">The blue channel pixel data.</param>
/// <param name="alphaPixelData">The alpha channel pixel data.</param>
/// <param name="width">The width of a row in pixels.</param>
/// <returns>The bytes read.</returns>
private static int ReadFloatChannelData(
BufferedReadStream stream,
ExrChannelInfo channel,
Span<byte> decompressedPixelData,
Span<float> redPixelData,
Span<float> greenPixelData,
Span<float> bluePixelData,
Span<float> 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;
}
}
/// <summary>
/// Reads UINT image channel data.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="channel">The channel info.</param>
/// <param name="decompressedPixelData">The decompressed pixel data.</param>
/// <param name="redPixelData">The red channel pixel data.</param>
/// <param name="greenPixelData">The green channel pixel data.</param>
/// <param name="bluePixelData">The blue channel pixel data.</param>
/// <param name="alphaPixelData">The alpha channel pixel data.</param>
/// <param name="width">The width of a row in pixels.</param>
/// <returns>The bytes read.</returns>
private int ReadUnsignedIntChannelData(
BufferedReadStream stream,
ExrChannelInfo channel,
Span<byte> decompressedPixelData,
Span<uint> redPixelData,
Span<uint> greenPixelData,
Span<uint> bluePixelData,
Span<uint> 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;
}
}
/// <summary>
/// Reads the channel data for pixel type HALF or FLOAT.
/// </summary>
/// <param name="channel">The channel info.</param>
/// <param name="decompressedPixelData">The decompressed pixel data.</param>
/// <param name="pixelData">The pixel data as float.</param>
/// <param name="width">The width in pixel of a row.</param>
/// <returns>The bytes read.</returns>
private static int ReadChannelData(ExrChannelInfo channel, Span<byte> decompressedPixelData, Span<float> pixelData, int width) => channel.PixelType switch
{
ExrPixelType.Half => ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width),
ExrPixelType.Float => ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width),
_ => 0,
};
/// <summary>
/// Reads the channel data for pixel type UINT.
/// </summary>
/// <param name="channel">The channel info.</param>
/// <param name="decompressedPixelData">The decompressed pixel data.</param>
/// <param name="pixelData">The pixel data as uint.</param>
/// <param name="width">The width in pixels.</param>
/// <returns>The bytes read.</returns>
private static int ReadChannelData(ExrChannelInfo channel, Span<byte> decompressedPixelData, Span<uint> pixelData, int width) => channel.PixelType switch
{
ExrPixelType.UnsignedInt => ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width),
_ => 0,
};
/// <summary>
/// Reads a pixel row with the pixel data being 16 bit half values.
/// </summary>
/// <param name="decompressedPixelData">The decompressed pixel data.</param>
/// <param name="channelData">The channel data as float.</param>
/// <param name="width">The width of a row in pixels.</param>
/// <returns>The bytes read.</returns>
private static int ReadPixelRowChannelHalfSingle(Span<byte> decompressedPixelData, Span<float> 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;
}
/// <summary>
/// Reads a pixel row with 32 bit float pixel data.
/// </summary>
/// <param name="decompressedPixelData">The decompressed pixel data.</param>
/// <param name="channelData">The pixel data as float.</param>
/// <param name="width">The width in pixels of a row.</param>
/// <returns>The bytes read.</returns>
private static int ReadPixelRowChannelSingle(Span<byte> decompressedPixelData, Span<float> 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<int, float>(ref intValue);
offset += 4;
}
return offset;
}
/// <summary>
/// Reads a pixel row with the pixel typ UINT.
/// </summary>
/// <param name="decompressedPixelData">The decompressed pixel bytes.</param>
/// <param name="channelData">The uint pixel data.</param>
/// <param name="width">The width of a row in pixels.</param>
/// <returns>The bytes read.</returns>
private static int ReadPixelRowChannelUnsignedInt(Span<byte> decompressedPixelData, Span<uint> 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;
}
/// <summary>
/// Validates that all image channels have the same type and are among the supported pixel types.
/// </summary>
/// <returns>The pixel type.</returns>
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 = 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;
}
/// <summary>
/// Determines the type image from the channel information.
/// </summary>
/// <returns>The image data type.</returns>
private ExrImageDataType DetermineImageDataType()
{
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;
}
/// <summary>
/// Reads the exr image header.
/// <see href="https://openexr.com/en/latest/OpenEXRFileLayout.html#header-attributes-all-files/"/>
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns>The image header attributes.</returns>
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.DetermineImageDataType();
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;
}
/// <summary>
/// Parses the image header attributes.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>The image header attributes.</returns>
private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream)
{
ExrAttribute attribute = this.ReadAttribute(stream);
IList<ExrChannelInfo> 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;
}
/// <summary>
/// Reads a attrbute from the stream, which consist of a name, a type and a size in bytes.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>A attribute.</returns>
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);
}
/// <summary>
/// Reads a box attribute, which is a xMin, xMax and yMin, yMax value.
/// </summary>
/// <param name="stream">The stream to reaad from.</param>
/// <returns>A box struct.</returns>
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);
}
/// <summary>
/// Reads the channel list from the stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="attributeSize">The size in bytes of the channel list attribute.</param>
/// <returns>The channel list.</returns>
private List<ExrChannelInfo> ReadChannelList(BufferedReadStream stream, int attributeSize)
{
List<ExrChannelInfo> 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 the exr channel list!");
}
return channels;
}
/// <summary>
/// Reads the channel information from the stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="bytesRead">The bytes read.</param>
/// <returns>Channel info.</returns>
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);
}
/// <summary>
/// Reads a the string from the stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>A string.</returns>
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();
}
/// <summary>
/// Determines whether the compression is supported.
/// </summary>
/// <returns> True if the compression is supported; otherwise, false>. </returns>
private bool IsSupportedCompression() => this.Compression switch
{
ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 => true,
_ => false,
};
/// <summary>
/// Determines whether this image has alpha channel.
/// </summary>
/// <returns> True if this image has a alpha channel; otherwise, false. </returns>
private bool HasAlpha()
{
foreach (ExrChannelInfo channelInfo in this.Channels)
{
if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
/// <summary>
/// Reads a unsigned long value from the stream.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <returns>The unsigned long value.</returns>
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);
}
/// <summary>
/// Reads a unsigned integer value from the stream.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <returns>The integer value.</returns>
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);
}
/// <summary>
/// Reads a signed integer value from the stream.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <returns>The integer value.</returns>
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);
}
/// <summary>
/// Reads a float value from the stream.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <returns>The float value.</returns>
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<int, float>(ref intValue);
}
}

13
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;
/// <summary>
/// Image decoder options for decoding OpenExr streams.
/// </summary>
public sealed class ExrDecoderOptions : ISpecializedDecoderOptions
{
/// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new();
}

29
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;
/// <summary>
/// Image encoder for writing an image to a stream in the OpenExr Format.
/// </summary>
public sealed class ExrEncoder : ImageEncoder
{
/// <summary>
/// Gets or sets the pixel type of the image.
/// </summary>
public ExrPixelType? PixelType { get; set; }
/// <summary>
/// Gets the compression type to use.
/// </summary>
public ExrCompression? Compression { get; init; }
/// <inheritdoc />
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
ExrEncoderCore encoder = new(this, image.Configuration, image.Configuration.MemoryAllocator);
encoder.Encode(image, stream, cancellationToken);
}
}

699
src/ImageSharp/Formats/Exr/ExrEncoderCore.cs

@ -0,0 +1,699 @@
// 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;
/// <summary>
/// Image encoder for writing an image to a stream in the OpenExr format.
/// </summary>
internal sealed class ExrEncoderCore
{
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[8];
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The global configuration.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The encoder with options.
/// </summary>
private readonly ExrEncoder encoder;
/// <summary>
/// The pixel type of the image.
/// </summary>
private ExrPixelType? pixelType;
/// <summary>
/// Initializes a new instance of the <see cref="ExrEncoderCore"/> class.
/// </summary>
/// <param name="encoder">The encoder with options.</param>
/// <param name="configuration">The configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param>
public ExrEncoderCore(ExrEncoder encoder, Configuration configuration, MemoryAllocator memoryAllocator)
{
this.configuration = configuration;
this.encoder = encoder;
this.memoryAllocator = memoryAllocator;
this.Compression = encoder.Compression ?? ExrCompression.None;
this.pixelType = encoder.PixelType;
}
/// <summary>
/// Gets or sets the compression implementation to use when encoding the image.
/// </summary>
internal ExrCompression Compression { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="ImageFrame{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
Buffer2D<TPixel> 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<ExrChannelInfo> channels =
[
new(ExrConstants.ChannelNames.Alpha, this.pixelType.Value, 0, 1, 1),
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;
}
}
}
/// <summary>
/// Encodes and writes pixel data with float pixel data to the stream.
/// </summary>
/// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="stream">The stream to write to.</param>
/// <param name="pixels">The pixel bufer.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="channels">The imagechannels.</param>
/// <param name="compression">The compression to use.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The array of pixel row offsets.</returns>
private ulong[] EncodeFloatingPointPixelData<TPixel>(
Stream stream,
Buffer2D<TPixel> pixels,
int width,
int height,
List<ExrChannelInfo> channels,
ExrCompression compression,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
using IMemoryOwner<float> rgbBuffer = this.memoryAllocator.Allocate<float>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
Span<float> redBuffer = rgbBuffer.GetSpan()[..width];
Span<float> greenBuffer = rgbBuffer.GetSpan().Slice(width, width);
Span<float> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<float> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, 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<TPixel> 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;
alphaBuffer[x] = vector4.W;
}
// Write pixel data to row block buffer.
Span<byte> rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow);
switch (this.pixelType)
{
case ExrPixelType.Float:
WriteSingleRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer);
break;
case ExrPixelType.Half:
WriteHalfSingleRow(rowBlockSpan, width, alphaBuffer, 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;
}
/// <summary>
/// Encodes and writes pixel data with the unsigned int pixel type to the stream.
/// </summary>
/// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="stream">The stream to write to.</param>
/// <param name="pixels">The pixel bufer.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="channels">The imagechannels.</param>
/// <param name="compression">The compression to use.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The array of pixel row offsets.</returns>
private ulong[] EncodeUnsignedIntPixelData<TPixel>(
Stream stream,
Buffer2D<TPixel> pixels,
int width,
int height,
List<ExrChannelInfo> channels,
ExrCompression compression,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
using IMemoryOwner<uint> rgbBuffer = this.memoryAllocator.Allocate<uint>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
Span<uint> redBuffer = rgbBuffer.GetSpan()[..width];
Span<uint> greenBuffer = rgbBuffer.GetSpan().Slice(width, width);
Span<uint> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<uint> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow);
Rgba128 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<TPixel> pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex);
for (int x = 0; x < width; x++)
{
Vector4 vector4 = pixelRowSpan[x].ToVector4();
rgb = Rgba128.FromVector4(vector4);
redBuffer[x] = rgb.R;
greenBuffer[x] = rgb.G;
blueBuffer[x] = rgb.B;
alphaBuffer[x] = rgb.A;
}
// Write row data to row block buffer.
Span<byte> rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow);
WriteUnsignedIntRow(rowBlockSpan, width, alphaBuffer, 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;
}
/// <summary>
/// Writes the image header to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="header">The header.</param>
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);
}
/// <summary>
/// Writes a row of pixels with the FLOAT pixel type to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="width">The width of a row in pixels.</param>
/// <param name="alphaBuffer">The alpha channel buffer.</param>
/// <param name="blueBuffer">The blue channel buffer.</param>
/// <param name="greenBuffer">The green channel buffer.</param>
/// <param name="redBuffer">The red channel buffer.</param>
private static void WriteSingleRow(Span<byte> buffer, int width, Span<float> alphaBuffer, Span<float> blueBuffer, Span<float> greenBuffer, Span<float> redBuffer)
{
int offset = 0;
for (int x = 0; x < width; x++)
{
WriteSingleToBuffer(buffer.Slice(offset, 4), alphaBuffer[x]);
offset += 4;
}
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;
}
}
/// <summary>
/// Writes a row of pixels with the HALF pixel type to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="width">The width of a row in pixels.</param>
/// <param name="alphaBuffer">The alpha channel buffer.</param>
/// <param name="blueBuffer">The blue channel buffer.</param>
/// <param name="greenBuffer">The green channel buffer.</param>
/// <param name="redBuffer">The red channel buffer.</param>
private static void WriteHalfSingleRow(Span<byte> buffer, int width, Span<float> alphaBuffer, Span<float> blueBuffer, Span<float> greenBuffer, Span<float> redBuffer)
{
int offset = 0;
for (int x = 0; x < width; x++)
{
WriteHalfSingleToBuffer(buffer.Slice(offset, 2), alphaBuffer[x]);
offset += 2;
}
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;
}
}
/// <summary>
/// Writes a row of pixels with unsigned int pixel data to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="width">The width of the row in pixels.</param>
/// <param name="alphaBuffer">The alpha channel buffer.</param>
/// <param name="blueBuffer">The blue channel buffer.</param>
/// <param name="greenBuffer">The green channel buffer.</param>
/// <param name="redBuffer">The red channel buffer.</param>
private static void WriteUnsignedIntRow(Span<byte> buffer, int width, Span<uint> alphaBuffer, Span<uint> blueBuffer, Span<uint> greenBuffer, Span<uint> redBuffer)
{
int offset = 0;
for (int x = 0; x < width; x++)
{
WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), alphaBuffer[x]);
offset += 4;
}
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;
}
}
/// <summary>
/// Writes the row offsets to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="height">The height in pixels of the image.</param>
/// <param name="rowOffsets">The row offsets.</param>
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);
}
}
/// <summary>
/// Writes the channel infos to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="channels">The channels.</param>
private void WriteChannels(Stream stream, IList<ExrChannelInfo> 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);
}
/// <summary>
/// Writes info about a single channel to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="channelInfo">The channel information.</param>
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.Linear);
// 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));
}
/// <summary>
/// Writes the compression type to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="compression">The compression type.</param>
private void WriteCompression(Stream stream, ExrCompression compression)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1);
stream.WriteByte((byte)compression);
}
/// <summary>
/// Writes the pixel aspect ratio to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="aspectRatio">The aspect ratio.</param>
private void WritePixelAspectRatio(Stream stream, float aspectRatio)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.PixelAspectRatio, ExrConstants.AttibuteTypes.Float, 4);
this.WriteSingle(stream, aspectRatio);
}
/// <summary>
/// Writes the line order to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="lineOrder">The line order.</param>
private void WriteLineOrder(Stream stream, ExrLineOrder lineOrder)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.LineOrder, ExrConstants.AttibuteTypes.LineOrder, 1);
stream.WriteByte((byte)lineOrder);
}
/// <summary>
/// Writes the screen window center to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="screenWindowCenter">The screen window center.</param>
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);
}
/// <summary>
/// Writes the screen width to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="screenWindowWidth">Width of the screen window.</param>
private void WriteScreenWindowWidth(Stream stream, float screenWindowWidth)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowWidth, ExrConstants.AttibuteTypes.Float, 4);
this.WriteSingle(stream, screenWindowWidth);
}
/// <summary>
/// Writes the data window to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="dataWindow">The data window.</param>
private void WriteDataWindow(Stream stream, ExrBox2i dataWindow)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DataWindow, ExrConstants.AttibuteTypes.BoxInt, 16);
this.WriteBoxInteger(stream, dataWindow);
}
/// <summary>
/// Writes the display window to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="displayWindow">The display window.</param>
private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DisplayWindow, ExrConstants.AttibuteTypes.BoxInt, 16);
this.WriteBoxInteger(stream, displayWindow);
}
/// <summary>
/// Writes attribute information to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="type">The type of the attribute.</param>
/// <param name="size">The size in bytes of the attribute.</param>
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));
}
/// <summary>
/// Writes a string to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="str">The string to write.</param>
private static void WriteString(Stream stream, string str)
{
foreach (char c in str)
{
stream.WriteByte((byte)c);
}
// Write termination byte.
stream.WriteByte(0);
}
/// <summary>
/// Writes box struct with xmin, xmax, ymin and y max to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="box">The box to write.</param>
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));
}
/// <summary>
/// Writes 32 bit float value to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="value">The float value to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private unsafe void WriteSingle(Stream stream, float value)
{
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value);
stream.Write(this.buffer.AsSpan(0, 4));
}
/// <summary>
/// Writes a 32 bit float value to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="value">The float value to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private static unsafe void WriteSingleToBuffer(Span<byte> buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value);
/// <summary>
/// Writes a 16 bit float value to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="value">The float value to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private static void WriteHalfSingleToBuffer(Span<byte> buffer, float value)
{
ushort valueAsShort = HalfTypeHelper.Pack(value);
BinaryPrimitives.WriteUInt16LittleEndian(buffer, valueAsShort);
}
/// <summary>
/// Writes one unsigned int to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="value">The uint value to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private static void WriteUnsignedIntToBuffer(Span<byte> buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
}

34
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the OpenExr format.
/// </summary>
public sealed class ExrFormat : IImageFormat<ExrMetadata>
{
private ExrFormat()
{
}
/// <summary>
/// Gets the current instance.
/// </summary>
public static ExrFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "EXR";
/// <inheritdoc/>
public string DefaultMimeType => "image/x-exr";
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => ExrConstants.MimeTypes;
/// <inheritdoc/>
public IEnumerable<string> FileExtensions => ExrConstants.FileExtensions;
/// <inheritdoc/>
public ExrMetadata CreateDefaultFormatMetadata() => new();
}

108
src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs

@ -0,0 +1,108 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Exr.Constants;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// The header of an EXR image.
/// <see href="https://openexr.com/en/latest/TechnicalIntroduction.html#header"/>
/// </summary>
internal class ExrHeaderAttributes
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrHeaderAttributes" /> class.
/// </summary>
/// <param name="channels">The image channels.</param>
/// <param name="compression">The compression used.</param>
/// <param name="dataWindow">The data window.</param>
/// <param name="displayWindow">The display window.</param>
/// <param name="lineOrder">The line order.</param>
/// <param name="aspectRatio">The aspect ratio.</param>
/// <param name="screenWindowWidth">Width of the screen window.</param>
/// <param name="screenWindowCenter">The screen window center.</param>
/// <param name="tileXSize">Size of the tile in x dimension.</param>
/// <param name="tileYSize">Size of the tile in y dimension.</param>
/// <param name="chunkCount">The chunk count.</param>
public ExrHeaderAttributes(
IList<ExrChannelInfo> 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;
}
/// <summary>
/// Gets or sets a description of the image channels stored in the file.
/// </summary>
public IList<ExrChannelInfo> Channels { get; set; }
/// <summary>
/// Gets or sets the compression method applied to the pixel data of all channels in the file.
/// </summary>
public ExrCompression Compression { get; set; }
/// <summary>
/// Gets or sets the image’s data window.
/// </summary>
public ExrBox2i DataWindow { get; set; }
/// <summary>
/// Gets or sets the image’s display window.
/// </summary>
public ExrBox2i DisplayWindow { get; set; }
/// <summary>
/// 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).
/// </summary>
public ExrLineOrder LineOrder { get; set; }
/// <summary>
/// Gets or sets the aspect ratio of the image.
/// </summary>
public float AspectRatio { get; set; }
/// <summary>
/// Gets or sets the screen width.
/// </summary>
public float ScreenWindowWidth { get; set; }
/// <summary>
/// Gets or sets the screen window center.
/// </summary>
public PointF ScreenWindowCenter { get; set; }
/// <summary>
/// Gets or sets the number of horizontal tiles.
/// </summary>
public uint? TileXSize { get; set; }
/// <summary>
/// Gets or sets the number of vertical tiles.
/// </summary>
public uint? TileYSize { get; set; }
/// <summary>
/// Gets or sets the chunk count. Indicates the number of chunks in this part. Required if the multipart bit (12) is set.
/// </summary>
public int? ChunkCount { get; set; }
}

34
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;
/// <summary>
/// Detects OpenExr file headers.
/// </summary>
public sealed class ExrImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize => 4;
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
{
if (header.Length >= this.HeaderSize)
{
int fileTypeMarker = BinaryPrimitives.ReadInt32LittleEndian(header);
return fileTypeMarker == ExrConstants.MagickBytes;
}
return false;
}
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
{
format = this.IsSupportedFileFormat(header) ? ExrFormat.Instance : null;
return format != null;
}
}

156
src/ImageSharp/Formats/Exr/ExrMetadata.cs

@ -0,0 +1,156 @@
// 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;
/// <summary>
/// Provides OpenExr specific metadata information for the image.
/// </summary>
public class ExrMetadata : IFormatMetadata<ExrMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrMetadata"/> class.
/// </summary>
public ExrMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExrMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private ExrMetadata(ExrMetadata other) => this.PixelType = other.PixelType;
/// <summary>
/// Gets or sets the pixel format.
/// </summary>
public ExrPixelType PixelType { get; set; } = ExrPixelType.Half;
/// <summary>
/// Gets or sets the image data type, either RGB, RGBA or gray.
/// </summary>
public ExrImageDataType ImageDataType { get; set; } = ExrImageDataType.Unknown;
/// <summary>
/// Gets or sets the compression method.
/// </summary>
public ExrCompression Compression { get; set; } = ExrCompression.None;
/// <inheritdoc/>
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
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
{
EncodingType type = this.Compression is ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Pxr24
? EncodingType.Lossy
: EncodingType.Lossless;
return new()
{
EncodingType = type,
PixelTypeInfo = this.GetPixelTypeInfo()
};
}
/// <inheritdoc/>
public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
PixelTypeInfo pixelTypeInfo = metadata.PixelTypeInfo;
PixelComponentInfo? info = pixelTypeInfo.ComponentInfo;
PixelColorType colorType = pixelTypeInfo.ColorType;
int bitsPerComponent = info?.GetMaximumComponentPrecision()
?? (pixelTypeInfo.BitsPerPixel <= 16 ? 16 : 32);
int componentCount = info?.ComponentCount ?? 0;
ExrImageDataType imageDataType = colorType switch
{
PixelColorType.Luminance => ExrImageDataType.Gray,
PixelColorType.RGB or PixelColorType.BGR => ExrImageDataType.Rgb,
PixelColorType.RGB | PixelColorType.Alpha
or PixelColorType.BGR | PixelColorType.Alpha
or PixelColorType.Luminance | PixelColorType.Alpha => ExrImageDataType.Rgba,
_ => componentCount switch
{
>= 4 => ExrImageDataType.Rgba,
>= 3 => ExrImageDataType.Rgb,
1 => ExrImageDataType.Gray,
_ => ExrImageDataType.Unknown,
}
};
return new()
{
PixelType = bitsPerComponent <= 16 ? ExrPixelType.Half : ExrPixelType.Float,
ImageDataType = imageDataType,
};
}
/// <inheritdoc/>
ExrMetadata IDeepCloneable<ExrMetadata>.DeepClone() => new(this);
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new ExrMetadata(this);
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}
}

33
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;
/// <summary>
/// Cold path optimizations for throwing exr format based exceptions.
/// </summary>
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}");
}

52
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
{
/// <summary>
/// Calcualtes the required bytes for a pixel row.
/// </summary>
/// <param name="channels">The image channels array.</param>
/// <param name="width">The width in pixels of a row.</param>
/// <returns>The number of bytes per row.</returns>
public static uint CalculateBytesPerRow(IList<ExrChannelInfo> 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;
}
/// <summary>
/// Determines how many pixel rows there are in a block. This varies depending on the compression used.
/// </summary>
/// <param name="compression">The compression used.</param>
/// <returns>Pixel rows in a block.</returns>
public static uint RowsPerBlock(ExrCompression compression) => compression switch
{
ExrCompression.Zip or ExrCompression.Pxr24 => 16,
ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Piz => 32,
_ => 1,
};
}

4
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)

103
src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs

@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Formats.Exr;
namespace SixLabors.ImageSharp; namespace SixLabors.ImageSharp;
@ -1143,4 +1144,106 @@ public static partial class ImageExtensions
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance),
cancellationToken); cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Exr format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsExr(this Image source, string path) => SaveAsExr(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Exr format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsExrAsync(this Image source, string path) => SaveAsExrAsync(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Exr format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsExrAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsExrAsync(source, path, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Exr format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsExr(this Image source, string path, ExrEncoder encoder) =>
source.Save(
path,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Exr format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Exr format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsExr(this Image source, Stream stream)
=> SaveAsExr(source, stream, default);
/// <summary>
/// Saves the image to the given stream with the Exr format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsExrAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsExrAsync(source, stream, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Exr format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsExr(this Image source, Stream stream, ExrEncoder encoder)
=> source.Save(
stream,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Exr format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
} }

21
src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs

@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Formats.Exr;
namespace SixLabors.ImageSharp; namespace SixLabors.ImageSharp;
@ -242,6 +243,26 @@ public static class ImageMetadataExtensions
/// <returns>The new <see cref="WebpMetadata"/></returns> /// <returns>The new <see cref="WebpMetadata"/></returns>
public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance); public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance);
/// <summary>
/// Gets the <see cref="ExrMetadata"/> from <paramref name="source"/>.<br/>
/// 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.
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>
/// The <see cref="ExrMetadata"/>
/// </returns>
public static ExrMetadata GetExrMetadata(this ImageMetadata source) => source.GetFormatMetadata(ExrFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="ExrMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetExrMetadata(ImageMetadata)"/>
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>The new <see cref="ExrMetadata"/></returns>
public static ExrMetadata CloneExrMetadata(this ImageMetadata source) => source.CloneFormatMetadata(ExrFormat.Instance);
/// <summary> /// <summary>
/// Gets the <see cref="CurFrameMetadata"/> from <paramref name="source"/>.<br/> /// Gets the <see cref="CurFrameMetadata"/> from <paramref name="source"/>.<br/>

3
src/ImageSharp/Formats/_Generated/_Formats.ttinclude

@ -14,7 +14,8 @@
"Qoi", "Qoi",
"Tga", "Tga",
"Tiff", "Tiff",
"Webp" "Webp",
"Exr"
]; ];
private static readonly string[] frameFormats = [ private static readonly string[] frameFormats = [

10
src/ImageSharp/ImageSharp.csproj

@ -56,6 +56,11 @@
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DependentUpon>InlineArray.tt</DependentUpon> <DependentUpon>InlineArray.tt</DependentUpon>
</Compile> </Compile>
<Compile Update="Formats\_Generated\ImageExtensions.Save.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ImageExtensions.Save.tt</DependentUpon>
</Compile>
<Compile Update="Formats\_Generated\ImageMetadataExtensions.cs"> <Compile Update="Formats\_Generated\ImageMetadataExtensions.cs">
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
@ -141,11 +146,6 @@
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DependentUpon>PorterDuffFunctions.Generated.tt</DependentUpon> <DependentUpon>PorterDuffFunctions.Generated.tt</DependentUpon>
</Compile> </Compile>
<Compile Update="Formats\_Generated\ImageExtensions.Save.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>ImageExtensions.Save.tt</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

203
src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs

@ -0,0 +1,203 @@
// 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;
/// <summary>
/// Pixel type containing three 32-bit unsigned normalized values ranging from 0 to 4294967295.
/// The color components are stored in red, green, blue.
/// <para>
/// Ranges from [0, 0, 0] to [1, 1, 1] in vector form.
/// </para>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public partial struct Rgb96 : IPixel<Rgb96>, IEquatable<Rgb96>
{
private const float InvMax = 1.0f / uint.MaxValue;
// Use double here because at this magnitude a float cannot represent all 32-bit
// integer values exactly. A float only has 24 bits of precision, so around
// uint.MaxValue it can only represent multiples of 256 and will round
// 4294967295 up to 4294967296. Double has 53 bits of precision and can
// represent all uint values exactly, avoiding precision loss before scaling.
private const double Max = uint.MaxValue;
/// <summary>
/// Gets the red component.
/// </summary>
public uint R;
/// <summary>
/// Gets the green component.
/// </summary>
public uint G;
/// <summary>
/// Gets the blue component.
/// </summary>
public uint B;
/// <summary>
/// Initializes a new instance of the <see cref="Rgb96"/> struct.
/// </summary>
/// <param name="r">The red component.</param>
/// <param name="g">The green component.</param>
/// <param name="b">The blue component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Rgb96(uint r, uint g, uint b)
{
this.R = r;
this.G = g;
this.B = b;
}
/// <summary>
/// Compares two <see cref="Rgb96"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Rgb96"/> on the left side of the operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
/// <param name="right">The <see cref="Rgb96"/> on the right side of the operand.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Rgb96 left, Rgb96 right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Rgb96"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Rgb96"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Rgb96"/> on the right side of the operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is not equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Rgb96 left, Rgb96 right) => !left.Equals(right);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToScaledVector4() => this.ToVector4();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToVector4() => new(
this.R * InvMax,
this.G * InvMax,
this.B * InvMax,
1.0f);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PixelOperations<Rgb96> CreatePixelOperations() => new();
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromScaledVector4(Vector4 source) => FromVector4(source);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromVector4(Vector4 source)
{
source = Numerics.Clamp(source, Vector4.Zero, Vector4.One);
return new Rgb96(
(uint)Math.Round(source.X * Max),
(uint)Math.Round(source.Y * Max),
(uint)Math.Round(source.Z * Max));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromAbgr32(Abgr32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromArgb32(Argb32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromBgr24(Bgr24 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromBgra32(Bgra32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromL8(L8 source)
{
uint rgb = ColorNumerics.From8BitTo32Bit(source.PackedValue);
return new Rgb96(rgb, rgb, rgb);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromL16(L16 source)
{
uint rgb = ColorNumerics.From16BitTo32Bit(source.PackedValue);
return new(rgb, rgb, rgb);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromLa16(La16 source)
{
uint rgb = ColorNumerics.From8BitTo32Bit((byte)source.PackedValue);
return new(rgb, rgb, rgb);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromLa32(La32 source)
{
uint rgb = ColorNumerics.From16BitTo32Bit(source.L);
return new(rgb, rgb, rgb);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromRgb24(Rgb24 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromRgba32(Rgba32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromRgb48(Rgb48 source) => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb96 FromRgba64(Rgba64 source) => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create<Rgb96>(
PixelComponentInfo.Create<Rgb96>(3, 32, 32, 32),
PixelColorType.RGB,
PixelAlphaRepresentation.None);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Rgba32 ToRgba32() => Rgba32.FromRgb96(this);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B);
/// <inheritdoc />
public override readonly string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})");
/// <inheritdoc/>
public override readonly bool Equals(object? obj) => obj is Rgb96 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B;
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B);
}

200
src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs

@ -0,0 +1,200 @@
// 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;
/// <summary>
/// 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.
/// <para>
/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form.
/// </para>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public partial struct Rgba128 : IPixel<Rgba128>, IEquatable<Rgba128>
{
private const float InvMax = 1.0f / uint.MaxValue;
// Use double here because at this magnitude a float cannot represent all 32-bit
// integer values exactly. A float only has 24 bits of precision, so around
// uint.MaxValue it can only represent multiples of 256 and will round
// 4294967295 up to 4294967296. Double has 53 bits of precision and can
// represent all uint values exactly, avoiding precision loss before scaling.
private const double Max = uint.MaxValue;
/// <summary>
/// Gets the red component.
/// </summary>
public uint R;
/// <summary>
/// Gets the green component.
/// </summary>
public uint G;
/// <summary>
/// Gets the blue component.
/// </summary>
public uint B;
/// <summary>
/// Gets the alpha channel.
/// </summary>
public uint A;
/// <summary>
/// Initializes a new instance of the <see cref="Rgba128"/> struct.
/// </summary>
/// <param name="r">The red component.</param>
/// <param name="g">The green component.</param>
/// <param name="b">The blue component.</param>
/// <param name="a">The alpha component.</param>
[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;
}
/// <summary>
/// Compares two <see cref="Rgba128"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Rgba128"/> on the left side of the operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
/// <param name="right">The <see cref="Rgba128"/> on the right side of the operand.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Rgba128 left, Rgba128 right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Rgba128"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Rgba128"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Rgba128"/> on the right side of the operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is not equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Rgba128 left, Rgba128 right) => !left.Equals(right);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector4 ToVector4() => new(
this.R * InvMax,
this.G * InvMax,
this.B * InvMax,
this.A * InvMax);
/// <inheritdoc/>
public static PixelOperations<Rgba128> CreatePixelOperations() => new();
/// <inheritdoc/>
public static Rgba128 FromScaledVector4(Vector4 source) => FromVector4(source);
/// <inheritdoc/>
public static Rgba128 FromVector4(Vector4 source)
{
source = Numerics.Clamp(source, Vector4.Zero, Vector4.One);
return new Rgba128(
(uint)Math.Round(source.X * Max),
(uint)Math.Round(source.Y * Max),
(uint)Math.Round(source.Z * Max),
(uint)Math.Round(source.W * Max));
}
/// <inheritdoc/>
public static Rgba128 FromAbgr32(Abgr32 source)
=> new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A));
/// <inheritdoc/>
public static Rgba128 FromArgb32(Argb32 source)
=> new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A));
/// <inheritdoc/>
public static Rgba128 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc/>
public static Rgba128 FromBgr24(Bgr24 source)
=> new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), uint.MaxValue);
/// <inheritdoc/>
public static Rgba128 FromBgra32(Bgra32 source)
=> new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A));
/// <inheritdoc/>
public static Rgba128 FromL8(L8 source)
{
uint rgb = ColorNumerics.From8BitTo32Bit(source.PackedValue);
return new Rgba128(rgb, rgb, rgb, rgb);
}
/// <inheritdoc/>
public static Rgba128 FromL16(L16 source)
{
uint rgb = ColorNumerics.From16BitTo32Bit(source.PackedValue);
return new(rgb, rgb, rgb, rgb);
}
/// <inheritdoc/>
public static Rgba128 FromLa16(La16 source)
{
uint rgb = ColorNumerics.From8BitTo32Bit((byte)source.PackedValue);
return new(rgb, rgb, rgb, rgb);
}
/// <inheritdoc/>
public static Rgba128 FromLa32(La32 source)
{
uint rgb = ColorNumerics.From16BitTo32Bit(source.L);
return new(rgb, rgb, rgb, rgb);
}
/// <inheritdoc/>
public static Rgba128 FromRgb24(Rgb24 source)
=> new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), uint.MaxValue);
/// <inheritdoc/>
public static Rgba128 FromRgba32(Rgba32 source)
=> new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A));
/// <inheritdoc/>
public static Rgba128 FromRgb48(Rgb48 source)
=> new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B), uint.MaxValue);
/// <inheritdoc/>
public static Rgba128 FromRgba64(Rgba64 source)
=> new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B), ColorNumerics.From16BitTo32Bit(source.A));
/// <inheritdoc/>
public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create<Rgba128>(
PixelComponentInfo.Create<Rgba128>(4, 32, 32, 32, 32),
PixelColorType.RGB | PixelColorType.Alpha,
PixelAlphaRepresentation.Unassociated);
/// <inheritdoc/>
public readonly Rgba32 ToRgba32() => Rgba32.FromRgba128(this);
/// <inheritdoc/>
public readonly Vector4 ToScaledVector4() => this.ToVector4();
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A);
/// <inheritdoc />
public override readonly string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})");
/// <inheritdoc/>
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;
/// <inheritdoc />
[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);
}

30
src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs

@ -314,6 +314,36 @@ public partial struct Rgba32 : IPixel<Rgba32>, IPackedVector<uint>
A = ColorNumerics.From16BitTo8Bit(source.A) A = ColorNumerics.From16BitTo8Bit(source.A)
}; };
/// <summary>
/// Initializes the pixel instance from an <see cref="Rgb96"/> value.
/// </summary>
/// <param name="source">The <see cref="Rgb96"/> value.</param>
/// <returns>The pixel value as Rgba32.</returns>
[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
};
/// <summary>
/// Initializes the pixel instance from an <see cref="Rgba128"/> value.
/// </summary>
/// <param name="source">The <see cref="Rgba128"/> value.</param>
/// <returns>The pixel value as Rgba32.</returns>
[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),
};
/// <summary> /// <summary>
/// Converts the value of this instance to a hexadecimal string. /// Converts the value of this instance to a hexadecimal string.
/// </summary> /// </summary>

2
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. 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 See https://github.com/ImageMagick/ImageMagick/commit/27a0a9c37f18af9c8d823a3ea076f600843b553c
--> -->
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="13.10.0" /> <PackageReference Update="Magick.NET-Q16-AnyCPU" Version="14.11.1" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="10.0.0-beta.25563.105" /> <PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="10.0.0-beta.25563.105" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" /> <PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" />
<PackageReference Update="Moq" Version="4.20.72" /> <PackageReference Update="Moq" Version="4.20.72" />

68
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<Rgba32> image = Image.Load<Rgba32>(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 |
*/
}

2
tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs

@ -27,7 +27,7 @@ public class DecodeTga
=> this.data = File.ReadAllBytes(this.TestImageFullPath); => this.data = File.ReadAllBytes(this.TestImageFullPath);
[Benchmark(Baseline = true, Description = "ImageMagick Tga")] [Benchmark(Baseline = true, Description = "ImageMagick Tga")]
public int TgaImageMagick() public uint TgaImageMagick()
{ {
MagickReadSettings settings = new() { Format = MagickFormat.Tga }; MagickReadSettings settings = new() { Format = MagickFormat.Tga };
using MagickImage image = new(new MemoryStream(this.data), settings); using MagickImage image = new(new MemoryStream(this.data), settings);

4
tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs

@ -42,7 +42,7 @@ public class DecodeWebp
} }
[Benchmark(Description = "Magick Lossy Webp")] [Benchmark(Description = "Magick Lossy Webp")]
public int WebpLossyMagick() public uint WebpLossyMagick()
{ {
MagickReadSettings settings = new() { Format = MagickFormat.WebP }; MagickReadSettings settings = new() { Format = MagickFormat.WebP };
using MemoryStream memoryStream = new(this.webpLossyBytes); using MemoryStream memoryStream = new(this.webpLossyBytes);
@ -59,7 +59,7 @@ public class DecodeWebp
} }
[Benchmark(Description = "Magick Lossless Webp")] [Benchmark(Description = "Magick Lossless Webp")]
public int WebpLosslessMagick() public uint WebpLosslessMagick()
{ {
MagickReadSettings settings = new() MagickReadSettings settings = new()
{ Format = MagickFormat.WebP }; { Format = MagickFormat.WebP };

16
tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs

@ -6,6 +6,7 @@ using System.Drawing.Drawing2D;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using ImageMagick; using ImageMagick;
using PhotoSauce.MagicScaler; using PhotoSauce.MagicScaler;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
@ -27,6 +28,7 @@ public enum JpegKind
Any = Baseline | Progressive Any = Baseline | Progressive
} }
[SupportedOSPlatform("windows")]
public class LoadResizeSaveStressRunner public class LoadResizeSaveStressRunner
{ {
private const int Quality = 75; private const int Quality = 75;
@ -158,7 +160,7 @@ public class LoadResizeSaveStressRunner
this.outputDirectory, this.outputDirectory,
Path.GetFileNameWithoutExtension(inputPath) + "-" + postfix + Path.GetExtension(inputPath)); Path.GetFileNameWithoutExtension(inputPath) + "-" + postfix + Path.GetExtension(inputPath));
private (int Width, int Height) ScaledSize(int inWidth, int inHeight, int outSize) private static (int Width, int Height) ScaledSize(int inWidth, int inHeight, int outSize)
{ {
int width, height; int width, height;
if (inWidth > inHeight) if (inWidth > inHeight)
@ -180,7 +182,7 @@ public class LoadResizeSaveStressRunner
using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true); using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true);
this.LogImageProcessed(image.Width, image.Height); this.LogImageProcessed(image.Width, image.Height);
(int width, int height) = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize); (int width, int height) = ScaledSize(image.Width, image.Height, this.ThumbnailSize);
Bitmap resized = new(width, height); Bitmap resized = new(width, height);
using Graphics graphics = Graphics.FromImage(resized); using Graphics graphics = Graphics.FromImage(resized);
using ImageAttributes attributes = new(); using ImageAttributes attributes = new();
@ -248,10 +250,10 @@ public class LoadResizeSaveStressRunner
public void MagickResize(string input) public void MagickResize(string input)
{ {
using MagickImage image = new(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 // 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 // Reduce the size of the file
image.Strip(); image.Strip();
@ -282,7 +284,7 @@ public class LoadResizeSaveStressRunner
{ {
using SKBitmap original = SKBitmap.Decode(input); using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height); this.LogImageProcessed(original.Width, original.Height);
(int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); (int width, int height) = ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, original.ColorType, original.AlphaType)); using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, original.ColorType, original.AlphaType));
using SKPaint paint = new() { FilterQuality = SKFilterQuality.High }; using SKPaint paint = new() { FilterQuality = SKFilterQuality.High };
SKCanvas canvas = surface.Canvas; SKCanvas canvas = surface.Canvas;
@ -300,7 +302,7 @@ public class LoadResizeSaveStressRunner
{ {
using SKBitmap original = SKBitmap.Decode(input); using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height); this.LogImageProcessed(original.Width, original.Height);
(int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); (int width, int height) = ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High); using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High);
if (resized == null) if (resized == null)
{ {
@ -319,7 +321,7 @@ public class LoadResizeSaveStressRunner
SKImageInfo info = codec.Info; SKImageInfo info = codec.Info;
this.LogImageProcessed(info.Width, info.Height); this.LogImageProcessed(info.Width, info.Height);
(int width, int height) = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize); (int width, int height) = ScaledSize(info.Width, info.Height, this.ThumbnailSize);
SKSizeI supportedScale = codec.GetScaledDimensions((float)width / info.Width); SKSizeI supportedScale = codec.GetScaledDimensions((float)width / info.Width);
using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height)); using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height));

65
tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs

@ -0,0 +1,65 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests;
namespace SixLabors.ImageSharp.Benchmarks;
public class ParallelProcessing
{
private Image<Rgba32> image;
private Image<Rgba32> foreground;
private Configuration configuration;
public static IEnumerable<int> MaxDegreeOfParallelismValues()
{
int processorCount = Environment.ProcessorCount;
for (int p = 1; p <= processorCount; p *= 2)
{
yield return p;
}
if ((processorCount & (processorCount - 1)) != 0)
{
yield return processorCount;
}
}
[ParamsSource(nameof(MaxDegreeOfParallelismValues))]
public int MaxDegreeOfParallelism { get; set; }
[GlobalSetup]
public void Setup()
{
this.image = new Image<Rgba32>(2048, 2048);
this.foreground = new Image<Rgba32>(2048, 2048);
this.configuration = Configuration.Default.Clone();
this.configuration.MaxDegreeOfParallelism = this.MaxDegreeOfParallelism;
}
[Benchmark]
public void DetectEdges() => this.image.Mutate(this.configuration, x => x.DetectEdges());
[Benchmark]
public void DrawImage() => this.image.Mutate(this.configuration, x => x.DrawImage(this.foreground, 0.5f));
[Benchmark]
public void Crop()
{
Rectangle bounds = this.image.Bounds;
bounds = new Rectangle(1, 1, bounds.Width - 2, bounds.Height - 2);
this.image
.Clone(this.configuration, x => x.Crop(bounds))
.Dispose();
}
[GlobalCleanup]
public void Cleanup()
{
this.image.Dispose();
this.foreground.Dispose();
}
}

1
tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj

@ -8,7 +8,6 @@
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
<RootNamespace>SixLabors.ImageSharp.Tests.ProfilingSandbox</RootNamespace> <RootNamespace>SixLabors.ImageSharp.Tests.ProfilingSandbox</RootNamespace>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<StartupObject>SixLabors.ImageSharp.Tests.ProfilingSandbox.Program</StartupObject>
<!--Used to hide test project from dotnet test--> <!--Used to hide test project from dotnet test-->
<IsTestProject>false</IsTestProject> <IsTestProject>false</IsTestProject>
<EnsureNETCoreAppRuntime>false</EnsureNETCoreAppRuntime> <EnsureNETCoreAppRuntime>false</EnsureNETCoreAppRuntime>

13
tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs

@ -3,6 +3,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Runtime.Versioning;
using System.Text; using System.Text;
using CommandLine; using CommandLine;
using CommandLine.Text; using CommandLine.Text;
@ -13,7 +14,8 @@ using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox; namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
// See ImageSharp.Benchmarks/LoadResizeSave/README.md // See ImageSharp.Benchmarks/LoadResizeSave/README.md
internal class LoadResizeSaveParallelMemoryStress [SupportedOSPlatform("windows")]
internal sealed class LoadResizeSaveParallelMemoryStress
{ {
private LoadResizeSaveParallelMemoryStress() private LoadResizeSaveParallelMemoryStress()
{ {
@ -206,14 +208,15 @@ internal class LoadResizeSaveParallelMemoryStress
StringBuilder bld = new(); StringBuilder bld = new();
bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |"); bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |");
bld.AppendLine( bld.AppendLine(
CultureInfo.InvariantCulture,
$"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |"); $"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |");
bld.Append("| "); bld.Append("| ");
bld.AppendFormat(F(nameof(this.TotalSeconds)), this.TotalSeconds); bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.TotalSeconds)), this.TotalSeconds);
bld.Append(" | "); bld.Append(" | ");
bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec); bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec);
bld.Append(" | "); bld.Append(" | ");
bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu); bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu);
bld.AppendLine(" |"); bld.AppendLine(" |");
return bld.ToString(); return bld.ToString();
@ -223,7 +226,7 @@ internal class LoadResizeSaveParallelMemoryStress
} }
} }
private class CommandLineOptions private sealed class CommandLineOptions
{ {
[Option('a', "async-imagesharp", Required = false, Default = false, HelpText = "Async ImageSharp without benchmark switching")] [Option('a', "async-imagesharp", Required = false, Default = false, HelpText = "Async ImageSharp without benchmark switching")]
public bool AsyncImageSharp { get; set; } public bool AsyncImageSharp { get; set; }

262
tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputBenchmark.cs

@ -0,0 +1,262 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using CommandLine;
using CommandLine.Text;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Convolution;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox;
public sealed class ProcessorThroughputBenchmark
{
private readonly CommandLineOptions options;
private readonly Configuration configuration;
private ulong totalProcessedPixels;
private ProcessorThroughputBenchmark(CommandLineOptions options)
{
this.options = options;
this.configuration = Configuration.Default.Clone();
this.configuration.MaxDegreeOfParallelism = options.ProcessorParallelism > 0
? options.ProcessorParallelism
: Environment.ProcessorCount;
}
public static Task RunAsync(string[] args)
{
CommandLineOptions options = null;
if (args.Length > 0)
{
options = CommandLineOptions.Parse(args);
if (options == null)
{
return Task.CompletedTask;
}
}
options ??= new CommandLineOptions();
return new ProcessorThroughputBenchmark(options.Normalize())
.RunAsync();
}
private async Task RunAsync()
{
SemaphoreSlim semaphore = new(this.options.ConcurrentRequests);
Console.WriteLine(this.options.Method);
Func<int> action = this.options.Method switch
{
Method.Crop => this.Crop,
Method.Edges => this.DetectEdges,
Method.EdgesCompass => this.EdgesCompass,
Method.DrawImage => this.DrawImage,
Method.BinaryThreshold => this.BinaryThreshold,
Method.Histogram => this.Histogram,
Method.OilPaint => this.OilPaint,
Method.GaussianBlur => this.GaussianBlur,
_ => throw new NotImplementedException(),
};
Console.WriteLine(this.options);
Console.WriteLine($"Running {this.options.Method} for {this.options.Seconds} seconds ...");
TimeSpan runFor = TimeSpan.FromSeconds(this.options.Seconds);
// inFlight starts at 1 to represent the dispatch loop itself
int inFlight = 1;
TaskCompletionSource drainTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
Stopwatch stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed < runFor && !drainTcs.Task.IsCompleted)
{
await semaphore.WaitAsync();
if (stopwatch.Elapsed >= runFor)
{
semaphore.Release();
break;
}
Interlocked.Increment(ref inFlight);
_ = ProcessImage();
async Task ProcessImage()
{
try
{
if (stopwatch.Elapsed >= runFor || drainTcs.Task.IsCompleted)
{
return;
}
await Task.Yield(); // "emulate IO", i.e., make sure the processing code is async
ulong pixels = (ulong)action();
Interlocked.Add(ref this.totalProcessedPixels, pixels);
}
catch (Exception ex)
{
Console.WriteLine(ex);
drainTcs.TrySetException(ex);
}
finally
{
semaphore.Release();
if (Interlocked.Decrement(ref inFlight) == 0)
{
drainTcs.TrySetResult();
}
}
}
}
// Release the dispatch loop's own count; if no work is in flight, this completes immediately
if (Interlocked.Decrement(ref inFlight) == 0)
{
drainTcs.TrySetResult();
}
await drainTcs.Task;
stopwatch.Stop();
double totalMegaPixels = this.totalProcessedPixels / 1_000_000.0;
double totalSeconds = stopwatch.ElapsedMilliseconds / 1000.0;
double megapixelsPerSec = totalMegaPixels / totalSeconds;
Console.WriteLine($"TotalSeconds: {totalSeconds:F2}");
Console.WriteLine($"MegaPixelsPerSec: {megapixelsPerSec:F2}");
}
private int OilPaint()
{
using Image<Rgba32> image = new(this.options.Width, this.options.Height);
image.Mutate(this.configuration, x => x.OilPaint());
return image.Width * image.Height;
}
private int DetectEdges()
{
using Image<Rgba32> image = new(this.options.Width, this.options.Height);
image.Mutate(this.configuration, x => x.DetectEdges());
return image.Width * image.Height;
}
private int EdgesCompass()
{
using Image<Rgba32> image = new(this.options.Width, this.options.Height);
image.Mutate(this.configuration, x => x.DetectEdges(EdgeDetectorCompassKernel.Kirsch));
return image.Width * image.Height;
}
private int Crop()
{
using Image<Rgba32> image = new(this.options.Width, this.options.Height);
Rectangle bounds = image.Bounds;
bounds = new Rectangle(1, 1, bounds.Width - 2, bounds.Height - 2);
image.Clone(this.configuration, x => x.Crop(bounds)).Dispose();
return image.Width * image.Height;
}
private int DrawImage()
{
using Image<Rgba32> image = new(this.options.Width, this.options.Height);
using Image<Rgba32> foreground = new(this.options.Width, this.options.Height);
image.Mutate(this.configuration, c => c.DrawImage(foreground, 0.5f));
return image.Width * image.Height;
}
private int BinaryThreshold()
{
using Image<Rgba32> image = new(this.options.Width, this.options.Height);
image.Mutate(this.configuration, c => c.BinaryThreshold(0.5f));
return image.Width * image.Height;
}
private int Histogram()
{
using Image<Rgba32> image = new(this.options.Width, this.options.Height);
image.Mutate(this.configuration, c => c.HistogramEqualization());
return image.Width * image.Height;
}
private int GaussianBlur()
{
using Image<Rgba32> image = new(this.options.Width, this.options.Height);
image.Mutate(this.configuration, c => c.GaussianBlur());
return image.Width * image.Height;
}
private enum Method
{
Edges,
EdgesCompass,
Crop,
DrawImage,
BinaryThreshold,
Histogram,
OilPaint,
GaussianBlur,
}
private sealed class CommandLineOptions
{
[Option('m', "method", Required = false, Default = Method.Edges, HelpText = "The stress test method to run (Edges, Crop)")]
public Method Method { get; set; } = Method.Edges;
[Option('p', "processor-parallelism", Required = false, Default = -1, HelpText = "Level of parallelism for the image processor")]
public int ProcessorParallelism { get; set; } = -1;
[Option('c', "concurrent-requests", Required = false, Default = -1, HelpText = "Number of concurrent in-flight requests")]
public int ConcurrentRequests { get; set; } = -1;
[Option('w', "width", Required = false, Default = 4000, HelpText = "Width of the test image")]
public int Width { get; set; } = 4000;
[Option('h', "height", Required = false, Default = 4000, HelpText = "Height of the test image")]
public int Height { get; set; } = 4000;
[Option('s', "seconds", Required = false, Default = 5, HelpText = "Duration of the stress test in seconds")]
public int Seconds { get; set; } = 5;
public override string ToString() => string.Join(
"|",
$"method: {this.Method}",
$"processor-parallelism: {this.ProcessorParallelism}",
$"concurrent-requests: {this.ConcurrentRequests}",
$"width: {this.Width}",
$"height: {this.Height}",
$"seconds: {this.Seconds}");
public CommandLineOptions Normalize()
{
if (this.ProcessorParallelism < 0)
{
this.ProcessorParallelism = Environment.ProcessorCount;
}
if (this.ConcurrentRequests < 0)
{
this.ConcurrentRequests = Environment.ProcessorCount;
}
return this;
}
public static CommandLineOptions Parse(string[] args)
{
CommandLineOptions result = null;
using Parser parser = new(settings => settings.CaseInsensitiveEnumValues = true);
ParserResult<CommandLineOptions> parserResult = parser.ParseArguments<CommandLineOptions>(args).WithParsed(o =>
{
result = o;
});
if (result == null)
{
Console.WriteLine(HelpText.RenderUsageText(parserResult));
}
return result;
}
}
}

80
tests/ImageSharp.Tests.ProfilingSandbox/Program.cs

@ -1,76 +1,38 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Reflection;
using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations;
using SixLabors.ImageSharp.Tests.ProfilingBenchmarks; using SixLabors.ImageSharp.Tests.ProfilingBenchmarks;
using SixLabors.ImageSharp.Tests.ProfilingSandbox;
using Xunit.Abstractions; using Xunit.Abstractions;
// in this file, comments are used for disabling stuff for local execution // in this file, comments are used for disabling stuff for local execution
#pragma warning disable SA1515 #pragma warning disable SA1515
#pragma warning disable SA1512 #pragma warning disable SA1512
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox; // LoadResizeSaveParallelMemoryStress.Run(args);
// ParallelProcessingStress.RunExperiment(args);
// ParallelProcessingStress.Run(args);
await ProcessorThroughputBenchmark.RunAsync(args);
public class Program // RunToVector4ProfilingTest();
{ // RunResizeProfilingTest();
private class ConsoleOutput : ITestOutputHelper
{
public void WriteLine(string message) => Console.WriteLine(message);
public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args);
}
/// <summary>
/// The main entry point. Useful for executing benchmarks and performance unit tests manually,
/// when the IDE test runners lack some of the functionality. Eg.: it's not possible to run JetBrains memory profiler for unit tests.
/// </summary>
/// <param name="args">
/// The arguments to pass to the program.
/// </param>
public static void Main(string[] args)
{
try
{
LoadResizeSaveParallelMemoryStress.Run(args);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
// RunJpegEncoderProfilingTests();
// RunJpegColorProfilingTests();
// RunDecodeJpegProfilingTests();
// RunToVector4ProfilingTest();
// RunResizeProfilingTest();
// Console.ReadLine();
}
private static Version GetNetCoreVersion() static void RunResizeProfilingTest()
{ {
Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; ResizeProfilingBenchmarks test = new(new ConsoleOutput());
Console.WriteLine(assembly.Location); test.ResizeBicubic(4000, 4000);
string[] assemblyPath = assembly.Location.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries); }
int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
{
return Version.Parse(assemblyPath[netCoreAppIndex + 1]);
}
return null; static void RunToVector4ProfilingTest()
} {
PixelOperationsTests.Rgba32_OperationsTests tests = new(new ConsoleOutput());
tests.Benchmark_ToVector4();
}
private static void RunResizeProfilingTest() sealed class ConsoleOutput : ITestOutputHelper
{ {
ResizeProfilingBenchmarks test = new(new ConsoleOutput()); public void WriteLine(string message) => Console.WriteLine(message);
test.ResizeBicubic(4000, 4000);
}
private static void RunToVector4ProfilingTest() public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args);
{
PixelOperationsTests.Rgba32_OperationsTests tests = new(new ConsoleOutput());
tests.Benchmark_ToVector4();
}
} }

2
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -20,7 +20,7 @@ public class ConfigurationTests
public Configuration DefaultConfiguration { get; } public Configuration DefaultConfiguration { get; }
private readonly int expectedDefaultConfigurationCount = 11; private readonly int expectedDefaultConfigurationCount = 12;
public ConfigurationTests() public ConfigurationTests()
{ {

54
tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs

@ -91,13 +91,22 @@ public class BmpDecoderTests
{ {
using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance); using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance);
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToOriginal(provider); image.CompareToReferenceOutput(provider);
} }
[Theory] [Theory]
[WithFile(Bit16Inverted, PixelTypes.Rgba32)] [WithFile(Bit16Inverted, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_16Bit_Inverted<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance);
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
}
[Theory]
[WithFile(Bit8Inverted, PixelTypes.Rgba32)] [WithFile(Bit8Inverted, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_Inverted<TPixel>(TestImageProvider<TPixel> provider) public void BmpDecoder_CanDecode_8Bit_Inverted<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance); using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance);
@ -156,7 +165,7 @@ public class BmpDecoderTests
{ {
using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance); using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance);
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToOriginal(provider); image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder(BmpFormat.Instance));
} }
[Theory] [Theory]
@ -186,12 +195,12 @@ public class BmpDecoderTests
public void BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta<TPixel>(TestImageProvider<TPixel> provider) public void BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
RleSkippedPixelHandling skippedPixelHandling = TestEnvironment.IsWindows ? RleSkippedPixelHandling.Black : RleSkippedPixelHandling.FirstColorOfPalette; RleSkippedPixelHandling skippedPixelHandling = RleSkippedPixelHandling.Black;
BmpDecoderOptions options = new() { RleSkippedPixelHandling = skippedPixelHandling }; BmpDecoderOptions options = new() { RleSkippedPixelHandling = skippedPixelHandling };
using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance, options); using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance, options);
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToOriginal(provider); image.CompareToReferenceOutput(provider);
} }
[Theory] [Theory]
@ -224,8 +233,8 @@ public class BmpDecoderTests
} }
} }
// An RLE-compressed image that uses “delta” codes, to skip over some pixels.
[Theory] [Theory]
[WithFile(RLE8Cut, PixelTypes.Rgba32)]
[WithFile(RLE8Delta, PixelTypes.Rgba32)] [WithFile(RLE8Delta, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_MagickRefDecoder<TPixel>(TestImageProvider<TPixel> provider) public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_MagickRefDecoder<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -236,11 +245,21 @@ public class BmpDecoderTests
image.CompareToOriginal(provider, MagickReferenceDecoder.Png); 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette };
using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance, options);
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
}
[Theory] [Theory]
[WithFile(RLE8, PixelTypes.Rgba32, false)] [WithFile(RLE8, PixelTypes.Rgba32, false)]
[WithFile(RLE8Inverted, PixelTypes.Rgba32, false)]
[WithFile(RLE8, PixelTypes.Rgba32, true)] [WithFile(RLE8, PixelTypes.Rgba32, true)]
[WithFile(RLE8Inverted, PixelTypes.Rgba32, true)]
public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit<TPixel>(TestImageProvider<TPixel> provider, bool enforceDiscontiguousBuffers) public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit<TPixel>(TestImageProvider<TPixel> provider, bool enforceDiscontiguousBuffers)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -255,6 +274,25 @@ public class BmpDecoderTests
image.CompareToOriginal(provider, MagickReferenceDecoder.Png); image.CompareToOriginal(provider, MagickReferenceDecoder.Png);
} }
[Theory]
[WithFile(RLE8Inverted, PixelTypes.Rgba32, false)]
[WithFile(RLE8Inverted, PixelTypes.Rgba32, true)]
public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted<TPixel>(TestImageProvider<TPixel> provider, bool enforceDiscontiguousBuffers)
where TPixel : unmanaged, IPixel<TPixel>
{
if (enforceDiscontiguousBuffers)
{
provider.LimitAllocatorBufferCapacity().InBytesSqrt(400);
}
BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette };
using Image<TPixel> 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] [Theory]
[WithFile(RLE24, PixelTypes.Rgba32, false)] [WithFile(RLE24, PixelTypes.Rgba32, false)]
[WithFile(RLE24Cut, PixelTypes.Rgba32, false)] [WithFile(RLE24Cut, PixelTypes.Rgba32, false)]

133
tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs

@ -0,0 +1,133 @@
// 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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.UintRgba, PixelTypes.Rgba32)]
public void ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Uint<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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);
}
}

80
tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs

@ -0,0 +1,80 @@
// 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]
[InlineData(null, ExrPixelType.Half)]
[InlineData(ExrPixelType.Float, ExrPixelType.Float)]
[InlineData(ExrPixelType.Half, ExrPixelType.Half)]
[InlineData(ExrPixelType.UnsignedInt, ExrPixelType.UnsignedInt)]
public void EncoderOptions_SetPixelType_Works(ExrPixelType? pixelType, ExrPixelType? expectedPixelType)
{
// arrange
ExrEncoder exrEncoder = new() { PixelType = pixelType };
using Image input = new Image<Rgb24>(10, 10);
using MemoryStream memStream = new();
// act
input.Save(memStream, exrEncoder);
// assert
memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ExrMetadata exrMetaData = output.Metadata.GetExrMetadata();
Assert.Equal(expectedPixelType, exrMetaData.PixelType);
}
[Theory]
[WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)]
public void ExrEncoder_WithNoCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestExrEncoderCore(provider, "NoCompression", compression: ExrCompression.None);
[Theory]
[WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)]
public void ExrEncoder_WithZipCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestExrEncoderCore(provider, "ZipCompression", compression: ExrCompression.Zip);
[Theory]
[WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)]
public void ExrEncoder_WithZipsCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestExrEncoderCore(provider, "ZipsCompression", compression: ExrCompression.Zips);
protected static void TestExrEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
object testOutputDetails,
ExrCompression compression = ExrCompression.None,
bool useExactComparer = true,
float compareTolerance = 0.001f,
IImageDecoder imageDecoder = null)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
ExrEncoder encoder = new()
{
Compression = compression,
};
// Does DebugSave & load reference CompareToReferenceInput():
image.VerifyEncoder(
provider,
"exr",
testOutputDetails: testOutputDetails,
encoder: encoder,
customComparer: useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance),
referenceDecoder: imageDecoder ?? ReferenceDecoder);
}
}

118
tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs

@ -0,0 +1,118 @@
// 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;
namespace SixLabors.ImageSharp.Tests.Formats.Exr;
[Trait("Format", "Exr")]
public class ExrMetadataTests
{
[Fact]
public void CloneIsDeep()
{
ExrMetadata meta = new()
{ ImageDataType = ExrImageDataType.Rgb, PixelType = ExrPixelType.Half, Compression = ExrCompression.None };
ExrMetadata clone = (ExrMetadata)meta.DeepClone();
clone.ImageDataType = ExrImageDataType.Gray;
clone.PixelType = ExrPixelType.Float;
clone.Compression = ExrCompression.Zip;
Assert.False(meta.ImageDataType.Equals(clone.ImageDataType));
Assert.False(meta.PixelType.Equals(clone.PixelType));
Assert.False(meta.Compression.Equals(clone.Compression));
}
[Theory]
[InlineData(TestImages.Exr.Uncompressed, 199, 297)]
public void Identify_DetectsCorrectWidthAndHeight<TPixel>(string imagePath, int expectedWidth, int expectedHeight)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream);
Assert.NotNull(imageInfo);
Assert.Equal(expectedWidth, imageInfo.Width);
Assert.Equal(expectedHeight, imageInfo.Height);
}
[Theory]
[InlineData(TestImages.Exr.Uncompressed, ExrPixelType.Half)]
[InlineData(TestImages.Exr.UncompressedFloatRgb, ExrPixelType.Float)]
[InlineData(TestImages.Exr.UncompressedUintRgb, ExrPixelType.UnsignedInt)]
public void Identify_DetectsCorrectPixelType(string imagePath, ExrPixelType expectedPixelType)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream);
Assert.NotNull(imageInfo);
ExrMetadata metadata = imageInfo.Metadata.GetExrMetadata();
Assert.NotNull(metadata);
Assert.Equal(expectedPixelType, metadata.PixelType);
}
[Theory]
[InlineData(TestImages.Exr.UncompressedRgba, ExrImageDataType.Rgba)]
[InlineData(TestImages.Exr.Rgb, ExrImageDataType.Rgb)]
[InlineData(TestImages.Exr.Gray, ExrImageDataType.Gray)]
public void Identify_DetectsCorrectImageDataType(string imagePath, ExrImageDataType expectedImageDataType)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream);
Assert.NotNull(imageInfo);
ExrMetadata metadata = imageInfo.Metadata.GetExrMetadata();
Assert.NotNull(metadata);
Assert.Equal(expectedImageDataType, metadata.ImageDataType);
}
[Theory]
[InlineData(TestImages.Exr.UncompressedRgba, ExrCompression.None)]
[InlineData(TestImages.Exr.B44, ExrCompression.B44)]
[InlineData(TestImages.Exr.Rle, ExrCompression.RunLengthEncoded)]
public void Identify_DetectsCorrectCompression(string imagePath, ExrCompression expectedCompression)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream);
Assert.NotNull(imageInfo);
ExrMetadata metadata = imageInfo.Metadata.GetExrMetadata();
Assert.NotNull(metadata);
Assert.Equal(expectedCompression, metadata.Compression);
}
[Theory]
[InlineData(PixelColorType.Binary, 1, ExrImageDataType.Unknown, ExrPixelType.Half)]
[InlineData(PixelColorType.Indexed, 8, ExrImageDataType.Unknown, ExrPixelType.Half)]
[InlineData(PixelColorType.Luminance, 16, ExrImageDataType.Gray, ExrPixelType.Half)]
[InlineData(PixelColorType.RGB, 48, ExrImageDataType.Rgb, ExrPixelType.Float)]
[InlineData(PixelColorType.BGR, 48, ExrImageDataType.Rgb, ExrPixelType.Float)]
[InlineData(PixelColorType.RGB | PixelColorType.Alpha, 64, ExrImageDataType.Rgba, ExrPixelType.Float)]
[InlineData(PixelColorType.BGR | PixelColorType.Alpha, 64, ExrImageDataType.Rgba, ExrPixelType.Float)]
[InlineData(PixelColorType.Luminance | PixelColorType.Alpha, 32, ExrImageDataType.Rgba, ExrPixelType.Float)]
[InlineData(PixelColorType.YCbCr, 48, ExrImageDataType.Unknown, ExrPixelType.Float)]
[InlineData(PixelColorType.CMYK, 64, ExrImageDataType.Unknown, ExrPixelType.Float)]
[InlineData(PixelColorType.YCCK, 64, ExrImageDataType.Unknown, ExrPixelType.Float)]
public void FromFormatConnectingMetadata_ConvertColorTypeAsExpected(PixelColorType pixelColorType, int bitsPerPixel, ExrImageDataType expectedImageDataType, ExrPixelType expectedPixelType)
{
FormatConnectingMetadata formatConnectingMetadata = new()
{
PixelTypeInfo = new PixelTypeInfo(bitsPerPixel)
{
ColorType = pixelColorType,
},
};
ExrMetadata actual = ExrMetadata.FromFormatConnectingMetadata(formatConnectingMetadata);
Assert.Equal(expectedImageDataType, actual.ImageDataType);
Assert.Equal(expectedPixelType, actual.PixelType);
}
}

136
tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs

@ -0,0 +1,136 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.Formats.Exr;
[Trait("Format", "Exr")]
public class ImageExtensionsTest
{
[Fact]
public void SaveAsExr_Path()
{
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest));
string file = Path.Combine(dir, "SaveAsExr_Path.exr");
using (Image<Rgba32> image = new(10, 10))
{
image.SaveAsExr(file);
}
IImageFormat format = Image.DetectFormat(file);
Assert.True(format is ExrFormat);
}
[Fact]
public async Task SaveAsExrAsync_Path()
{
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest));
string file = Path.Combine(dir, "SaveAsExrAsync_Path.exr");
using (Image<Rgba32> image = new(10, 10))
{
await image.SaveAsExrAsync(file);
}
IImageFormat format = Image.DetectFormat(file);
Assert.True(format is ExrFormat);
}
[Fact]
public void SaveAsExr_Path_Encoder()
{
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions));
string file = Path.Combine(dir, "SaveAsExr_Path_Encoder.exr");
using (Image<Rgba32> image = new(10, 10))
{
image.SaveAsExr(file, new ExrEncoder());
}
IImageFormat format = Image.DetectFormat(file);
Assert.True(format is ExrFormat);
}
[Fact]
public async Task SaveAsExrAsync_Path_Encoder()
{
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions));
string file = Path.Combine(dir, "SaveAsExrAsync_Path_Encoder.exr");
using (Image<Rgba32> image = new(10, 10))
{
await image.SaveAsExrAsync(file, new ExrEncoder());
}
IImageFormat format = Image.DetectFormat(file);
Assert.True(format is ExrFormat);
}
[Fact]
public void SaveAsExr_Stream()
{
using MemoryStream memoryStream = new();
using (Image<Rgba32> image = new(10, 10))
{
image.SaveAsExr(memoryStream);
}
memoryStream.Position = 0;
IImageFormat format = Image.DetectFormat(memoryStream);
Assert.True(format is ExrFormat);
}
[Fact]
public async Task SaveAsExrAsync_StreamAsync()
{
using MemoryStream memoryStream = new();
using (Image<Rgba32> image = new(10, 10))
{
await image.SaveAsExrAsync(memoryStream);
}
memoryStream.Position = 0;
IImageFormat format = Image.DetectFormat(memoryStream);
Assert.True(format is ExrFormat);
}
[Fact]
public void SaveAsExr_Stream_Encoder()
{
using MemoryStream memoryStream = new();
using (Image<Rgba32> image = new(10, 10))
{
image.SaveAsExr(memoryStream, new ExrEncoder());
}
memoryStream.Position = 0;
IImageFormat format = Image.DetectFormat(memoryStream);
Assert.True(format is ExrFormat);
}
[Fact]
public async Task SaveAsExrAsync_Stream_Encoder()
{
using MemoryStream memoryStream = new();
using (Image<Rgba32> image = new(10, 10))
{
await image.SaveAsExrAsync(memoryStream, new ExrEncoder());
}
memoryStream.Position = 0;
IImageFormat format = Image.DetectFormat(memoryStream);
Assert.True(format is ExrFormat);
}
}

56
tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs

@ -24,5 +24,59 @@ public class ColorNumericsTests
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
// TODO: We need to test all ColorNumerics methods! [Theory]
[InlineData((ushort)0, (byte)0)]
[InlineData((ushort)128, (byte)0)]
[InlineData((ushort)129, (byte)1)]
[InlineData((ushort)257, (byte)1)]
[InlineData((ushort)32896, (byte)128)]
[InlineData(ushort.MaxValue, byte.MaxValue)]
public void From16BitTo8Bit_ReturnsExpectedValue(ushort component, byte expected)
{
byte actual = ColorNumerics.From16BitTo8Bit(component);
Assert.Equal(expected, actual);
}
[Fact]
public void From16BitTo8Bit_RoundTripsAllExpanded8BitValues()
{
for (int i = 0; i <= byte.MaxValue; i++)
{
byte expected = (byte)i;
ushort component = ColorNumerics.From8BitTo16Bit(expected);
byte actual = ColorNumerics.From16BitTo8Bit(component);
Assert.Equal(expected, actual);
}
}
[Theory]
[InlineData(0U, (byte)0)]
[InlineData(8421504U, (byte)0)]
[InlineData(8421505U, (byte)1)]
[InlineData(16843009U, (byte)1)]
[InlineData(2155905152U, (byte)128)]
[InlineData(uint.MaxValue, byte.MaxValue)]
public void From32BitTo8Bit_ReturnsExpectedValue(uint component, byte expected)
{
byte actual = ColorNumerics.From32BitTo8Bit(component);
Assert.Equal(expected, actual);
}
[Fact]
public void From32BitTo8Bit_RoundTripsAllExpanded8BitValues()
{
for (int i = 0; i <= byte.MaxValue; i++)
{
byte expected = (byte)i;
uint component = ColorNumerics.From8BitTo32Bit(expected);
byte actual = ColorNumerics.From32BitTo8Bit(component);
Assert.Equal(expected, actual);
}
}
} }

311
tests/ImageSharp.Tests/PixelFormats/Rgb96Tests.cs

@ -0,0 +1,311 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
[Trait("Category", "PixelFormats")]
public class Rgb96Tests
{
[Theory]
[InlineData(1, 2, 3)]
[InlineData(50, 70, 80)]
[InlineData(1000, 2000, 3000)]
public void Constructor(uint r, uint g, uint b)
{
Rgb96 p = new(r, g, b);
Assert.Equal(r, p.R);
Assert.Equal(g, p.G);
Assert.Equal(b, p.B);
}
[Fact]
public void Rgb96_ToVector4()
{
Assert.Equal(Vector4.UnitW, new Rgb96(0, 0, 0).ToVector4());
Assert.Equal(Vector4.One, new Rgb96(uint.MaxValue, uint.MaxValue, uint.MaxValue).ToVector4());
Assert.Equal(new Vector4(0.5F, 0.5F, 0.5F, 1.0F), new Rgb96(uint.MaxValue / 2, uint.MaxValue / 2, uint.MaxValue / 2).ToVector4());
}
[Theory]
[InlineData(uint.MaxValue, uint.MaxValue, uint.MaxValue)]
[InlineData(0, 0, 0)]
[InlineData(uint.MaxValue / 2, 100, 2222)]
public void Rgb96_ToScaledVector4(uint r, uint g, uint b)
{
// arrange
Rgb96 rgb = new(r, g, b);
float max = uint.MaxValue;
float rr = r / max;
float gg = g / max;
float bb = b / max;
// act
Vector4 actual = rgb.ToScaledVector4();
// assert
Assert.Equal(rr, actual.X);
Assert.Equal(gg, actual.Y);
Assert.Equal(bb, actual.Z);
Assert.Equal(1.0F, actual.W);
}
[Theory]
[InlineData(0, 0, 0)]
[InlineData(5000, 100, 2222)]
public void Rgb96_FromScaledVector4(uint r, uint g, uint b)
{
// arrange
Rgb96 source = new(r, g, b);
Vector4 scaled = source.ToScaledVector4();
// act
Rgb96 actual = Rgb96.FromScaledVector4(scaled);
// assert
Assert.Equal(source, actual);
}
[Fact]
public void Rgb96_ToRgba32()
{
// arrange
Rgb96 rgb96 = new((uint)(uint.MaxValue * 0.1), uint.MaxValue / 2, uint.MaxValue);
Rgba32 expected = new(25, 127, 255, 255);
// act
Rgba32 actual = rgb96.ToRgba32();
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Equality_WhenTrue()
{
Rgb96 c1 = new(100, 2000, 3000);
Rgb96 c2 = new(100, 2000, 3000);
Assert.True(c1.Equals(c2));
Assert.True(c1.GetHashCode() == c2.GetHashCode());
}
[Fact]
public void Equality_WhenFalse()
{
Rgb96 c1 = new(100, 2000, 3000);
Rgb96 c2 = new(101, 2000, 3000);
Rgb96 c3 = new(100, 2000, 3001);
Assert.False(c1.Equals(c2));
Assert.False(c2.Equals(c3));
Assert.False(c3.Equals(c1));
}
[Fact]
public void Rgb96_FromRgba32()
{
// arrange
Rgba32 source = new(25, 127, 255, 255);
Rgb96 expected = new(421075225, 2139062143, uint.MaxValue);
// act
Rgb96 actual = Rgb96.FromRgba32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromRgb24()
{
// arrange
Rgb24 source = new(25, 127, 255);
Rgb96 expected = new(421075225, 2139062143, uint.MaxValue);
// act
Rgb96 actual = Rgb96.FromRgb24(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromRgb48()
{
// arrange
Rgb48 source = new(0, 32767, ushort.MaxValue);
Rgb96 expected = new(0, 2147450879, uint.MaxValue);
// act
Rgb96 actual = Rgb96.FromRgb48(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromRgba64()
{
// arrange
Rgba64 source = new(0, 32767, ushort.MaxValue, ushort.MaxValue);
Rgb96 expected = new(0, 2147450879, uint.MaxValue);
// act
Rgb96 actual = Rgb96.FromRgba64(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromLa32()
{
// arrange
La32 source = new(32767, ushort.MaxValue);
Rgb96 expected = new(2147450879, 2147450879, 2147450879);
// act
Rgb96 actual = Rgb96.FromLa32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromLa16()
{
// arrange
La16 source = new(127, byte.MaxValue);
Rgb96 expected = new(2139062143, 2139062143, 2139062143);
// act
Rgb96 actual = Rgb96.FromLa16(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromL16()
{
// arrange
L16 source = new(32767);
Rgb96 expected = new(2147450879, 2147450879, 2147450879);
// act
Rgb96 actual = Rgb96.FromL16(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromL8()
{
// arrange
L8 source = new(127);
Rgb96 expected = new(2139062143, 2139062143, 2139062143);
// act
Rgb96 actual = Rgb96.FromL8(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromBgra32()
{
// arrange
Bgra32 source = new(127, 25, 255);
Rgb96 expected = new(2139062143, 421075225, uint.MaxValue);
// act
Rgb96 actual = Rgb96.FromBgra32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromBgr24()
{
// arrange
Bgr24 source = new(127, 25, 255);
Rgb96 expected = new(2139062143, 421075225, uint.MaxValue);
// act
Rgb96 actual = Rgb96.FromBgr24(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromBgra5551()
{
// arrange
Bgra5551 source = new(1.0f, 1.0f, 1.0f, 1.0f);
Rgb96 expected = new(uint.MaxValue, uint.MaxValue, uint.MaxValue);
// act
Rgb96 actual = Rgb96.FromBgra5551(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromArgb32()
{
// arrange
Argb32 source = new(127, 25, 255);
Rgb96 expected = new(2139062143, 421075225, uint.MaxValue);
// act
Rgb96 actual = Rgb96.FromArgb32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_FromAbgr32()
{
// arrange
Abgr32 source = new(127, 25, 255);
Rgb96 expected = new(2139062143, 421075225, uint.MaxValue);
// act
Rgb96 actual = Rgb96.FromAbgr32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgb96_PixelInformation()
{
PixelTypeInfo info = Rgb96.GetPixelTypeInfo();
Assert.Equal(Unsafe.SizeOf<Rgb96>() * 8, info.BitsPerPixel);
Assert.Equal(PixelAlphaRepresentation.None, info.AlphaRepresentation);
Assert.Equal(PixelColorType.RGB, info.ColorType);
PixelComponentInfo? maybeComponentInfo = info.ComponentInfo;
Assert.NotNull(maybeComponentInfo);
PixelComponentInfo componentInfo = maybeComponentInfo.Value;
Assert.Equal(3, componentInfo.ComponentCount);
Assert.Equal(0, componentInfo.Padding);
Assert.Equal(32, componentInfo.GetComponentPrecision(0));
Assert.Equal(32, componentInfo.GetComponentPrecision(1));
Assert.Equal(32, componentInfo.GetComponentPrecision(2));
Assert.Equal(32, componentInfo.GetMaximumComponentPrecision());
}
}

313
tests/ImageSharp.Tests/PixelFormats/Rgba128Tests.cs

@ -0,0 +1,313 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
[Trait("Category", "PixelFormats")]
public class Rgba128Tests
{
[Theory]
[InlineData(1, 2, 3, 4)]
[InlineData(50, 70, 80, 90)]
[InlineData(1000, 2000, 3000, 4000)]
public void Constructor(uint r, uint g, uint b, uint a)
{
Rgba128 p = new(r, g, b, a);
Assert.Equal(r, p.R);
Assert.Equal(g, p.G);
Assert.Equal(b, p.B);
}
[Fact]
public void Rgba128_ToVector4()
{
Assert.Equal(Vector4.Zero, new Rgba128(0, 0, 0, 0).ToVector4());
Assert.Equal(Vector4.One, new Rgba128(uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue).ToVector4());
Assert.Equal(new Vector4(0.5F, 0.5F, 0.5F, 0.5F), new Rgba128(uint.MaxValue / 2, uint.MaxValue / 2, uint.MaxValue / 2, uint.MaxValue / 2).ToVector4());
}
[Theory]
[InlineData(uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue)]
[InlineData(0, 0, 0, 0)]
[InlineData(uint.MaxValue / 2, 100, 2222, 3333)]
public void Rgba128_ToScaledVector4(uint r, uint g, uint b, uint a)
{
// arrange
Rgba128 rgb = new(r, g, b, a);
float max = uint.MaxValue;
float rr = r / max;
float gg = g / max;
float bb = b / max;
float aa = a / max;
// act
Vector4 actual = rgb.ToScaledVector4();
// assert
Assert.Equal(rr, actual.X);
Assert.Equal(gg, actual.Y);
Assert.Equal(bb, actual.Z);
Assert.Equal(aa, actual.W);
}
[Theory]
[InlineData(0, 0, 0, 0)]
[InlineData(5000, 100, 2222, 3333)]
public void Rgba128_FromScaledVector4(uint r, uint g, uint b, uint a)
{
// arrange
Rgba128 source = new(r, g, b, a);
Vector4 scaled = source.ToScaledVector4();
// act
Rgba128 actual = Rgba128.FromScaledVector4(scaled);
// assert
Assert.Equal(source, actual);
}
[Fact]
public void Rgba128_ToRgba32()
{
// arrange
Rgba128 rgb96 = new((uint)(uint.MaxValue * 0.1), uint.MaxValue / 2, uint.MaxValue, uint.MaxValue);
Rgba32 expected = new(25, 127, 255, 255);
// act
Rgba32 actual = rgb96.ToRgba32();
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Equality_WhenTrue()
{
Rgba128 c1 = new(100, 2000, 3000, 4000);
Rgba128 c2 = new(100, 2000, 3000, 4000);
Assert.True(c1.Equals(c2));
Assert.True(c1.GetHashCode() == c2.GetHashCode());
}
[Fact]
public void Equality_WhenFalse()
{
Rgba128 c1 = new(100, 2000, 3000, 4000);
Rgba128 c2 = new(101, 2000, 3000, 4000);
Rgba128 c3 = new(100, 2000, 3001, 4000);
Assert.False(c1.Equals(c2));
Assert.False(c2.Equals(c3));
Assert.False(c3.Equals(c1));
}
[Fact]
public void Rgba128_FromRgba32()
{
// arrange
Rgba32 source = new(25, 127, 255, 255);
Rgba128 expected = new(421075225, 2139062143, uint.MaxValue, uint.MaxValue);
// act
Rgba128 actual = Rgba128.FromRgba32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromRgb24()
{
// arrange
Rgb24 source = new(25, 127, 255);
Rgba128 expected = new(421075225, 2139062143, uint.MaxValue, uint.MaxValue);
// act
Rgba128 actual = Rgba128.FromRgb24(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromRgb48()
{
// arrange
Rgb48 source = new(0, 32767, ushort.MaxValue);
Rgba128 expected = new(0, 2147450879, uint.MaxValue, uint.MaxValue);
// act
Rgba128 actual = Rgba128.FromRgb48(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromRgba64()
{
// arrange
Rgba64 source = new(0, 32767, ushort.MaxValue, ushort.MaxValue);
Rgba128 expected = new(0, 2147450879, uint.MaxValue, uint.MaxValue);
// act
Rgba128 actual = Rgba128.FromRgba64(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromLa32()
{
// arrange
La32 source = new(32767, ushort.MaxValue);
Rgba128 expected = new(2147450879, 2147450879, 2147450879, 2147450879);
// act
Rgba128 actual = Rgba128.FromLa32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromLa16()
{
// arrange
La16 source = new(127, byte.MaxValue);
Rgba128 expected = new(2139062143, 2139062143, 2139062143, 2139062143);
// act
Rgba128 actual = Rgba128.FromLa16(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromL16()
{
// arrange
L16 source = new(32767);
Rgba128 expected = new(2147450879, 2147450879, 2147450879, 2147450879);
// act
Rgba128 actual = Rgba128.FromL16(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromL8()
{
// arrange
L8 source = new(127);
Rgba128 expected = new(2139062143, 2139062143, 2139062143, 2139062143);
// act
Rgba128 actual = Rgba128.FromL8(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromBgra32()
{
// arrange
Bgra32 source = new(127, 25, 255);
Rgba128 expected = new(2139062143, 421075225, uint.MaxValue, uint.MaxValue);
// act
Rgba128 actual = Rgba128.FromBgra32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromBgr24()
{
// arrange
Bgr24 source = new(127, 25, 255);
Rgba128 expected = new(2139062143, 421075225, uint.MaxValue, uint.MaxValue);
// act
Rgba128 actual = Rgba128.FromBgr24(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromBgra5551()
{
// arrange
Bgra5551 source = new(1.0f, 1.0f, 1.0f, 1.0f);
Rgba128 expected = new(uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue);
// act
Rgba128 actual = Rgba128.FromBgra5551(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromArgb32()
{
// arrange
Argb32 source = new(127, 25, 255);
Rgba128 expected = new(2139062143, 421075225, uint.MaxValue, uint.MaxValue);
// act
Rgba128 actual = Rgba128.FromArgb32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_FromAbgr32()
{
// arrange
Abgr32 source = new(127, 25, 255);
Rgba128 expected = new(2139062143, 421075225, uint.MaxValue, uint.MaxValue);
// act
Rgba128 actual = Rgba128.FromAbgr32(source);
// assert
Assert.Equal(expected, actual);
}
[Fact]
public void Rgba128_PixelInformation()
{
PixelTypeInfo info = Rgba128.GetPixelTypeInfo();
Assert.Equal(Unsafe.SizeOf<Rgba128>() * 8, info.BitsPerPixel);
Assert.Equal(PixelAlphaRepresentation.Unassociated, info.AlphaRepresentation);
Assert.Equal(PixelColorType.RGB | PixelColorType.Alpha, info.ColorType);
PixelComponentInfo? maybeComponentInfo = info.ComponentInfo;
Assert.NotNull(maybeComponentInfo);
PixelComponentInfo componentInfo = maybeComponentInfo.Value;
Assert.Equal(4, componentInfo.ComponentCount);
Assert.Equal(0, componentInfo.Padding);
Assert.Equal(32, componentInfo.GetComponentPrecision(0));
Assert.Equal(32, componentInfo.GetComponentPrecision(1));
Assert.Equal(32, componentInfo.GetComponentPrecision(2));
Assert.Equal(32, componentInfo.GetComponentPrecision(3));
Assert.Equal(32, componentInfo.GetMaximumComponentPrecision());
}
}

16
tests/ImageSharp.Tests/TestImages.cs

@ -1381,4 +1381,20 @@ public static class TestImages
public const string CurReal = "Icon/cur_real.cur"; public const string CurReal = "Icon/cur_real.cur";
public const string CurFake = "Icon/cur_fake.ico"; public const string CurFake = "Icon/cur_fake.ico";
} }
public static class Exr
{
public const string Benchmark = "Exr/Calliphora_benchmark.exr";
public const string Uncompressed = "Exr/Calliphora_uncompressed.exr";
public const string UncompressedRgba = "Exr/Calliphora_uncompressed_rgba.exr";
public const string UncompressedFloatRgb = "Exr/rgb_float32_uncompressed.exr";
public const string UncompressedUintRgb = "Exr/Calliphora_uint32_uncompressed.exr";
public const string UintRgba = "Exr/rgba_uint_uncompressed.exr";
public const string Zip = "Exr/Calliphora_zip.exr";
public const string Zips = "Exr/Calliphora_zips.exr";
public const string Rle = "Exr/Calliphora_rle.exr";
public const string B44 = "Exr/Calliphora_b44.exr";
public const string Rgb = "Exr/Calliphora_rgb.exr";
public const string Gray = "Exr/Calliphora_gray.exr";
}
} }

18
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -6,6 +6,7 @@ using ImageMagick;
using ImageMagick.Formats; using ImageMagick.Formats;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff;
@ -42,6 +43,8 @@ public class MagickReferenceDecoder : ImageDecoder
public static MagickReferenceDecoder WebP { get; } = new(WebpFormat.Instance); public static MagickReferenceDecoder WebP { get; } = new(WebpFormat.Instance);
public static MagickReferenceDecoder Exr { get; } = new(ExrFormat.Instance);
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken) protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{ {
ImageMetadata metadata = new(); ImageMetadata metadata = new();
@ -58,14 +61,14 @@ public class MagickReferenceDecoder : ImageDecoder
MagickReadSettings settings = new() MagickReadSettings settings = new()
{ {
FrameCount = (int)options.MaxFrames FrameCount = options.MaxFrames
}; };
settings.SetDefines(bmpReadDefines); settings.SetDefines(bmpReadDefines);
settings.SetDefines(pngReadDefines); settings.SetDefines(pngReadDefines);
using MagickImageCollection magickImageCollection = new(stream, settings); using MagickImageCollection magickImageCollection = new(stream, settings);
int imageWidth = magickImageCollection.Max(x => x.Width); int imageWidth = (int)magickImageCollection.Max(x => x.Width);
int imageHeight = magickImageCollection.Max(x => x.Height); int imageHeight = (int)magickImageCollection.Max(x => x.Height);
List<ImageFrame<TPixel>> framesList = []; List<ImageFrame<TPixel>> framesList = [];
foreach (IMagickImage<ushort> magicFrame in magickImageCollection) foreach (IMagickImage<ushort> magicFrame in magickImageCollection)
@ -74,10 +77,10 @@ public class MagickReferenceDecoder : ImageDecoder
framesList.Add(frame); framesList.Add(frame);
Buffer2DRegion<TPixel> buffer = frame.PixelBuffer.GetRegion( Buffer2DRegion<TPixel> buffer = frame.PixelBuffer.GetRegion(
imageWidth - magicFrame.Width, (int)(imageWidth - magicFrame.Width),
imageHeight - magicFrame.Height, (int)(imageHeight - magicFrame.Height),
magicFrame.Width, (int)magicFrame.Width,
magicFrame.Height); (int)magicFrame.Height);
using IUnsafePixelCollection<ushort> pixels = magicFrame.GetPixelsUnsafe(); using IUnsafePixelCollection<ushort> pixels = magicFrame.GetPixelsUnsafe();
if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1) if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1)
@ -118,6 +121,7 @@ public class MagickReferenceDecoder : ImageDecoder
PixelType = metadata.GetDecodedPixelTypeInfo() PixelType = metadata.GetDecodedPixelTypeInfo()
}; };
} }
private static void FromRgba32Bytes<TPixel>( private static void FromRgba32Bytes<TPixel>(
Configuration configuration, Configuration configuration,
Span<byte> rgbaBytes, Span<byte> rgbaBytes,

4
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs

@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Pbm;
@ -63,12 +64,13 @@ public static partial class TestEnvironment
new TgaConfigurationModule(), new TgaConfigurationModule(),
new WebpConfigurationModule(), new WebpConfigurationModule(),
new TiffConfigurationModule(), new TiffConfigurationModule(),
new ExrConfigurationModule(),
new QoiConfigurationModule()); new QoiConfigurationModule());
IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration(); IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration();
IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder(); IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder();
// Magick codecs should work on all platforms // Magick codecs should work on all platforms.
cfg.ConfigureCodecs( cfg.ConfigureCodecs(
PngFormat.Instance, PngFormat.Instance,
MagickReferenceDecoder.Png, MagickReferenceDecoder.Png,

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_issue735.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:15de93db72e2de0ad1a21b67931df57c1a129f062fe1ed3cb7f083761af4fe36
size 61931

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7a568f9f12adc5606e5b74d25a4bf083e2b565d73635704e205400d5a073fcd3
size 9895

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565pal.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7a568f9f12adc5606e5b74d25a4bf083e2b565d73635704e205400d5a073fcd3
size 9895

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16bfdef.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c05e0b14a389d11f531d4238900106a81f25a741a55bbbcd160b8b67eb32adb6
size 7019

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bf.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d577ac117b8260ad08e6f051c949d9187f04699eed0f063476aaf42eada2366
size 20640

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bfdef.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d577ac117b8260ad08e6f051c949d9187f04699eed0f063476aaf42eada2366
size 20640

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_16Bit_Inverted_Rgba32_test16-inverted.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:53a33da2c1a84c0e8426e1a2ba24e2fe2de17a7bc75760184fb10bf6dc96954a
size 7896

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rlecut.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f0c97446dec6af009423dcae431c44d23963179c3c3bfbeaf3f3b52022a33f68
size 2414

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rletrns.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a651b81764696cd4fe42113d5ac26006f54820d2df8b2103793e82c66597b2b0
size 2837

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_rle4-delta-320x240.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55672d405a3d085c72cfc9fb26bc3955bf858e525c54c6c4505f093f011f378d
size 3188

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted_Rgba32_RunLengthEncoded-inverted.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e774cba4dda2fe9d3cdff141e7a8c1de7f3e9c8014093abf8697a34e6cc7144
size 5379

3
tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder_Rgba32_pal8rlecut.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44d281c31cd264dcb812df8cb5dc9d5042e915d64e8013af5577dbfea6cbb1cf
size 3955

3
tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint_Rgba32_Calliphora_uint32_uncompressed.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f66bf45b5d80e412314341567b8cf240d56b5872f01ce9f3b0d6034d85e8e942
size 163327

3
tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Uint_Rgba32_rgba_uint_uncompressed.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:107c6d26cdb8aac539011acf7213376e930cb705f5815b010b31ce4b9e738f9b
size 25780

3
tests/Images/Input/Exr/Calliphora_b44.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7a6ac7fd879fc2dae646adaa14382aa10e5b8d02634af4668dd04bce09148151
size 157973

3
tests/Images/Input/Exr/Calliphora_benchmark.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e3368860692927e709365f2d37b92411068e77a0f23624ff57af6089ec69f357
size 2592888

3
tests/Images/Input/Exr/Calliphora_gray.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a5daadcfd4ff0e45282d39d6c54f9a13651da3fd8841abda580e76661555470
size 124245

3
tests/Images/Input/Exr/Calliphora_rgb.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f54e5e57df1b8cdf1b26418d5696dd9cefbd7ed3bf31cdcde06fd9a7bf5e3724
size 362681

3
tests/Images/Input/Exr/Calliphora_rle.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ccd25a952240a75d9c8b646be5001a2b006c053314e30cdf12e35ac865a31ae2
size 292545

3
tests/Images/Input/Exr/Calliphora_uint32_uncompressed.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bed68d2e491cad0bdd0fa668ce57328951927d722f130ba7c5c5475f6a3e0d96
size 289183

3
tests/Images/Input/Exr/Calliphora_uncompressed.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f54e5e57df1b8cdf1b26418d5696dd9cefbd7ed3bf31cdcde06fd9a7bf5e3724
size 362681

3
tests/Images/Input/Exr/Calliphora_uncompressed_rgba.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e9f6b5afa3c10c895ba67b51568295be40c2b1057224437600028487c581291
size 481899

3
tests/Images/Input/Exr/Calliphora_zip.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a8b8e3b9a74179cb51a7a85031b3d177e109c20a56392804b10873273ff2163e
size 181871

3
tests/Images/Input/Exr/Calliphora_zips.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c6962a5b06c677648d5e22ff30ba8a28e7801fffb32259984e23bbaa97061b1
size 220159

3
tests/Images/Input/Exr/rgb_float32_uncompressed.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ab8b71824ca384fc2cde1f74a6f34c38169fa328ccc67f17d08333e9de42f55
size 243558

3
tests/Images/Input/Exr/rgba_uint_uncompressed.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b45345cb1e4539e5298ccce3ccfc0f27b7a469836bfd7aa13c4d9f6c52baa7a
size 482731
Loading…
Cancel
Save