From 70cfcc9904b10d81c13d5d17c3e09ac3d0cc16f4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 3 Jan 2022 12:47:40 +0100 Subject: [PATCH] Add support for decoding EXR images with alpha channel --- .../Formats/OpenExr/ExrCompression.cs | 53 ++++- .../Formats/OpenExr/ExrConstants.cs | 14 ++ .../Formats/OpenExr/ExrDecoderCore.cs | 123 +++++++++--- .../Formats/OpenExr/ExrEncoderCore.cs | 6 +- .../PixelImplementations/Rgb96.cs | 8 +- .../PixelImplementations/Rgba128.cs | 183 ++++++++++++++++++ 6 files changed, 357 insertions(+), 30 deletions(-) create mode 100644 src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs diff --git a/src/ImageSharp/Formats/OpenExr/ExrCompression.cs b/src/ImageSharp/Formats/OpenExr/ExrCompression.cs index bcc177bde3..64df462ed3 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrCompression.cs @@ -5,6 +5,57 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { internal enum ExrCompression { - None = 0 + /// + /// 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/OpenExr/ExrConstants.cs b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs index 40c34cce23..079adec7ed 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs @@ -47,6 +47,9 @@ namespace SixLabors.ImageSharp.Formats.OpenExr public const string ScreenWindowWidth = "screenWindowWidth"; } + /// + /// EXR attribute types. + /// internal static class AttibuteTypes { public const string ChannelList = "chlist"; @@ -61,5 +64,16 @@ namespace SixLabors.ImageSharp.Formats.OpenExr 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"; + } } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index 053adbd074..4a86dbad82 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -74,9 +74,9 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { this.ReadExrHeader(stream); - if (this.Compression != ExrCompression.None) + if (this.Compression is not ExrCompression.None) { - ExrThrowHelper.ThrowNotSupported("Only uncompressed EXR images are supported"); + ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); } ExrPixelType pixelType = this.ValidateChannels(); @@ -104,10 +104,11 @@ namespace SixLabors.ImageSharp.Formats.OpenExr private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 3); + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); 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); TPixel color = default; for (int y = 0; y < this.Height; y++) @@ -125,12 +126,13 @@ namespace SixLabors.ImageSharp.Formats.OpenExr stream.Read(this.buffer, 0, 4); uint pixelDataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + bool hasAlpha = false; for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; switch (channel.ChannelName) { - case "R": + case ExrConstants.ChannelNames.Red: switch (channel.PixelType) { case ExrPixelType.Half: @@ -143,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr break; - case "B": + case ExrConstants.ChannelNames.Blue: switch (channel.PixelType) { case ExrPixelType.Half: @@ -156,7 +158,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr break; - case "G": + case ExrConstants.ChannelNames.Green: switch (channel.PixelType) { case ExrPixelType.Half: @@ -169,6 +171,20 @@ namespace SixLabors.ImageSharp.Formats.OpenExr break; + case ExrConstants.ChannelNames.Alpha: + hasAlpha = true; + switch (channel.PixelType) + { + case ExrPixelType.Half: + this.ReadPixelRowChannelHalfSingle(stream, alphaPixelData); + break; + case ExrPixelType.Float: + this.ReadPixelRowChannelSingle(stream, alphaPixelData); + break; + } + + break; + default: // Skip unknown channel. int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; @@ -179,11 +195,23 @@ namespace SixLabors.ImageSharp.Formats.OpenExr stream.Position = nextRowOffsetPosition; - for (int x = 0; x < this.Width; x++) + if (hasAlpha) + { + for (int x = 0; x < this.Width; x++) + { + var pixelValue = new HalfVector4(redPixelData[x], greenPixelData[x], bluePixelData[x], alphaPixelData[x]); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; + } + } + else { - var pixelValue = new HalfVector4(redPixelData[x], greenPixelData[x], bluePixelData[x], 1.0f); - color.FromVector4(pixelValue.ToVector4()); - pixelRow[x] = color; + for (int x = 0; x < this.Width; x++) + { + var pixelValue = new HalfVector4(redPixelData[x], greenPixelData[x], bluePixelData[x], 1.0f); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; + } } } } @@ -191,10 +219,11 @@ namespace SixLabors.ImageSharp.Formats.OpenExr private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 3); + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); 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); TPixel color = default; for (int y = 0; y < this.Height; y++) @@ -212,12 +241,13 @@ namespace SixLabors.ImageSharp.Formats.OpenExr stream.Read(this.buffer, 0, 4); uint pixelDataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + bool hasAlpha = false; for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; switch (channel.ChannelName) { - case "R": + case ExrConstants.ChannelNames.Red: switch (channel.PixelType) { case ExrPixelType.UnsignedInt: @@ -227,7 +257,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr break; - case "B": + case ExrConstants.ChannelNames.Blue: switch (channel.PixelType) { case ExrPixelType.UnsignedInt: @@ -237,7 +267,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr break; - case "G": + case ExrConstants.ChannelNames.Green: switch (channel.PixelType) { case ExrPixelType.UnsignedInt: @@ -247,6 +277,17 @@ namespace SixLabors.ImageSharp.Formats.OpenExr break; + case ExrConstants.ChannelNames.Alpha: + hasAlpha = true; + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + this.ReadPixelRowChannelUnsignedInt(stream, alphaPixelData); + break; + } + + break; + default: // Skip unknown channel. int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; @@ -257,11 +298,23 @@ namespace SixLabors.ImageSharp.Formats.OpenExr stream.Position = nextRowOffsetPosition; - for (int x = 0; x < this.Width; x++) + if (hasAlpha) { - var pixelValue = new Rgb96(redPixelData[x], greenPixelData[x], bluePixelData[x]); - color.FromVector4(pixelValue.ToVector4()); - pixelRow[x] = color; + for (int x = 0; x < this.Width; x++) + { + var pixelValue = new Rgba128(redPixelData[x], greenPixelData[x], bluePixelData[x], alphaPixelData[x]); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; + } + } + else + { + for (int x = 0; x < this.Width; x++) + { + var pixelValue = new Rgb96(redPixelData[x], greenPixelData[x], bluePixelData[x]); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; + } } } } @@ -324,19 +377,45 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { if (this.Channels.Count == 0) { - ExrThrowHelper.ThrowInvalidImageContentException("At least one channel of pixel data expected!"); + ExrThrowHelper.ThrowInvalidImageContentException("At least one channel of pixel data is expected!"); + } + + // Find pixel the type of any channel which is R, G, B or A. + ExrPixelType? pixelType = null; + for (int i = 0; i < this.Channels.Count; i++) + { + if (this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Blue) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Green) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Red) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Alpha)) + { + pixelType = this.Channels[i].PixelType; + break; + } } - ExrPixelType pixelType = this.Channels[0].PixelType; - for (int i = 1; i < this.Channels.Count; i++) + if (!pixelType.HasValue) { + ExrThrowHelper.ThrowInvalidImageContentException("Pixel channel data is unknown! Only R, G, B and A are supported."); + } + + for (int i = 0; i < this.Channels.Count; i++) + { + // Ignore channels which we cannot interpret. + if (!(this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Blue) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Green) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Red))) + { + continue; + } + if (pixelType != this.Channels[i].PixelType) { ExrThrowHelper.ThrowInvalidImageContentException("All channels are expected to have the same pixel data!"); } } - return pixelType; + return pixelType.Value; } private ExrHeader ReadExrHeader(BufferedReadStream stream) diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index 221877a063..1890d0b2b1 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -76,9 +76,9 @@ namespace SixLabors.ImageSharp.Formats.OpenExr ScreenWindowWidth = 1, Channels = new List() { - new("B", this.pixelType.Value, 0, 1, 1), - new("G", this.pixelType.Value, 0, 1, 1), - new("R", this.pixelType.Value, 0, 1, 1), + new(ExrConstants.ChannelNames.Blue, this.pixelType.Value, 0, 1, 1), + new(ExrConstants.ChannelNames.Green, this.pixelType.Value, 0, 1, 1), + new(ExrConstants.ChannelNames.Red, this.pixelType.Value, 0, 1, 1), } }; diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs index 83333a377e..2809a4c319 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs @@ -10,7 +10,7 @@ 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 + /// The color components are stored in red, green, blue. /// /// Ranges from [0, 0, 0] to [1, 1, 1] in vector form. /// @@ -23,17 +23,17 @@ namespace SixLabors.ImageSharp.PixelFormats private const double Max = uint.MaxValue; /// - /// Gets or sets the red component. + /// Gets the red component. /// public uint R; /// - /// Gets or sets the green component. + /// Gets the green component. /// public uint G; /// - /// Gets or sets the blue component. + /// Gets the blue component. /// public uint B; diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs new file mode 100644 index 0000000000..01fc16d45a --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs @@ -0,0 +1,183 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +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 + { + 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); + + /// + public PixelOperations CreatePixelOperations() => new(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromScaledVector4(Vector4 vector) => this.FromVector4(vector); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromVector4(Vector4 vector) + { + vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One); + this.R = (uint)(vector.X * Max); + this.G = (uint)(vector.Y * Max); + this.B = (uint)(vector.Z * Max); + this.A = (uint)(vector.W * Max); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToVector4() => new( + this.R * InvMax, + this.G * InvMax, + this.B * InvMax, + this.A * InvMax); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromBgra5551(Bgra5551 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromArgb32(Argb32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromBgr24(Bgr24 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromBgra32(Bgra32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromL8(L8 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromL16(L16 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromLa16(La16 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromLa32(La32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgb24(Rgb24 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgba32(Rgba32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromAbgr32(Abgr32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToRgba32(ref Rgba32 dest) => dest.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgb48(Rgb48 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgba64(Rgba64 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + public override bool Equals(object obj) => obj is Rgba128 other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Rgba128 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B) && this.A.Equals(other.A); + + /// + public override string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})"); + } +}