Browse Source

Add support for decoding EXR images with alpha channel

pull/3096/head
Brian Popow 5 years ago
parent
commit
70cfcc9904
  1. 53
      src/ImageSharp/Formats/OpenExr/ExrCompression.cs
  2. 14
      src/ImageSharp/Formats/OpenExr/ExrConstants.cs
  3. 123
      src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs
  4. 6
      src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs
  5. 8
      src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs
  6. 183
      src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs

53
src/ImageSharp/Formats/OpenExr/ExrCompression.cs

@ -5,6 +5,57 @@ namespace SixLabors.ImageSharp.Formats.OpenExr
{
internal enum ExrCompression
{
None = 0
/// <summary>
/// Pixel data is not compressed.
/// </summary>
None = 0,
/// <summary>
/// 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.
/// </summary>
RunLengthEncoded = 1,
/// <summary>
/// Uses the open source zlib library for compression. Unlike ZIP compression, this operates one scan line at a time.
/// Compression is lossless.
/// </summary>
Zips = 2,
/// <summary>
/// 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.
/// </summary>
Zip = 3,
/// <summary>
/// A wavelet transform is applied to the pixel data, and the result is Huffman-encoded.
/// Compression is lossless.
/// </summary>
Piz = 4,
/// <summary>
/// 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.
/// </summary>
Pxr24 = 5,
/// <summary>
/// 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.
/// </summary>
B44 = 6,
/// <summary>
/// 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.
/// </summary>
B44A = 7
}
}

14
src/ImageSharp/Formats/OpenExr/ExrConstants.cs

@ -47,6 +47,9 @@ namespace SixLabors.ImageSharp.Formats.OpenExr
public const string ScreenWindowWidth = "screenWindowWidth";
}
/// <summary>
/// EXR attribute types.
/// </summary>
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";
}
}
}

123
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<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<float> rowBuffer = this.memoryAllocator.Allocate<float>(this.Width * 3);
using IMemoryOwner<float> rowBuffer = this.memoryAllocator.Allocate<float>(this.Width * 4);
Span<float> redPixelData = rowBuffer.GetSpan().Slice(0, this.Width);
Span<float> greenPixelData = rowBuffer.GetSpan().Slice(this.Width, this.Width);
Span<float> bluePixelData = rowBuffer.GetSpan().Slice(this.Width * 2, this.Width);
Span<float> 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<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<uint> rowBuffer = this.memoryAllocator.Allocate<uint>(this.Width * 3);
using IMemoryOwner<uint> rowBuffer = this.memoryAllocator.Allocate<uint>(this.Width * 4);
Span<uint> redPixelData = rowBuffer.GetSpan().Slice(0, this.Width);
Span<uint> greenPixelData = rowBuffer.GetSpan().Slice(this.Width, this.Width);
Span<uint> bluePixelData = rowBuffer.GetSpan().Slice(this.Width * 2, this.Width);
Span<uint> 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)

6
src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs

@ -76,9 +76,9 @@ namespace SixLabors.ImageSharp.Formats.OpenExr
ScreenWindowWidth = 1,
Channels = new List<ExrChannelInfo>()
{
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),
}
};

8
src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.PixelFormats
{
/// <summary>
/// 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.
/// <para>
/// Ranges from [0, 0, 0] to [1, 1, 1] in vector form.
/// </para>
@ -23,17 +23,17 @@ namespace SixLabors.ImageSharp.PixelFormats
private const double Max = uint.MaxValue;
/// <summary>
/// Gets or sets the red component.
/// Gets the red component.
/// </summary>
public uint R;
/// <summary>
/// Gets or sets the green component.
/// Gets the green component.
/// </summary>
public uint G;
/// <summary>
/// Gets or sets the blue component.
/// Gets the blue component.
/// </summary>
public uint B;

183
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
{
/// <summary>
/// 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.
/// <para>
/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form.
/// </para>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public partial struct Rgba128 : IPixel<Rgba128>
{
private const float InvMax = 1.0f / uint.MaxValue;
private const double Max = uint.MaxValue;
/// <summary>
/// Gets the red component.
/// </summary>
public uint R;
/// <summary>
/// Gets the green component.
/// </summary>
public uint G;
/// <summary>
/// Gets the blue component.
/// </summary>
public uint B;
/// <summary>
/// Gets the alpha channel.
/// </summary>
public uint A;
/// <summary>
/// Initializes a new instance of the <see cref="Rgba128"/> struct.
/// </summary>
/// <param name="r">The red component.</param>
/// <param name="g">The green component.</param>
/// <param name="b">The blue component.</param>
/// <param name="a">The alpha component.</param>
[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;
}
/// <summary>
/// Compares two <see cref="Rgba128"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Rgba128"/> on the left side of the operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
/// <param name="right">The <see cref="Rgba128"/> on the right side of the operand.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Rgba128 left, Rgba128 right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Rgba128"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Rgba128"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Rgba128"/> on the right side of the operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is not equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Rgba128 left, Rgba128 right) => !left.Equals(right);
/// <inheritdoc />
public PixelOperations<Rgba128> CreatePixelOperations() => new();
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromScaledVector4(Vector4 vector) => this.FromVector4(vector);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToScaledVector4() => this.ToVector4();
/// <inheritdoc />
[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);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToVector4() => new(
this.R * InvMax,
this.G * InvMax,
this.B * InvMax,
this.A * InvMax);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromBgra5551(Bgra5551 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromArgb32(Argb32 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromBgr24(Bgr24 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromBgra32(Bgra32 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromL8(L8 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromL16(L16 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromLa16(La16 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromLa32(La32 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromRgb24(Rgb24 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromRgba32(Rgba32 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromAbgr32(Abgr32 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ToRgba32(ref Rgba32 dest) => dest.FromScaledVector4(this.ToScaledVector4());
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromRgb48(Rgb48 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FromRgba64(Rgba64 source) => this.FromScaledVector4(source.ToScaledVector4());
/// <inheritdoc />
public override bool Equals(object obj) => obj is Rgba128 other && this.Equals(other);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A);
/// <inheritdoc />
[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);
/// <inheritdoc />
public override string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})");
}
}
Loading…
Cancel
Save