From aaf21434a82dc878981571d2461c7e430e072762 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 29 Dec 2021 11:02:43 +0100 Subject: [PATCH] Decode EXR images with unsigned int pixel data --- .../Formats/ImageDecoderUtilities.cs | 2 +- src/ImageSharp/Formats/OpenExr/ExrBox2i.cs | 16 +- .../Formats/OpenExr/ExrDecoderCore.cs | 174 +++++++++++++++--- .../Formats/OpenExr/ExrPixelType.cs | 4 +- .../Formats/OpenExr/ExrThrowHelper.cs | 3 + .../PixelImplementations/Rgb96.cs | 172 +++++++++++++++++ 6 files changed, 333 insertions(+), 38 deletions(-) create mode 100644 src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs index 5d77fb0c8c..49e783ee46 100644 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs @@ -170,6 +170,6 @@ namespace SixLabors.ImageSharp.Formats private static InvalidImageContentException DefaultLargeImageExceptionFactory( InvalidMemoryOperationException memoryOperationException, Size dimensions) => - new InvalidImageContentException(dimensions, memoryOperationException); + new(dimensions, memoryOperationException); } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs b/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs index c45dc087ed..6fd610026b 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs @@ -7,18 +7,18 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { public ExrBox2i(int xMin, int yMin, int xMax, int yMax) { - this.xMin = xMin; - this.yMin = yMin; - this.xMax = xMax; - this.yMax = yMax; + this.XMin = xMin; + this.YMin = yMin; + this.XMax = xMax; + this.YMax = yMax; } - public int xMin { get; } + public int XMin { get; } - public int yMin { get; } + public int YMin { get; } - public int xMax { get; } + public int XMax { get; } - public int yMax { get; } + public int YMax { get; } } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index bbd7636a9e..79b117b736 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -72,19 +72,35 @@ namespace SixLabors.ImageSharp.Formats.OpenExr public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - ExrHeader header = this.ReadExrHeader(stream); + this.ReadExrHeader(stream); if (this.Compression != ExrCompression.None) { ExrThrowHelper.ThrowNotSupported("Only uncompressed EXR images are supported"); } - int bitsPerPixel = CalculateBitsPerPixel(header.Channels); + ExrPixelType pixelType = this.ValidateChannels(); var image = new Image(this.Configuration, this.Width, this.Height, this.metadata); - Buffer2D pixels = image.GetRootFramePixelBuffer(); + switch (pixelType) + { + case ExrPixelType.Half: + case ExrPixelType.Float: + this.DecodeFloatingPointPixelData(stream, pixels); + break; + default: + ExrThrowHelper.ThrowNotSupported("Pixel type is not supported"); + break; + } + + return image; + } + + private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels) + where TPixel : unmanaged, IPixel + { using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 3); Span redPixelData = rowBuffer.GetSpan().Slice(0, this.Width); Span bluePixelData = rowBuffer.GetSpan().Slice(this.Width, this.Width); @@ -152,7 +168,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr default: // Skip unknown channel. - int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.Uint ? 4 : 2; + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; stream.Position += this.Width * channelDataSizeInBytes; break; } @@ -167,8 +183,84 @@ namespace SixLabors.ImageSharp.Formats.OpenExr pixelRow[x] = color; } } + } - return image; + private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 3); + Span redPixelData = rowBuffer.GetSpan().Slice(0, this.Width); + Span bluePixelData = rowBuffer.GetSpan().Slice(this.Width, this.Width); + Span greenPixelData = rowBuffer.GetSpan().Slice(this.Width * 2, this.Width); + + TPixel color = default; + for (int y = 0; y < this.Height; y++) + { + stream.Read(this.buffer, 0, 8); + ulong rowOffset = BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); + long nextRowOffsetPosition = stream.Position; + + stream.Position = (long)rowOffset; + stream.Read(this.buffer, 0, 4); + uint rowIndex = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + + stream.Read(this.buffer, 0, 4); + uint pixelDataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) + { + ExrChannelInfo channel = this.Channels[channelIdx]; + switch (channel.ChannelName) + { + case "R": + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + this.ReadPixelRowChannelUnsignedInt(stream, redPixelData); + break; + } + + break; + + case "B": + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + this.ReadPixelRowChannelUnsignedInt(stream, bluePixelData); + break; + } + + break; + + case "G": + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + this.ReadPixelRowChannelUnsignedInt(stream, greenPixelData); + break; + } + + break; + + default: + // Skip unknown channel. + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; + stream.Position += this.Width * channelDataSizeInBytes; + break; + } + } + + stream.Position = nextRowOffsetPosition; + + 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; + } + } } private void ReadPixelRowChannelHalfSingle(BufferedReadStream stream, Span channelData) @@ -187,16 +279,63 @@ namespace SixLabors.ImageSharp.Formats.OpenExr } } + private void ReadPixelRowChannelUnsignedInt(BufferedReadStream stream, Span channelData) + { + for (int x = 0; x < this.Width; x++) + { + stream.Read(this.buffer, 0, 4); + channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + } + } + /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { ExrHeader header = this.ReadExrHeader(stream); - int bitsPerPixel = CalculateBitsPerPixel(header.Channels); + int bitsPerPixel = this.CalculateBitsPerPixel(); return new ImageInfo(new PixelTypeInfo(bitsPerPixel), this.Width, this.Height, new ImageMetadata()); } + private int CalculateBitsPerPixel() + { + int bitsPerPixel = 0; + for (int i = 0; i < this.Channels.Count; i++) + { + ExrChannelInfo channel = this.Channels[0]; + if (channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt) + { + bitsPerPixel += 32; + } + else if (channel.PixelType == ExrPixelType.Half) + { + bitsPerPixel += 16; + } + } + + return bitsPerPixel; + } + + private ExrPixelType ValidateChannels() + { + if (this.Channels.Count == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("At least one channel of pixel data expected!"); + } + + ExrPixelType pixelType = this.Channels[0].PixelType; + for (int i = 1; i < this.Channels.Count; i++) + { + if (pixelType != this.Channels[i].PixelType) + { + ExrThrowHelper.ThrowInvalidImageContentException("All channels are expected to have the same pixel data!"); + } + } + + return pixelType; + } + private ExrHeader ReadExrHeader(BufferedReadStream stream) { // Skip over the magick bytes. @@ -220,8 +359,8 @@ namespace SixLabors.ImageSharp.Formats.OpenExr ExrThrowHelper.ThrowInvalidImageHeader(); } - this.Width = header.DataWindow.Value.xMax - header.DataWindow.Value.xMin + 1; - this.Height = header.DataWindow.Value.yMax - header.DataWindow.Value.yMin + 1; + this.Width = header.DataWindow.Value.XMax - header.DataWindow.Value.XMin + 1; + this.Height = header.DataWindow.Value.YMax - header.DataWindow.Value.YMin + 1; this.Channels = header.Channels; this.Compression = header.Compression.GetValueOrDefault(); @@ -358,25 +497,6 @@ namespace SixLabors.ImageSharp.Formats.OpenExr return new ExrChannelInfo(channelName, pixelType, pLinear, xSampling, ySampling); } - private static int CalculateBitsPerPixel(IList channels) - { - int bitsPerPixel = 0; - for (int i = 0; i < channels.Count; i++) - { - ExrChannelInfo channel = channels[0]; - if (channel.PixelType is ExrPixelType.Float or ExrPixelType.Uint) - { - bitsPerPixel += 32; - } - else if (channel.PixelType == ExrPixelType.Half) - { - bitsPerPixel += 16; - } - } - - return bitsPerPixel; - } - private static string ReadString(BufferedReadStream stream) { var str = new StringBuilder(); diff --git a/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs b/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs index 4e3427cf43..fc3dc8ef9c 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs @@ -3,12 +3,12 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { - internal enum ExrPixelType : int + internal enum ExrPixelType { /// /// unsigned int (32 bit). /// - Uint = 0, + UnsignedInt = 0, /// /// half (16 bit floating point). diff --git a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs index 227a961188..ae76541117 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(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"); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs new file mode 100644 index 0000000000..405ed40a14 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs @@ -0,0 +1,172 @@ +// 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 three 32-bit unsigned normalized values ranging from 0 to 4294967295. + /// The color components are stored in red, green, blue + /// + /// Ranges from [0, 0, 0] to [1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public partial struct Rgb96 : IPixel + { + private const float Max = uint.MaxValue; + + /// + /// Gets or sets the red component. + /// + public uint R; + + /// + /// Gets or sets the green component. + /// + public uint G; + + /// + /// Gets or sets the blue component. + /// + public uint B; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgb96(uint r, uint g, uint b) + { + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// 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 ==(Rgb96 left, Rgb96 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 !=(Rgb96 left, Rgb96 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) + { + this.R = (uint)(vector.X * Max); + this.G = (uint)(vector.Y * Max); + this.B = (uint)(vector.Z * Max); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToVector4() => new( + this.R / Max, + this.G / Max, + this.B / Max, + 1.0f); + + /// + [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 Rgb96 other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); + + /// + public override string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})"); + } +}