Browse Source

Add support for decoding b44 compressed exr files

pull/3096/head
Brian Popow 4 years ago
parent
commit
a9620ee52f
  1. 191
      src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs
  2. 2
      src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs
  3. 16
      src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs
  4. 8
      src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs
  5. 6
      src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs
  6. 39
      src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs
  7. 13
      src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs
  8. 8
      src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs
  9. 10
      tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs
  10. 1
      tests/ImageSharp.Tests/TestImages.cs
  11. 3
      tests/Images/Input/Exr/Calliphora_b44.exr

191
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<ushort> 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<ushort>((int)(width * rowsPerBlock * channelCount));
}
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<ushort> outputBuffer = MemoryMarshal.Cast<byte, ushort>(buffer);
Span<ushort> decompressed = this.tmpBuffer.GetSpan();
int outputOffset = 0;
int bytesLeft = (int)compressedBytes;
for (int i = 0; i < this.channelCount && bytesLeft > 0; i++)
{
for (int y = 0; y < this.rowsPerBlock; y += 4)
{
Span<ushort> row0 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row1 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row2 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row3 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
int rowOffset = 0;
for (int x = 0; x < this.width && bytesLeft > 0; x += 4)
{
int bytesRead = stream.Read(this.scratch, 0, 3);
if (bytesRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream");
}
if (this.scratch[2] >= 13 << 2)
{
Unpack3(this.scratch, this.s);
bytesLeft -= 3;
}
else
{
bytesRead = stream.Read(this.scratch, 3, 11);
if (bytesRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream");
}
Unpack14(this.scratch, this.s);
bytesLeft -= 14;
}
int n = x + 3 < this.width ? 4 : this.width - x;
if (y + 3 < this.rowsPerBlock)
{
this.s.AsSpan(0, n).CopyTo(row0.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<byte> b, Span<ushort> s)
{
s[0] = (ushort)((b[0] << 8) | b[1]);
ushort shift = (ushort)(b[2] >> 2);
ushort bias = (ushort)(0x20u << shift);
s[4] = (ushort)(s[0] + ((((b[2] << 4) | (b[3] >> 4)) & 0x3fu) << shift) - bias);
s[8] = (ushort)(s[4] + ((((b[3] << 2) | (b[4] >> 6)) & 0x3fu) << shift) - bias);
s[12] = (ushort)(s[8] + ((b[4] & 0x3fu) << shift) - bias);
s[1] = (ushort)(s[0] + ((uint)(b[5] >> 2) << shift) - bias);
s[5] = (ushort)(s[4] + ((((b[5] << 4) | (b[6] >> 4)) & 0x3fu) << shift) - bias);
s[9] = (ushort)(s[8] + ((((b[6] << 2) | (b[7] >> 6)) & 0x3fu) << shift) - bias);
s[13] = (ushort)(s[12] + ((b[7] & 0x3fu) << shift) - bias);
s[2] = (ushort)(s[1] + ((uint)(b[8] >> 2) << shift) - bias);
s[6] = (ushort)(s[5] + ((((b[8] << 4) | (b[9] >> 4)) & 0x3fu) << shift) - bias);
s[10] = (ushort)(s[9] + ((((b[9] << 2) | (b[10] >> 6)) & 0x3fu) << shift) - bias);
s[14] = (ushort)(s[13] + ((b[10] & 0x3fu) << shift) - bias);
s[3] = (ushort)(s[2] + ((uint)(b[11] >> 2) << shift) - bias);
s[7] = (ushort)(s[6] + ((((b[11] << 4) | (b[12] >> 4)) & 0x3fu) << shift) - bias);
s[11] = (ushort)(s[10] + ((((b[12] << 2) | (b[13] >> 6)) & 0x3fu) << shift) - bias);
s[15] = (ushort)(s[14] + ((b[13] & 0x3fu) << shift) - bias);
for (int i = 0; i < 16; ++i)
{
if ((s[i] & 0x8000) != 0)
{
s[i] &= 0x7fff;
}
else
{
s[i] = (ushort)~s[i];
}
}
}
// Unpack a 3-byte block into 4 by 4 identical 16-bit pixels.
private static void Unpack3(Span<byte> b, Span<ushort> s)
{
s[0] = (ushort)((b[0] << 8) | b[1]);
if ((s[0] & 0x8000) != 0)
{
s[0] &= 0x7fff;
}
else
{
s[0] = (ushort)~s[0];
}
for (int i = 1; i < 16; ++i)
{
s[i] = s[0];
}
}
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}

2
src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs → 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
{

16
src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs → 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<byte> tmpBuffer;
private readonly ushort[] s = new ushort[16];
public RunLengthCompression(MemoryAllocator allocator, uint uncompressedBytes)
: base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate<byte>((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;

8
src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs → 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<byte> 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)

6
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));
}

39
src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs

@ -57,12 +57,24 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals
/// </summary>
public Size Dimensions => new(this.Width, this.Height);
/// <summary>
/// Gets or sets the image width.
/// </summary>
private int Width { get; set; }
/// <summary>
/// Gets or sets the image height.
/// </summary>
private int Height { get; set; }
/// <summary>
/// Gets or sets the image channel info's.
/// </summary>
private IList<ExrChannelInfo> Channels { get; set; }
/// <summary>
/// Gets or sets the compression method.
/// </summary>
private ExrCompressionType Compression { get; set; }
private ExrImageDataType ImageDataType { get; set; }
@ -76,11 +88,15 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals
where TPixel : unmanaged, IPixel<TPixel>
{
this.ReadExrHeader(stream);
this.IsSupportedCompression();
if (!this.IsSupportedCompression())
{
ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported");
}
ExrPixelType pixelType = this.ValidateChannels();
this.ReadImageDataType();
Image<TPixel> image = new Image<TPixel>(this.configuration, this.Width, this.Height, this.metadata);
Image<TPixel> image = new (this.configuration, this.Width, this.Height, this.metadata);
Buffer2D<TPixel> 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<float> rowBuffer = this.memoryAllocator.Allocate<float>(width * 4);
using IMemoryOwner<byte> decompressedPixelDataBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock);
@ -118,7 +135,7 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals
Span<float> bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width);
Span<float> alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, 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<uint> rowBuffer = this.memoryAllocator.Allocate<uint>(width * 4);
using IMemoryOwner<byte> decompressedPixelDataBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock);
@ -173,7 +191,7 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals
Span<uint> bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width);
Span<uint> alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, 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()

13
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<uint> greenBuffer = rgbBuffer.GetSpan().Slice(width, width);
Span<uint> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
var rgb = default(Rgb96);
Rgb96 rgb = default;
for (int y = 0; y < height; y++)
{
Span<TPixel> 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)
{

8
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;
/// <summary>
@ -10,21 +8,15 @@ namespace SixLabors.ImageSharp.Formats.OpenExr;
/// </summary>
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);
}

10
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(ExrDecoder);
image.DebugSave(provider);
image.CompareToOriginal(provider);
}
}

1
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";
}
}

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

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