diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index ca83b0764e..a3e0c99489 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.OpenExr; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; @@ -124,7 +125,7 @@ namespace SixLabors.ImageSharp /// /// Gets or sets the that is currently in use. /// - public ImageFormatManager ImageFormatsManager { get; set; } = new ImageFormatManager(); + public ImageFormatManager ImageFormatsManager { get; set; } = new(); /// /// Gets or sets the that is currently in use. @@ -192,7 +193,7 @@ namespace SixLabors.ImageSharp /// Creates a shallow copy of the . /// /// A new configuration instance. - public Configuration Clone() => new Configuration + public Configuration Clone() => new() { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism, StreamProcessingBufferSize = this.StreamProcessingBufferSize, @@ -214,9 +215,10 @@ namespace SixLabors.ImageSharp /// . /// . /// . + /// . /// /// The default configuration of . - internal static Configuration CreateDefaultInstance() => new Configuration( + internal static Configuration CreateDefaultInstance() => new( new PngConfigurationModule(), new JpegConfigurationModule(), new GifConfigurationModule(), @@ -224,6 +226,7 @@ namespace SixLabors.ImageSharp new PbmConfigurationModule(), new TgaConfigurationModule(), new TiffConfigurationModule(), - new WebpConfigurationModule()); + new WebpConfigurationModule(), + new ExrConfigurationModule()); } } diff --git a/src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs b/src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs index 2338572476..d68841ad1f 100644 --- a/src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs @@ -45,9 +45,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public short ScreenHeight { get; } - public static BmpArrayFileHeader Parse(Span data) - { - return MemoryMarshal.Cast(data)[0]; - } + public static BmpArrayFileHeader Parse(Span data) => MemoryMarshal.Cast(data)[0]; } } diff --git a/src/ImageSharp/Formats/Bmp/BmpConstants.cs b/src/ImageSharp/Formats/Bmp/BmpConstants.cs index 0b9499eeb3..4684406b29 100644 --- a/src/ImageSharp/Formats/Bmp/BmpConstants.cs +++ b/src/ImageSharp/Formats/Bmp/BmpConstants.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace SixLabors.ImageSharp.Formats.Bmp { /// - /// Defines constants relating to BMPs + /// Defines constants relating to BMP images. /// internal static class BmpConstants { diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index 129b3a1aa0..4c433b203a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -4,8 +4,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Bmp diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 41adc1cfff..fe31a686bf 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Gets the dimensions of the image. /// - public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height); + public Size Dimensions => new(this.infoHeader.Width, this.infoHeader.Height); /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) diff --git a/src/ImageSharp/Formats/Bmp/BmpFormat.cs b/src/ImageSharp/Formats/Bmp/BmpFormat.cs index d92a73104e..2b24c43879 100644 --- a/src/ImageSharp/Formats/Bmp/BmpFormat.cs +++ b/src/ImageSharp/Formats/Bmp/BmpFormat.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Gets the current instance. /// - public static BmpFormat Instance { get; } = new BmpFormat(); + public static BmpFormat Instance { get; } = new(); /// public string Name => "BMP"; @@ -32,6 +32,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp public IEnumerable FileExtensions => BmpConstants.FileExtensions; /// - public BmpMetadata CreateDefaultFormatMetadata() => new BmpMetadata(); + public BmpMetadata CreateDefaultFormatMetadata() => new(); } } diff --git a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs index b380486a3f..82376a30cf 100644 --- a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs @@ -15,10 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp public int HeaderSize => 2; /// - public IImageFormat DetectFormat(ReadOnlySpan header) - { - return this.IsSupportedFileFormat(header) ? BmpFormat.Instance : null; - } + public IImageFormat DetectFormat(ReadOnlySpan header) => this.IsSupportedFileFormat(header) ? BmpFormat.Instance : null; private bool IsSupportedFileFormat(ReadOnlySpan header) { diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 0d0c05c9f4..6cd9d19f5d 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -279,15 +279,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseCore(ReadOnlySpan data) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseCore(ReadOnlySpan data) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(4, 2)), height: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(6, 2)), planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(8, 2)), bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(10, 2))); - } /// /// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height @@ -296,15 +293,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2))); - } /// /// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes). @@ -312,9 +306,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseV3(ReadOnlySpan data) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseV3(ReadOnlySpan data) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), @@ -326,7 +318,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4))); - } /// /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. @@ -336,9 +327,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Indicates, if the alpha bitmask is present. /// The parsed header. /// - public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), @@ -354,7 +343,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0); - } /// /// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are diff --git a/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs b/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs new file mode 100644 index 0000000000..66423f50dc --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + [DebuggerDisplay("Name: {Name}, Type: {Type}, Length: {Length}")] + internal class ExrAttribute + { + public static readonly ExrAttribute EmptyAttribute = new(string.Empty, string.Empty, 0); + + public ExrAttribute(string name, string type, int length) + { + this.Name = name; + this.Type = type; + this.Length = length; + } + + public string Name { get; } + + public string Type { get; } + + public int Length { get; } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs b/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs new file mode 100644 index 0000000000..c45dc087ed --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + internal struct ExrBox2i + { + public ExrBox2i(int xMin, int yMin, int xMax, int yMax) + { + this.xMin = xMin; + this.yMin = yMin; + this.xMax = xMax; + this.yMax = yMax; + } + + public int xMin { get; } + + public int yMin { get; } + + public int xMax { get; } + + public int yMax { get; } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs b/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs new file mode 100644 index 0000000000..81b40ea66f --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct ExrChannelInfo + { + public ExrChannelInfo(string channelName, ExrPixelType pixelType, byte pLinear, int xSampling, int ySampling) + { + this.ChannelName = channelName; + this.PixelType = pixelType; + this.PLinear = pLinear; + this.XSampling = xSampling; + this.YSampling = ySampling; + } + + public string ChannelName { get; } + + public ExrPixelType PixelType { get; } + + public byte PLinear { get; } + + public int XSampling { get; } + + public int YSampling { get; } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrCompression.cs b/src/ImageSharp/Formats/OpenExr/ExrCompression.cs new file mode 100644 index 0000000000..3fa8cadfd9 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrCompression.cs @@ -0,0 +1,10 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + internal enum ExrCompression : int + { + None = 0 + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs b/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs new file mode 100644 index 0000000000..ff0d3ee6a4 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Registers the image encoders, decoders and mime type detectors for the OpenExr format. + /// + public sealed class ExrConfigurationModule : IConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, new ExrDecoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new ExrImageFormatDetector()); + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs new file mode 100644 index 0000000000..b3f1a34245 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Defines constants relating to OpenExr images. + /// + internal static class ExrConstants + { + /// + /// The list of mimetypes that equate to a OpenExr image. + /// + public static readonly IEnumerable MimeTypes = new[] { "image/x-exr" }; + + /// + /// The list of file extensions that equate to a OpenExr image. + /// + public static readonly IEnumerable FileExtensions = new[] { "exr" }; + + /// + /// The magick bytes identifying an OpenExr image. + /// + public static readonly int MagickBytes = 20000630; + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs new file mode 100644 index 0000000000..0c645d71e1 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Image decoder for generating an image out of a OpenExr stream. + /// + public sealed class ExrDecoder : IImageDecoder, IExrDecoderOptions, IImageInfoDetector + { + /// + public Image Decode(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + var decoder = new ExrDecoderCore(configuration, this); + return decoder.Decode(configuration, stream); + } + + /// + public Image Decode(Configuration configuration, Stream stream) + => this.Decode(configuration, stream); + + /// + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + var decoder = new ExrDecoderCore(configuration, this); + return decoder.DecodeAsync(configuration, stream, cancellationToken); + } + + /// + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + + /// + public IImageInfo Identify(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + return new ExrDecoderCore(configuration, this).Identify(configuration, stream); + } + + /// + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(stream, nameof(stream)); + + return new ExrDecoderCore(configuration, this).IdentifyAsync(configuration, stream, cancellationToken); + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs new file mode 100644 index 0000000000..c5c9425066 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -0,0 +1,256 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using SixLabors.ImageSharp.Formats.Pbm; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Performs the OpenExr decoding operation. + /// + internal sealed class ExrDecoderCore : IImageDecoderInternals + { + /// + /// Reusable buffer. + /// + private readonly byte[] buffer = new byte[4]; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The bitmap decoder options. + /// + private readonly IExrDecoderOptions options; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The options. + public ExrDecoderCore(Configuration configuration, IExrDecoderOptions options) + { + this.Configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + this.options = options; + } + + /// + public Configuration Configuration { get; } + + /// + /// Gets the dimensions of the image. + /// + public Size Dimensions => new(this.Width, this.Height); + + private int Width { get; set; } + + private int Height { get; set; } + + /// + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel => throw new NotImplementedException(); + + /// + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + // Skip over the magick bytes. + stream.Read(this.buffer, 0, 4); + + // Read version number. + byte version = (byte)stream.ReadByte(); + if (version != 2) + { + ExrThrowHelper.ThrowNotSupportedVersion(); + } + + // Next three bytes contain info's about the image. + // We ignore those for now. + stream.Read(this.buffer, 0, 3); + + ExrHeader header = this.ParseHeader(stream); + + if (!header.IsValid()) + { + ExrThrowHelper.ThrowInvalidImageHeader(); + } + + if (header.Channels.Count != 3) + { + ExrThrowHelper.ThrowNotSupported("Only 3 channels are supported!"); + } + + this.Width = header.DataWindow.Value.xMax - header.DataWindow.Value.xMin + 1; + this.Height = header.DataWindow.Value.yMax - header.DataWindow.Value.yMin + 1; + + // TODO: calculate bits per pixel. + int bitsPerPixel = 48; + + return new ImageInfo(new PixelTypeInfo(bitsPerPixel), this.Width, this.Height, new ImageMetadata()); + } + + private ExrHeader ParseHeader(BufferedReadStream stream) + { + ExrAttribute attribute = this.ReadAttribute(stream); + var header = new ExrHeader(); + + while (!attribute.Equals(ExrAttribute.EmptyAttribute)) + { + switch (attribute.Name) + { + case "channels": + IList channels = this.ParseChannelList(stream, attribute.Length); + header.Channels = channels; + break; + case "compression": + var compression = (ExrCompression)stream.ReadByte(); + header.Compression = compression; + break; + case "dataWindow": + ExrBox2i dataWindow = this.ReadBox2i(stream); + header.DataWindow = dataWindow; + break; + case "displayWindow": + ExrBox2i displayWindow = this.ReadBox2i(stream); + header.DisplayWindow = displayWindow; + break; + case "lineOrder": + var lineOrder = (ExrLineOrder)stream.ReadByte(); + header.LineOrder = lineOrder; + break; + case "pixelAspectRatio": + float aspectRatio = stream.ReadSingle(this.buffer); + header.AspectRatio = aspectRatio; + break; + case "screenWindowCenter": + float screenWindowCenterX = stream.ReadSingle(this.buffer); + float screenWindowCenterY = stream.ReadSingle(this.buffer); + header.ScreenWindowCenter = new PointF(screenWindowCenterX, screenWindowCenterY); + break; + case "screenWindowWidth": + float screenWindowWidth = stream.ReadSingle(this.buffer); + header.ScreenWindowWidth = screenWindowWidth; + break; + default: + // Skip unknown attribute bytes. + stream.Skip(attribute.Length); + break; + } + + attribute = this.ReadAttribute(stream); + } + + return header; + } + + private ExrAttribute ReadAttribute(BufferedReadStream stream) + { + string attributeName = ReadString(stream); + if (attributeName.Equals(string.Empty)) + { + return ExrAttribute.EmptyAttribute; + } + + string attributeType = ReadString(stream); + + stream.Read(this.buffer, 0, 4); + int attributeSize = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + + return new ExrAttribute(attributeName, attributeType, attributeSize); + } + + private ExrBox2i ReadBox2i(BufferedReadStream stream) + { + stream.Read(this.buffer, 0, 4); + int xMin = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + + stream.Read(this.buffer, 0, 4); + int yMin = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + + stream.Read(this.buffer, 0, 4); + int xMax = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + + stream.Read(this.buffer, 0, 4); + int yMax = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + + return new ExrBox2i(xMin, yMin, xMax, yMax); + } + + private List ParseChannelList(BufferedReadStream stream, int attributeSize) + { + var channels = new List(); + while (attributeSize > 1) + { + ExrChannelInfo channelInfo = this.ReadChannelInfo(stream, out int bytesRead); + channels.Add(channelInfo); + attributeSize -= bytesRead; + } + + // Last byte should be a null byte. + int byteRead = stream.ReadByte(); + + return channels; + } + + private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesRead) + { + string channelName = ReadString(stream); + bytesRead = channelName.Length + 1; + + stream.Read(this.buffer, 0, 4); + bytesRead += 4; + var pixelType = (ExrPixelType)BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + + byte pLinear = (byte)stream.ReadByte(); + byte reserved0 = (byte)stream.ReadByte(); + byte reserved1 = (byte)stream.ReadByte(); + byte reserved2 = (byte)stream.ReadByte(); + bytesRead += 4; + + stream.Read(this.buffer, 0, 4); + bytesRead += 4; + int xSampling = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + + stream.Read(this.buffer, 0, 4); + bytesRead += 4; + int ySampling = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + + return new ExrChannelInfo(channelName, pixelType, pLinear, xSampling, ySampling); + } + + private static string ReadString(BufferedReadStream stream) + { + var str = new StringBuilder(); + int character = stream.ReadByte(); + if (character == 0) + { + // End of file header reached. + return string.Empty; + } + + while (character != 0) + { + if (character == -1) + { + ExrThrowHelper.ThrowInvalidImageHeader(); + } + + str.Append((char)character); + character = stream.ReadByte(); + } + + return str.ToString(); + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrFormat.cs b/src/ImageSharp/Formats/OpenExr/ExrFormat.cs new file mode 100644 index 0000000000..2cbce0970c --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrFormat.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using SixLabors.ImageSharp.Formats.Bmp; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Registers the image encoders, decoders and mime type detectors for the OpenExr format. + /// + public sealed class ExrFormat : IImageFormat + { + private ExrFormat() + { + } + + /// + /// Gets the current instance. + /// + public static ExrFormat Instance { get; } = new(); + + /// + public string Name => "EXR"; + + /// + public string DefaultMimeType => "image/x-exr"; + + /// + public IEnumerable MimeTypes => ExrConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => ExrConstants.FileExtensions; + + /// + public ExrMetadata CreateDefaultFormatMetadata() => new(); + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrHeader.cs b/src/ImageSharp/Formats/OpenExr/ExrHeader.cs new file mode 100644 index 0000000000..1ed464ebd2 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrHeader.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + internal class ExrHeader + { + public IList Channels { get; set; } + + public ExrCompression? Compression { get; set; } + + public ExrBox2i? DataWindow { get; set; } + + public ExrBox2i? DisplayWindow { get; set; } + + public ExrLineOrder? LineOrder { get; set; } + + public float? AspectRatio { get; set; } + + public float? ScreenWindowWidth { get; set; } + + public PointF? ScreenWindowCenter { get; set; } + + public bool IsValid() + { + if (!this.Compression.HasValue) + { + return false; + } + + if (!this.DataWindow.HasValue) + { + return false; + } + + if (!this.DisplayWindow.HasValue) + { + return false; + } + + if (!this.LineOrder.HasValue) + { + return false; + } + + if (!this.AspectRatio.HasValue) + { + return false; + } + + if (!this.ScreenWindowWidth.HasValue) + { + return false; + } + + if (!this.ScreenWindowCenter.HasValue) + { + return false; + } + + if (this.Channels is null) + { + return false; + } + + return true; + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs b/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs new file mode 100644 index 0000000000..27230306db --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Detects OpenExr file headers. + /// + public sealed class ExrImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 4; + + /// + public IImageFormat DetectFormat(ReadOnlySpan header) => this.IsSupportedFileFormat(header) ? ExrFormat.Instance : null; + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length >= this.HeaderSize) + { + int fileTypeMarker = BinaryPrimitives.ReadInt32LittleEndian(header); + return fileTypeMarker == ExrConstants.MagickBytes; + } + + return false; + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs b/src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs new file mode 100644 index 0000000000..1005414f22 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + internal enum ExrLineOrder : byte + { + IncreasingY = 0, + + DecreasingY = 1, + + RandomY = 2 + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs new file mode 100644 index 0000000000..f3723c5426 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Provides OpenExr specific metadata information for the image. + /// + public class ExrMetadata : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public ExrMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private ExrMetadata(ExrMetadata other) + { + } + + /// + public IDeepCloneable DeepClone() => new ExrMetadata(this); + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs b/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs new file mode 100644 index 0000000000..4e3427cf43 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + internal enum ExrPixelType : int + { + /// + /// unsigned int (32 bit). + /// + Uint = 0, + + /// + /// half (16 bit floating point). + /// + Half = 1, + + /// + /// float (32 bit floating point). + /// + Float = 2 + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs new file mode 100644 index 0000000000..227a961188 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Cold path optimizations for throwing exr format based exceptions. + /// + internal static class ExrThrowHelper + { + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNotSupportedVersion() => throw new NotSupportedException("Unsupported EXR version"); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNotSupported(string msg) => throw new NotSupportedException(msg); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowInvalidImageHeader() => throw new InvalidImageContentException("Invalid EXR image header"); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowInvalidImageHeader(string msg) => throw new InvalidImageContentException(msg); + } +} diff --git a/src/ImageSharp/Formats/OpenExr/IExrDecoderOptions.cs b/src/ImageSharp/Formats/OpenExr/IExrDecoderOptions.cs new file mode 100644 index 0000000000..e61ce85378 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/IExrDecoderOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Image decoder options for decoding OpenExr streams. + /// + internal interface IExrDecoderOptions + { + } +} diff --git a/src/ImageSharp/Formats/OpenExr/README.md b/src/ImageSharp/Formats/OpenExr/README.md new file mode 100644 index 0000000000..c71ab113d1 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/README.md @@ -0,0 +1,4 @@ +### Some useful links for documentation about the OpenEXR format: + +- [Technical Introduction](https://openexr.readthedocs.io/en/latest/TechnicalIntroduction.html) +- [OpenExr file layout](https://openexr.readthedocs.io/en/latest/OpenEXRFileLayout.html) \ No newline at end of file diff --git a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs index 581d3e592b..5aa6430d1c 100644 --- a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs +++ b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs @@ -1,7 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Buffers.Binary; using System.IO; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.IO; namespace SixLabors.ImageSharp.Formats.Pbm @@ -61,5 +64,28 @@ namespace SixLabors.ImageSharp.Formats.Pbm return value; } + + /// + /// Reads a float. + /// + /// The stream to read from. + /// A scratch buffer of size 4 bytes. + /// The byte order. Defaults to little endian. + /// the value. + public static float ReadSingle(this BufferedReadStream stream, Span scratch, ByteOrder byteOrder = ByteOrder.LittleEndian) + { + stream.Read(scratch, 0, 4); + int intValue; + if (byteOrder == ByteOrder.LittleEndian) + { + intValue = BinaryPrimitives.ReadInt32LittleEndian(scratch); + } + else + { + intValue = BinaryPrimitives.ReadInt32BigEndian(scratch); + } + + return Unsafe.As(ref intValue); + } } }