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