diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs new file mode 100644 index 0000000000..d4eaf67ebb --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors +{ + internal class NoneExrCompression : ExrBaseDecompressor + { + public NoneExrCompression(MemoryAllocator allocator, uint uncompressedBytes) + : base(allocator, uncompressedBytes) + { + } + + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + => stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.UncompressedBytes)); + + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs new file mode 100644 index 0000000000..3f3db53279 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO.Compression; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors +{ + internal class ZipsExrCompression : ExrBaseDecompressor + { + private readonly IMemoryOwner tmpBuffer; + + public ZipsExrCompression(MemoryAllocator allocator, uint uncompressedBytes) + : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); + + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + long pos = stream.Position; + using var deframeStream = new ZlibInflateStream( + stream, + () => + { + int left = (int)(compressedBytes - (stream.Position - pos)); + return left > 0 ? left : 0; + }); + deframeStream.AllocateNewBytes((int)this.UncompressedBytes, true); + DeflateStream dataStream = deframeStream.CompressedStream; + + Span tmp = this.tmpBuffer.GetSpan(); + int totalRead = 0; + while (totalRead < buffer.Length) + { + int bytesRead = dataStream.Read(tmp, totalRead, buffer.Length - totalRead); + if (bytesRead <= 0) + { + break; + } + + totalRead += bytesRead; + } + + Reconstruct(tmp, this.UncompressedBytes); + Interleave(tmp, this.UncompressedBytes, buffer); + } + + private 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++; + } + } + + private 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++]; + } + } + + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs new file mode 100644 index 0000000000..7faf4e77d6 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +{ + internal abstract class ExrBaseCompression : IDisposable + { + private bool isDisposed; + + protected ExrBaseCompression(MemoryAllocator allocator, uint bytePerRow) + { + this.Allocator = allocator; + this.UncompressedBytes = bytePerRow; + } + + /// + /// Gets the memory allocator. + /// + protected MemoryAllocator Allocator { get; } + + /// + /// Gets the uncompressed bytes. + /// + public uint UncompressedBytes { 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/OpenExr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs new file mode 100644 index 0000000000..1eb31ff528 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +{ + internal abstract class ExrBaseDecompressor : ExrBaseCompression + { + protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytePerRow) + : base(allocator, bytePerRow) + { + } + + public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs similarity index 96% rename from src/ImageSharp/Formats/OpenExr/ExrCompression.cs rename to src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs index 64df462ed3..3b47ed6ecb 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression { - internal enum ExrCompression + internal enum ExrCompressionType { /// /// Pixel data is not compressed. diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs new file mode 100644 index 0000000000..7ce46b4af5 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +{ + internal static class ExrDecompressorFactory + { + public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint bytesPerRow) + { + switch (method) + { + case ExrCompressionType.None: + return new NoneExrCompression(memoryAllocator, bytesPerRow); + case ExrCompressionType.Zips: + return new ZipsExrCompression(memoryAllocator, bytesPerRow); + default: + throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); + } + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index 4a86dbad82..b5cae391df 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -5,8 +5,10 @@ using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; +using SixLabors.ImageSharp.Formats.OpenExr.Compression; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -66,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr private IList Channels { get; set; } - private ExrCompression Compression { get; set; } + private ExrCompressionType Compression { get; set; } /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) @@ -74,7 +76,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { this.ReadExrHeader(stream); - if (this.Compression is not ExrCompression.None) + if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips) { ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); } @@ -104,12 +106,19 @@ namespace SixLabors.ImageSharp.Formats.OpenExr private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { + bool hasAlpha = this.HasAlpha(); + uint bytesPerRow = this.CalculateBytesPerRow(); + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerRow); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); Span redPixelData = rowBuffer.GetSpan().Slice(0, this.Width); Span greenPixelData = rowBuffer.GetSpan().Slice(this.Width, this.Width); Span bluePixelData = rowBuffer.GetSpan().Slice(this.Width * 2, this.Width); Span alphaPixelData = rowBuffer.GetSpan().Slice(this.Width * 3, this.Width); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerRow); + TPixel color = default; for (int y = 0; y < this.Height; y++) { @@ -124,9 +133,10 @@ namespace SixLabors.ImageSharp.Formats.OpenExr Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); stream.Read(this.buffer, 0, 4); - uint pixelDataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + uint compressedBytesCount = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); - bool hasAlpha = false; + int offset = 0; for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; @@ -136,10 +146,10 @@ namespace SixLabors.ImageSharp.Formats.OpenExr switch (channel.PixelType) { case ExrPixelType.Half: - this.ReadPixelRowChannelHalfSingle(stream, redPixelData); + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), redPixelData); break; case ExrPixelType.Float: - this.ReadPixelRowChannelSingle(stream, redPixelData); + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), redPixelData); break; } @@ -149,10 +159,10 @@ namespace SixLabors.ImageSharp.Formats.OpenExr switch (channel.PixelType) { case ExrPixelType.Half: - this.ReadPixelRowChannelHalfSingle(stream, bluePixelData); + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), bluePixelData); break; case ExrPixelType.Float: - this.ReadPixelRowChannelSingle(stream, bluePixelData); + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), bluePixelData); break; } @@ -162,24 +172,23 @@ namespace SixLabors.ImageSharp.Formats.OpenExr switch (channel.PixelType) { case ExrPixelType.Half: - this.ReadPixelRowChannelHalfSingle(stream, greenPixelData); + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), greenPixelData); break; case ExrPixelType.Float: - this.ReadPixelRowChannelSingle(stream, greenPixelData); + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), greenPixelData); break; } break; case ExrConstants.ChannelNames.Alpha: - hasAlpha = true; switch (channel.PixelType) { case ExrPixelType.Half: - this.ReadPixelRowChannelHalfSingle(stream, alphaPixelData); + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), alphaPixelData); break; case ExrPixelType.Float: - this.ReadPixelRowChannelSingle(stream, alphaPixelData); + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), alphaPixelData); break; } @@ -219,12 +228,19 @@ namespace SixLabors.ImageSharp.Formats.OpenExr private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { + bool hasAlpha = this.HasAlpha(); + uint bytesPerRow = this.CalculateBytesPerRow(); + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerRow); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); Span redPixelData = rowBuffer.GetSpan().Slice(0, this.Width); Span greenPixelData = rowBuffer.GetSpan().Slice(this.Width, this.Width); Span bluePixelData = rowBuffer.GetSpan().Slice(this.Width * 2, this.Width); Span alphaPixelData = rowBuffer.GetSpan().Slice(this.Width * 3, this.Width); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerRow); + TPixel color = default; for (int y = 0; y < this.Height; y++) { @@ -239,9 +255,10 @@ namespace SixLabors.ImageSharp.Formats.OpenExr Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); stream.Read(this.buffer, 0, 4); - uint pixelDataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + uint compressedBytesCount = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); - bool hasAlpha = false; + int offset = 0; for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; @@ -251,7 +268,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr switch (channel.PixelType) { case ExrPixelType.UnsignedInt: - this.ReadPixelRowChannelUnsignedInt(stream, redPixelData); + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), redPixelData); break; } @@ -261,7 +278,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr switch (channel.PixelType) { case ExrPixelType.UnsignedInt: - this.ReadPixelRowChannelUnsignedInt(stream, bluePixelData); + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), bluePixelData); break; } @@ -271,18 +288,17 @@ namespace SixLabors.ImageSharp.Formats.OpenExr switch (channel.PixelType) { case ExrPixelType.UnsignedInt: - this.ReadPixelRowChannelUnsignedInt(stream, greenPixelData); + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), greenPixelData); break; } break; case ExrConstants.ChannelNames.Alpha: - hasAlpha = true; switch (channel.PixelType) { case ExrPixelType.UnsignedInt: - this.ReadPixelRowChannelUnsignedInt(stream, alphaPixelData); + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), alphaPixelData); break; } @@ -319,29 +335,44 @@ namespace SixLabors.ImageSharp.Formats.OpenExr } } - private void ReadPixelRowChannelHalfSingle(BufferedReadStream stream, Span channelData) + private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData) { + int offset = 0; + ushort shortValue = 0; for (int x = 0; x < this.Width; x++) { - channelData[x] = stream.ReadHalfSingle(this.buffer); + shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2)); + channelData[x] = HalfTypeHelper.Unpack(shortValue); + offset += 2; } + + return offset; } - private void ReadPixelRowChannelSingle(BufferedReadStream stream, Span channelData) + private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData) { + int offset = 0; + int intValue = 0; for (int x = 0; x < this.Width; x++) { - channelData[x] = stream.ReadSingle(this.buffer); + intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + channelData[x] = Unsafe.As(ref intValue); + offset += 4; } + + return offset; } - private void ReadPixelRowChannelUnsignedInt(BufferedReadStream stream, Span channelData) + private int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Span channelData) { + int offset = 0; for (int x = 0; x < this.Width; x++) { - stream.Read(this.buffer, 0, 4); - channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + offset += 4; } + + return offset; } /// @@ -465,7 +496,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr header.Channels = channels; break; case ExrConstants.AttributeNames.Compression: - header.Compression = (ExrCompression)stream.ReadByte(); + header.Compression = (ExrCompressionType)stream.ReadByte(); break; case ExrConstants.AttributeNames.DataWindow: ExrBox2i dataWindow = this.ReadBoxInteger(stream); @@ -602,5 +633,39 @@ namespace SixLabors.ImageSharp.Formats.OpenExr return str.ToString(); } + + private bool HasAlpha() + { + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A")) + { + return true; + } + } + + return false; + } + + private uint CalculateBytesPerRow() + { + uint bytesPerRow = 0; + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A") || channelInfo.ChannelName.Equals("R") || channelInfo.ChannelName.Equals("G") || channelInfo.ChannelName.Equals("B")) + { + if (channelInfo.PixelType == ExrPixelType.Half) + { + bytesPerRow += 2 * (uint)this.Width; + } + else + { + bytesPerRow += 4 * (uint)this.Width; + } + } + } + + return bytesPerRow; + } } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index 1890d0b2b1..63c171c354 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Threading; +using SixLabors.ImageSharp.Formats.OpenExr.Compression; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -67,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr int height = image.Height; var header = new ExrHeader() { - Compression = ExrCompression.None, + Compression = ExrCompressionType.None, AspectRatio = 1.0f, DataWindow = new ExrBox2i(0, 0, width - 1, height - 1), DisplayWindow = new ExrBox2i(0, 0, width - 1, height - 1), @@ -293,7 +294,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr stream.WriteByte(0); } - private void WriteCompression(Stream stream, ExrCompression compression) + private void WriteCompression(Stream stream, ExrCompressionType compression) { this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1); stream.WriteByte((byte)compression); diff --git a/src/ImageSharp/Formats/OpenExr/ExrHeader.cs b/src/ImageSharp/Formats/OpenExr/ExrHeader.cs index 1ed464ebd2..a463abf72b 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrHeader.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrHeader.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; +using SixLabors.ImageSharp.Formats.OpenExr.Compression; namespace SixLabors.ImageSharp.Formats.OpenExr { @@ -9,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { public IList Channels { get; set; } - public ExrCompression? Compression { get; set; } + public ExrCompressionType? Compression { get; set; } public ExrBox2i? DataWindow { get; set; } diff --git a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs index ae76541117..b68fb5a2f4 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs @@ -11,6 +11,9 @@ 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);