diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index c5c9425066..ea40857d8d 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; using System.Text; @@ -22,7 +23,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr /// /// Reusable buffer. /// - private readonly byte[] buffer = new byte[4]; + private readonly byte[] buffer = new byte[8]; /// /// Used for allocating memory during processing operations. @@ -34,6 +35,11 @@ namespace SixLabors.ImageSharp.Formats.OpenExr /// private readonly IExrDecoderOptions options; + /// + /// The metadata. + /// + private ImageMetadata metadata; + /// /// Initializes a new instance of the class. /// @@ -58,12 +64,102 @@ namespace SixLabors.ImageSharp.Formats.OpenExr private int Height { get; set; } + private IList Channels { get; set; } + /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel => throw new NotImplementedException(); + where TPixel : unmanaged, IPixel + { + ExrHeader header = this.ReadExrHeader(stream); + + int bitsPerPixel = CalculateBitsPerPixel(header.Channels); + + var image = new Image(this.Configuration, this.Width, this.Height, this.metadata); + + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + // TODO: we for now assume the image pixel type is HalfSingle. + 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++) + { + switch (this.Channels[channelIdx].ChannelName) + { + case "R": + for (int x = 0; x < this.Width; x++) + { + redPixelData[x] = stream.ReadHalfSingle(this.buffer); + } + + break; + + case "B": + for (int x = 0; x < this.Width; x++) + { + bluePixelData[x] = stream.ReadHalfSingle(this.buffer); + } + + break; + + case "G": + for (int x = 0; x < this.Width; x++) + { + greenPixelData[x] = stream.ReadHalfSingle(this.buffer); + } + + break; + + default: + // Skip unknown channel. + // TODO: can we assume the same data size as the others here? + stream.Position += this.Width * 2; + break; + } + } + + stream.Position = nextRowOffsetPosition; + + 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; + } + } + + return image; + } /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + ExrHeader header = this.ReadExrHeader(stream); + + int bitsPerPixel = CalculateBitsPerPixel(header.Channels); + + return new ImageInfo(new PixelTypeInfo(bitsPerPixel), this.Width, this.Height, new ImageMetadata()); + } + + private ExrHeader ReadExrHeader(BufferedReadStream stream) { // Skip over the magick bytes. stream.Read(this.buffer, 0, 4); @@ -93,11 +189,11 @@ namespace SixLabors.ImageSharp.Formats.OpenExr 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; - // TODO: calculate bits per pixel. - int bitsPerPixel = 48; + this.metadata = new ImageMetadata(); - return new ImageInfo(new PixelTypeInfo(bitsPerPixel), this.Width, this.Height, new ImageMetadata()); + return header; } private ExrHeader ParseHeader(BufferedReadStream stream) @@ -110,11 +206,16 @@ namespace SixLabors.ImageSharp.Formats.OpenExr switch (attribute.Name) { case "channels": - IList channels = this.ParseChannelList(stream, attribute.Length); + IList channels = this.ReadChannelList(stream, attribute.Length); header.Channels = channels; break; case "compression": var compression = (ExrCompression)stream.ReadByte(); + if (compression != ExrCompression.None) + { + ExrThrowHelper.ThrowNotSupported("Only uncompressed EXR images are supported"); + } + header.Compression = compression; break; case "dataWindow": @@ -187,7 +288,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr return new ExrBox2i(xMin, yMin, xMax, yMax); } - private List ParseChannelList(BufferedReadStream stream, int attributeSize) + private List ReadChannelList(BufferedReadStream stream, int attributeSize) { var channels = new List(); while (attributeSize > 1) @@ -229,6 +330,25 @@ 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/Pbm/BufferedReadStreamExtensions.cs b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs index 5aa6430d1c..95095edc6e 100644 --- a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs +++ b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.IO; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Pbm { @@ -66,12 +67,12 @@ namespace SixLabors.ImageSharp.Formats.Pbm } /// - /// Reads a float. + /// Reads a 32 bit float. /// /// The stream to read from. /// A scratch buffer of size 4 bytes. /// The byte order. Defaults to little endian. - /// the value. + /// the float value. public static float ReadSingle(this BufferedReadStream stream, Span scratch, ByteOrder byteOrder = ByteOrder.LittleEndian) { stream.Read(scratch, 0, 4); @@ -87,5 +88,28 @@ namespace SixLabors.ImageSharp.Formats.Pbm return Unsafe.As(ref intValue); } + + /// + /// Reads a 16 bit float. + /// + /// The stream to read from. + /// A scratch buffer of size 2 bytes. + /// The byte order. Defaults to little endian. + /// The float value. + public static float ReadHalfSingle(this BufferedReadStream stream, Span scratch, ByteOrder byteOrder = ByteOrder.LittleEndian) + { + stream.Read(scratch, 0, 2); + ushort shortValue; + if (byteOrder == ByteOrder.LittleEndian) + { + shortValue = BinaryPrimitives.ReadUInt16LittleEndian(scratch); + } + else + { + shortValue = BinaryPrimitives.ReadUInt16BigEndian(scratch); + } + + return HalfTypeHelper.Unpack(shortValue); + } } }