diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs new file mode 100644 index 0000000000..55e9003bc1 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs @@ -0,0 +1,191 @@ +// 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.OpenExr.Compression.Decompressors; + +internal class B44Compression : ExrBaseDecompressor +{ + private readonly int width; + + private readonly int height; + + private readonly uint rowsPerBlock; + + private readonly int channelCount; + + private byte[] scratch = new byte[14]; + + private ushort[] s = new ushort[16]; + + private IMemoryOwner tmpBuffer; + + public B44Compression(MemoryAllocator allocator, uint uncompressedBytes, int width, int height, uint rowsPerBlock, int channelCount) + : base(allocator, uncompressedBytes) + { + this.width = width; + this.height = height; + 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/OpenExr/Compression/Compressors/NoneExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs similarity index 89% rename from src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs rename to src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs index 333c453ebd..f5f16a0fd4 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; internal class NoneExrCompression : ExrBaseDecompressor { diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs similarity index 82% rename from src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs rename to src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs index 08126c4de5..0838dc1dcf 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs @@ -5,12 +5,14 @@ using System.Buffers; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; internal class RunLengthCompression : ExrBaseDecompressor { private readonly IMemoryOwner tmpBuffer; + private readonly ushort[] s = new ushort[16]; + public RunLengthCompression(MemoryAllocator allocator, uint uncompressedBytes) : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); @@ -34,12 +36,6 @@ internal class RunLengthCompression : ExrBaseDecompressor return; } - // Check the input buffer is big enough to contain 'count' bytes of remaining data. - if (compressedBytes < 0) - { - return; - } - for (int i = 0; i < count; i++) { uncompressed[offset + i] = ReadNextByte(stream); @@ -58,12 +54,6 @@ internal class RunLengthCompression : ExrBaseDecompressor return; } - // Check the input buffer is big enough to contain byte to be duplicated. - if (compressedBytes < 0) - { - return; - } - for (int i = 0; i < count + 1; i++) { uncompressed[offset + i] = value; diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs similarity index 86% rename from src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs rename to src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs index d90f684e64..e315af8e8f 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; internal class ZipExrCompression : ExrBaseDecompressor { @@ -21,15 +21,15 @@ internal class ZipExrCompression : ExrBaseDecompressor Span uncompressed = this.tmpBuffer.GetSpan(); long pos = stream.Position; - using ZlibInflateStream deframeStream = new( + using ZlibInflateStream inflateStream = new( stream, () => { int left = (int)(compressedBytes - (stream.Position - pos)); return left > 0 ? left : 0; }); - deframeStream.AllocateNewBytes((int)this.UncompressedBytes, true); - DeflateStream dataStream = deframeStream.CompressedStream; + inflateStream.AllocateNewBytes((int)this.UncompressedBytes, true); + DeflateStream dataStream = inflateStream.CompressedStream; int totalRead = 0; while (totalRead < buffer.Length) diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs index 2d01efd87b..538231548c 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs @@ -1,14 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +using SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; internal static class ExrDecompressorFactory { - public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint uncompressedBytes) + public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint uncompressedBytes, int width, int height, uint rowsPerBlock, int channelCount) { switch (method) { @@ -20,6 +20,8 @@ internal static class ExrDecompressorFactory return new ZipExrCompression(memoryAllocator, uncompressedBytes); case ExrCompressionType.RunLengthEncoded: return new RunLengthCompression(memoryAllocator, uncompressedBytes); + case ExrCompressionType.B44: + return new B44Compression(memoryAllocator, uncompressedBytes, width, height, rowsPerBlock, channelCount); default: throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index ca244c5674..2304f3f203 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -57,12 +57,24 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals /// public Size Dimensions => new(this.Width, this.Height); + /// + /// 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 ExrCompressionType Compression { get; set; } private ExrImageDataType ImageDataType { get; set; } @@ -76,11 +88,15 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals where TPixel : unmanaged, IPixel { this.ReadExrHeader(stream); - this.IsSupportedCompression(); + if (!this.IsSupportedCompression()) + { + ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); + } + ExrPixelType pixelType = this.ValidateChannels(); this.ReadImageDataType(); - Image image = new Image(this.configuration, this.Width, this.Height, this.metadata); + Image image = new (this.configuration, this.Width, this.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); switch (pixelType) @@ -109,6 +125,7 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals 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); @@ -118,7 +135,7 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals 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, bytesPerBlock); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); TPixel color = default; for (uint y = 0; y < height; y += rowsPerBlock) @@ -164,6 +181,7 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals 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); @@ -173,7 +191,7 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals 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, bytesPerBlock); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); TPixel color = default; for (uint y = 0; y < height; y += rowsPerBlock) @@ -609,12 +627,19 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals return pixelType.Value; } - private void IsSupportedCompression() + private bool IsSupportedCompression() { - if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.Zip and not ExrCompressionType.RunLengthEncoded) + switch (this.Compression) { - ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); + case ExrCompressionType.None: + case ExrCompressionType.Zip: + case ExrCompressionType.Zips: + case ExrCompressionType.RunLengthEncoded: + case ExrCompressionType.B44: + return true; } + + return false; } private void ReadImageDataType() diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index 76e186eb8a..21fb0ade49 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Buffers.Binary; +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.OpenExr.Compression; using SixLabors.ImageSharp.Memory; @@ -162,14 +163,14 @@ internal sealed class ExrEncoderCore : IImageEncoderInternals Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); - var rgb = default(Rgb96); + Rgb96 rgb = default; for (int y = 0; y < height; y++) { Span pixelRowSpan = pixels.DangerousGetRowSpan(y); for (int x = 0; x < width; x++) { - var vector4 = pixelRowSpan[x].ToVector4(); + Vector4 vector4 = pixelRowSpan[x].ToVector4(); rgb.FromVector4(vector4); redBuffer[x] = rgb.R; @@ -336,10 +337,10 @@ internal sealed class ExrEncoderCore : IImageEncoderInternals private void WriteAttributeInformation(Stream stream, string name, string type, int size) { // Write attribute name. - this.WriteString(stream, name); + WriteString(stream, name); // Write attribute type. - this.WriteString(stream, type); + WriteString(stream, type); // Write attribute size. BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size); @@ -348,7 +349,7 @@ internal sealed class ExrEncoderCore : IImageEncoderInternals private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) { - this.WriteString(stream, channelInfo.ChannelName); + WriteString(stream, channelInfo.ChannelName); BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType); stream.Write(this.buffer.AsSpan(0, 4)); @@ -367,7 +368,7 @@ internal sealed class ExrEncoderCore : IImageEncoderInternals stream.Write(this.buffer.AsSpan(0, 4)); } - private void WriteString(Stream stream, string str) + private static void WriteString(Stream stream, string str) { foreach (char c in str) { diff --git a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs index 62f9f1b2f4..b609d0038c 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Formats.OpenExr; /// @@ -10,21 +8,15 @@ namespace SixLabors.ImageSharp.Formats.OpenExr; /// internal static class ExrThrowHelper { - [MethodImpl(InliningOptions.ColdPath)] public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); - [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); - [MethodImpl(InliningOptions.ColdPath)] public static void ThrowNotSupportedVersion() => throw new NotSupportedException("Unsupported EXR version"); - [MethodImpl(InliningOptions.ColdPath)] public static void ThrowNotSupported(string msg) => throw new NotSupportedException(msg); - [MethodImpl(InliningOptions.ColdPath)] public static void ThrowInvalidImageHeader() => throw new InvalidImageContentException("Invalid EXR image header"); - [MethodImpl(InliningOptions.ColdPath)] public static void ThrowInvalidImageHeader(string msg) => throw new InvalidImageContentException(msg); } diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 709a442630..29b75bc9cd 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -51,4 +51,14 @@ public class ExrDecoderTests image.DebugSave(provider); image.CompareToOriginal(provider); } + + [Theory] + [WithFile(TestImages.Exr.B44, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_B44Compressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider); + } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8922e3ebee..25187b0fc2 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1005,5 +1005,6 @@ public static class TestImages 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"; } } diff --git a/tests/Images/Input/Exr/Calliphora_b44.exr b/tests/Images/Input/Exr/Calliphora_b44.exr new file mode 100644 index 0000000000..ebe464170f --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_b44.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cde761051241cba1e7f8466d71602abdb130677de55602fe6afec4b287c75b9d +size 2533521